diff --git "a/.MEMO/1--\347\273\237\344\270\200\345\256\232\344\271\211properties\345\260\235\350\257\225\345\244\261\350\264\245.md" "b/.MEMO/1--\347\273\237\344\270\200\345\256\232\344\271\211properties\345\260\235\350\257\225\345\244\261\350\264\245.md" new file mode 100644 index 000000000..c002729ad --- /dev/null +++ "b/.MEMO/1--\347\273\237\344\270\200\345\256\232\344\271\211properties\345\260\235\350\257\225\345\244\261\350\264\245.md" @@ -0,0 +1,41 @@ + +## 未完成目标 + + +### 1、尝试将所有 `` 依赖版本号定义在同一个 pom.xml 里。 **[❌失败]** + +**尝试1:将所有 `` 定义在 `sa-token-dependencies` 里:** + +结果: 无法在 `sa-token-spring-boot2/3/4-dependencies` 中引用这些 ``,因为 ` import` 只会导入目标的 `` 版本号定义,不会导入目标的 `` 属性。 + +`` 只会在 父子结构中向下传递,不会在 ` import` 中传递。 + + +**尝试2:将所有 `` 定义在 `sa-token-parent` 里:** + +结果:在 `sa-token-dependencies` 里无法引用这些 ``,因为 `sa-token-parent` 不是 `sa-token-dependencies` 的父模块。 + +将 `sa-token-parent` 定义为 `sa-token-dependencies` 的父模块行吗? + +不行,因为在 `sa-token-parent` 通过 ` import` 导入了 `sa-token-dependencies`,如果再把 `sa-token-parent` 定义为 `sa-token-dependencies` 的父模块,会造成循环依赖。 + +执行 `mvn package` 打包时,maven 会直接报错: + +``` +[ERROR] [ERROR] Some problems were encountered while processing the POMs: +[ERROR] The dependencies of type=pom and with scope=import form a cycle: cn.dev33:sa-token-parent:1.44.0 -> cn.dev33:sa-token-basic-dependencies:1.44.0 -> cn.dev33:sa-token-basic-dependencies:1.44.0 @ cn.dev33:sa-token-basic-dependencies:1.44.0 + @ +[ERROR] The build could not read 1 project -> [Help 1] +[ERROR] +[ERROR] The project cn.dev33:sa-token-parent:1.44.0 (E:\work\project-yun\sa-token\pom.xml) has 1 error +[ERROR] The dependencies of type=pom and with scope=import form a cycle: cn.dev33:sa-token-parent:1.44.0 -> cn.dev33:sa-token-basic-dependencies:1.44.0 -> cn.dev33:sa-token-basic-dependencies:1.44.0 @ cn.dev33:sa-token-basic-dependencies:1.44.0 +[ERROR] +[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. +[ERROR] Re-run Maven using the -X switch to enable full debug logging. +[ERROR] +[ERROR] For more information about the errors and possible solutions, please read the following articles: +[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectBuildingException +``` + + + diff --git "a/.MEMO/2--2026-3-1_\350\257\241\345\274\202\350\260\203\350\257\225\350\256\260\345\275\225.txt" "b/.MEMO/2--2026-3-1_\350\257\241\345\274\202\350\260\203\350\257\225\350\256\260\345\275\225.txt" new file mode 100644 index 000000000..90539c2dd --- /dev/null +++ "b/.MEMO/2--2026-3-1_\350\257\241\345\274\202\350\260\203\350\257\225\350\256\260\345\275\225.txt" @@ -0,0 +1,133 @@ + +2026-3-1 调试记录 + +启动 SaOAuth2ServerApplication,报错空指针:SaOAuth2ServerController 文件的 SaOAuth2Strategy.instance.notLoginView 空指针,SaOAuth2Strategy.instance 为 null + + +SaOAuth2Strategy.instance 的定义为: +public static final SaOAuth2Strategy instance = new SaOAuth2Strategy(); + +看代码是无论如何也不可能空指针的,诡异。 + +在 main 方法第一句加上测试 + +@SpringBootApplication +public class SaOAuth2ServerApplication { + + public static void main(String[] args) { + System.out.println(SaOAuth2Strategy.instance); + SpringApplication.run(SaOAuth2ServerApplication.class, args); + System.out.println("\nSa-Token-OAuth2 Server端启动成功,配置如下:"); + System.out.println(SaOAuth2Manager.getServerConfig()); + } + +} + +打印居然为 null。 + +询问 AI,解释的乱七八糟,没有参考价值。 + +然后在根目录执行 mvn clean,居然无法成功。sa-token-test 模块无法 clean 。 + +报错 test 依赖不存在 + + + org.springframework.boot + spring-boot-starter-test + test + + +最后必须在 dependencyManagement 加上这个才行 + + + org.springframework.boot + spring-boot-starter-test + 2.7.18 + + +可是就算不加,我也已经在 sa-token-spring-boot2-dependencies 中定义这个依赖了呀,为什么 在 sa-token-test 中无法 import spring-boot-starter-test ? + +加了后,mvn clean 执行成功了 + +但是 mvn package 又开始无法打包。 + +可以昨天我明明能打包成功的啊?今天好像就变动了一下 sa-token-test 中的依赖配置。这有什么影响吗? + +而且打包报错信息居然是:sa-token-jboot-plugin 插件中 javax.servlet.http.HttpServletRequest 无法转换为 HttpServletRequest + +什么东西啊。 + +抓头挠腮解决不了。 + +这个插件已经十几个版本没有变动过代码了,代码不变,打包环境不变,命令不变,今天就突然报这种莫名其妙的错误,无奈,只能先去除这个插件,不让它参与打包。 + +继续打包,又开始报错: + +sa-token-jfinal-plugin 中 cn.dev33.satoken.context.SaTokenContext 无法转换为 SaTokenContext。 + +这一瞬间我怀疑自己正处于梦中。 + +纠结了半分钟,继续去除此插件,继续打包。 + +打包成功了。 + +启动 SaOAuth2ServerApplication,启动成功,SaOAuth2ServerController 文件的 SaOAuth2Strategy.instance.notLoginView 空指针问题,消失了。 + +请问中间的这几个报错和这个空指针有任何关联吗?我请问呢? + + + +注:以上所有叙述均为最后打包成功后进行回忆,可能细节上略有偏差。 + + + + + + + +两小时后: +本来可以运行成功的代码,只要一改子模块的代码就无法再运行成功,报错:java: 无法访问SaRequest。 +试了好多解决方案,不行。 + +--- +吃了两份炉盖香酥鸡饼,原来人在压力大的时候真的需要补充能量。 +--- + +继续报错: +Maven 资源编译器: 模块 'sa-token-oauth2' 所需的 Maven 项目配置不可用。仅当从 IDE 启动外部构建时,才支持 Maven 项目编译。 +sa-token-jwt、sso、sign 等模块均出现此问题 + +最后: +把项目删掉,重新下载一份,导入 +项目可以运行成功了,但是每次修改子模块,在 demo 示例里无法实时起作用。需要 mvn clean install 才能看到效果。 + +最后: +取消勾选 maven 配置项:Delegate IDE build/run actions to Maven + +一切问题解决,包括最上面的诡异调试现象也消失了。 + +idea,你给老子爬 + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/.MEMO/3--sa-token_\346\234\200\346\226\260\347\211\210\346\211\200\346\234\211\344\276\235\350\265\226.txt" "b/.MEMO/3--sa-token_\346\234\200\346\226\260\347\211\210\346\211\200\346\234\211\344\276\235\350\265\226.txt" new file mode 100644 index 000000000..a2f31bb3f --- /dev/null +++ "b/.MEMO/3--sa-token_\346\234\200\346\226\260\347\211\210\346\211\200\346\234\211\344\276\235\350\265\226.txt" @@ -0,0 +1,344 @@ + + + cn.dev33 + sa-token-bom + 1.45.0 + pom + import + + + + + cn.dev33 + sa-token-core + 1.45.0 + + + + + cn.dev33 + sa-token-spring-boot-starter + 1.45.0 + + + + + cn.dev33 + sa-token-spring-boot3-starter + 1.45.0 + + + + + cn.dev33 + sa-token-spring-boot4-starter + 1.45.0 + + + + + cn.dev33 + sa-token-reactor-spring-boot-starter + 1.45.0 + + + + + cn.dev33 + sa-token-reactor-spring-boot3-starter + 1.45.0 + + + + + cn.dev33 + sa-token-reactor-spring-boot4-starter + 1.45.0 + + + + + cn.dev33 + sa-token-jboot-plugin + 1.45.0 + + + + + cn.dev33 + sa-token-jfinal-plugin + 1.45.0 + + + + + cn.dev33 + sa-token-loveqq-boot-starter + 1.45.0 + + + + + cn.dev33 + sa-token-servlet + 1.45.0 + + + + + cn.dev33 + sa-token-jakarta-servlet + 1.45.0 + + + + + cn.dev33 + sa-token-plugin + 1.45.0 + + + + + cn.dev33 + sa-token-alone-redis + 1.45.0 + + + + + cn.dev33 + sa-token-redis-template + 1.45.0 + + + + + cn.dev33 + sa-token-redis-template-jdk-serializer + 1.45.0 + + + + + cn.dev33 + sa-token-redis-jackson + 1.45.0 + + + + + cn.dev33 + sa-token-redisson + 1.45.0 + + + + + cn.dev33 + sa-token-redisson-spring-boot-starter + 1.45.0 + + + + + cn.dev33 + sa-token-redisx + 1.45.0 + + + + + cn.dev33 + sa-token-hutool-timed-cache + 1.45.0 + + + + + cn.dev33 + sa-token-caffeine + 1.45.0 + + + + + cn.dev33 + sa-token-jackson + 1.45.0 + + + + + cn.dev33 + sa-token-jackson3 + 1.45.0 + + + + + cn.dev33 + sa-token-fastjson + 1.45.0 + + + + + cn.dev33 + sa-token-fastjson2 + 1.45.0 + + + + + cn.dev33 + sa-token-snack3 + 1.45.0 + + + + + cn.dev33 + sa-token-snack4 + 1.45.0 + + + + + cn.dev33 + sa-token-serializer-features + 1.45.0 + + + + + cn.dev33 + sa-token-thymeleaf + 1.45.0 + + + + + cn.dev33 + sa-token-freemarker + 1.45.0 + + + + + cn.dev33 + sa-token-dubbo + 1.45.0 + + + + + cn.dev33 + sa-token-dubbo3 + 1.45.0 + + + + + cn.dev33 + sa-token-grpc + 1.45.0 + + + + + cn.dev33 + sa-token-forest + 1.45.0 + + + + + cn.dev33 + sa-token-okhttps + 1.45.0 + + + + + cn.dev33 + sa-token-jwt + 1.45.0 + + + + + cn.dev33 + sa-token-temp-jwt + 1.45.0 + + + + + cn.dev33 + sa-token-oauth2 + 1.45.0 + + + + + cn.dev33 + sa-token-apikey + 1.45.0 + + + + + cn.dev33 + sa-token-sign + 1.45.0 + + + + + cn.dev33 + sa-token-quick-login + 1.45.0 + + + + + cn.dev33 + sa-token-sso + 1.45.0 + + + + + cn.dev33 + sa-token-spring-aop + 1.45.0 + + + + + cn.dev33 + sa-token-spring-el + 1.45.0 + + + + + cn.dev33 + sa-token-spring-boot-webmvc-reactor-v2v3v4-common + 1.45.0 + + + + + cn.dev33 + sa-token-spring-boot-reactor-v2v3v4-common + 1.45.0 + + + + + cn.dev33 + sa-token-spring-boot-webmvc-v3v4-common + 1.45.0 + diff --git "a/.MEMO/4--sa-token\344\276\235\350\265\226\345\205\263\347\263\273\350\215\211\347\250\277.txt" "b/.MEMO/4--sa-token\344\276\235\350\265\226\345\205\263\347\263\273\350\215\211\347\250\277.txt" new file mode 100644 index 000000000..d59cb0f19 --- /dev/null +++ "b/.MEMO/4--sa-token\344\276\235\350\265\226\345\205\263\347\263\273\350\215\211\347\250\277.txt" @@ -0,0 +1,38 @@ + + + sa-token-bom 定义 sa-token 所有自身依赖的版本 + sa-token-dependencies 定义 sa-token 所有第三方依赖版本,并继承 sa-token-bom + sa-token-spring-boot2-dependencies 定义 spring-boot2 相关依赖版本 + sa-token-spring-boot3-dependencies 定义 spring-boot3 相关依赖版本 + sa-token-spring-boot4-dependencies 定义 spring-boot4 相关依赖版本 + + sa-token-parent 父级 pom.xml 引入 sa-token-dependencies + + + + sa-token-spring-boot-webmvc-reactor-v2v3v4-common import sa-token-spring-boot2-dependencies + + sa-token-spring-boot-starter import sa-token-spring-boot2-dependencies + sa-token-reactor-spring-boot-starter import sa-token-spring-boot2-dependencies + sa-token-spring-boot-webmvc-v3v4-common import sa-token-spring-boot3-dependencies + 引入 sa-token-spring-boot-webmvc-reactor-v2v3v4-common + + + sa-token-spring-boot3-starter import sa-token-spring-boot3-dependencies + sa-token-spring-boot4-starter import sa-token-spring-boot4-dependencies + 引入 sa-token-spring-boot-webmvc-v3v4-common + + + + +A 定义了 , + +B通过如下方式引入 A: + + + A + + + +那么 A 里定义的 ,在 B 里生效吗? +不生效 \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE.md b/.gitee/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..9aa0e4420 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE.md @@ -0,0 +1,2 @@ +请在以下地址复制 issue 模板进行提交: +https://sa-token.com/doc.html#/fun/issue-template diff --git "a/.github/ISSUE_TEMPLATE/bug\345\217\215\351\246\210.md" "b/.github/ISSUE_TEMPLATE/bug\345\217\215\351\246\210.md" new file mode 100644 index 000000000..827b53155 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/bug\345\217\215\351\246\210.md" @@ -0,0 +1,22 @@ +--- +name: bug反馈 +about: 当你明确框架存在 bug 时,选择这个模板提交 +title: '' +labels: '' +assignees: '' + +--- + +### 使用版本: + + +### 报错信息: + + +### 希望结果: + + +### 复现步骤: + + +< 备注:如果复现步骤比较复杂,请将 demo 上传到 gitee 并留下地址 > diff --git "a/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\346\217\220\351\227\256.md" "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\346\217\220\351\227\256.md" new file mode 100644 index 000000000..49f1a94e7 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\346\217\220\351\227\256.md" @@ -0,0 +1,13 @@ +--- +name: 功能提问 +about: 对框架的某个功能看不明白时,选择这个模板提交 +title: '' +labels: '' +assignees: '' + +--- + +### 对以下问题有疑问: + + +< 备注:请尽量详细描述问题所在 > diff --git "a/.github/ISSUE_TEMPLATE/\345\273\272\350\256\256\345\242\236\345\212\240\346\226\260\345\212\237\350\203\275.md" "b/.github/ISSUE_TEMPLATE/\345\273\272\350\256\256\345\242\236\345\212\240\346\226\260\345\212\237\350\203\275.md" new file mode 100644 index 000000000..795542680 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\345\273\272\350\256\256\345\242\236\345\212\240\346\226\260\345\212\237\350\203\275.md" @@ -0,0 +1,16 @@ +--- +name: 建议增加新功能 +about: 当你有一个好 idea 时,选择这个模板提交 +title: '' +labels: '' +assignees: '' + +--- + +### 建议增加的新功能: + + +### 应用场景阐述: + + +< 备注:请尽量详细描述功能应用场景 > diff --git "a/.github/ISSUE_TEMPLATE/\351\242\204\346\234\237\344\270\215\347\254\246.md" "b/.github/ISSUE_TEMPLATE/\351\242\204\346\234\237\344\270\215\347\254\246.md" new file mode 100644 index 000000000..3ae76bc3a --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\351\242\204\346\234\237\344\270\215\347\254\246.md" @@ -0,0 +1,25 @@ +--- +name: 预期不符 +about: 当框架的运行结果和你的预期不一致时,选择这个模板提交 +title: '' +labels: '' +assignees: '' + +--- + +### 使用版本: + + +### 涉及的功能模块: + + +### 测试步骤: ++ 我经过以下步骤测试: + ++ 得出以下结果: + ++ 其中第 xx 行的代码输出表现 和文档上描述的不一致: + ++ 我的理解是: + +请问,是我的理解不对,还是文档出了问题? diff --git a/.gitignore b/.gitignore index 63c6ba826..4210c541e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,32 @@ unpackage/ /.factorypath .idea/ +.vscode/ +sa-token-three-plugin/ +sa-token-doc/big-file/ +.flattened-pom.xml + +.qoder +.soloncode/ +.cursor +.firecrawl/ +.agents +.claude +.github + +AGENTS.md +CLAUDE.md + +/update-log.md + +# 本地临时产出(审查报告等) +temp-file/ + +# Python 编译缓存 +__pycache__/ +*.pyc + +# 本地工具脚本(todo 审查等) +tools/ \ No newline at end of file diff --git a/sa-token-doc/doc/logo.png b/.idea/icon.png similarity index 100% rename from sa-token-doc/doc/logo.png rename to .idea/icon.png diff --git a/LICENSE b/LICENSE index f918b5acb..12ea52ab6 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2011-2019 hubin. + Copyright 2011-Present hubin. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 3172f242b..02e2cd811 100644 --- a/README.md +++ b/README.md @@ -1,250 +1,344 @@ +简体中文 | [繁體中文](./README_zh_TW.md) | [English](./README_en.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) +

- logo + logo

-

Sa-Token v1.24.0

-

这可能是史上功能最全的 Java 权限认证框架!

+

Sa-Token v1.45.0

+

✨ 开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅!

- - + + + - - + +

+ +

+ 在线文档:https://sa-token.com +  |  + 需求提交 +

+ --- +### 📝 前言: -## 在线资料 - -- [在线文档:http://sa-token.dev33.cn/](http://sa-token.dev33.cn/) - -- [需求提交:我们深知一个优秀的项目需要海纳百川,点我在线提交需求](http://sa-app.dev33.cn/wall.html?name=sa-token) - -- [开源不易,求鼓励,点个star吧 !](###) - - -## Sa-Token 介绍 -Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题 - -框架集成简单、开箱即用、API设计清爽,通过Sa-Token,你将以一种极其简单的方式实现系统的权限认证部分 - -- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录 -- **权限认证** —— 权限认证、角色认证、会话二级认证 -- **Session会话** —— 全端共享Session、单端独享Session、自定义Session -- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线 -- **账号封禁** —— 指定天数封禁、永久封禁、设定解封时间 -- **持久层扩展** —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失 -- **分布式会话** —— 提供jwt集成、共享数据中心两种分布式会话方案 -- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证 -- **单点登录** —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定 -- **OAuth2.0认证** —— 基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式 -- **二级认证** —— 在已登录的基础上再次认证,保证安全性 -- **独立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 -── sa-token - ├── sa-token-core // [核心] Sa-Token 核心模块 - ├── sa-token-starter // [整合] Sa-Token 与其它框架整合 - ├── sa-token-servlet // [整合] Sa-Token 整合 Servlet容器实现类包 - ├── sa-token-spring-boot-starter // [整合] Sa-Token 整合 SpringBoot 快速集成 - ├── sa-token-reactor-spring-boot-starter // [整合] Sa-Token 整合 Reactor响应式编程 快速集成 - ├── sa-token-solon-plugin // [整合] Sa-Token 整合 Solon 快速集成 - ├── sa-token-plugin // [插件] Sa-Token 插件合集 - ├── sa-token-dao-redis // [插件] Sa-Token 整合 Redis (使用jdk默认序列化方式) - ├── sa-token-dao-redis-jackson // [插件] Sa-Token 整合 Redis (使用jackson序列化方式) - ├── sa-token-spring-aop // [插件] Sa-Token 整合 SpringAOP 注解鉴权 - ├── sa-token-temp-jwt // [插件] Sa-Token 整合 jwt 临时令牌鉴权 - ├── sa-token-quick-login // [插件] Sa-Token 快速注入登录页插件 - ├── sa-token-alone-redis // [插件] Sa-Token 独立Redis插件,实现[权限缓存与业务缓存分离] - ├── sa-token-oauth2 // [插件] Sa-Token 实现 OAuth2.0 模块 - ├── sa-token-demo // [示例] Sa-Token 示例合集 - ├── sa-token-demo-springboot // [示例] Sa-Token 整合 SpringBoot - ├── sa-token-demo-webflux // [示例] Sa-Token 整合 WebFlux - ├── sa-token-demo-jwt // [示例] Sa-Token 集成 jwt - ├── sa-token-demo-solon // [示例] Sa-Token 集成 Solon - ├── sa-token-demo-quick-login // [示例] Sa-Token 集成 quick-login 模块 - ├── sa-token-demo-alone-redis // [示例] Sa-Token 集成 alone-redis 模块 - ├── sa-token-demo-sso1 // [示例] Sa-Token 集成 SSO单点登录-模式一 - ├── sa-token-demo-sso2-server // [示例] Sa-Token 集成 SSO单点登录-模式二 认证中心 - ├── sa-token-demo-sso2-client // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端 - ├── sa-token-demo-sso3-server // [示例] Sa-Token 集成 SSO单点登录-模式三 认证中心 - ├── sa-token-demo-sso3-client // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 - ├── sa-token-demo-oauth2-server // [示例] Sa-Token 集成 OAuth2.0 (服务端) - ├── sa-token-demo-oauth2-client // [示例] Sa-Token 集成 OAuth2.0 (客户端) - ├── sa-token-doc // [文档] Sa-Token 开发文档 - ├──pom.xml // [依赖] 顶级pom文件 -``` +回望 2020 年初,我为 Sa-Token 提交第一行代码之际,彼时市面上 Java 缺少的不仅是一个简洁好用的鉴权框架,更是一整套清晰、自洽的权限架构设计思想。 -##### Sa-Token 功能结构图 -![sa-token-js](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/x/sa-token-js3.png 's-w') +因此,这几年间我将大量时间倾注在 Sa-Token 的文档编写,几乎每一章节、每一句话、每一个字都经过反复修改、精细打磨,以求做到最清晰、干练、易懂的表述。用心阅读文档,你学习到的将不止是 Sa-Token 框架本身,更是绝大多数场景下权限设计的最佳实践。 -##### Sa-Token 认证流程图 -![sa-token-rz](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/x/sa-token-rz2.png 's-w') -## Sa-Token-SSO 单点登录 -对于单点登录,网上教程大多以CAS模式为主,其实对于不同的系统架构,实现单点登录的步骤也大为不同,Sa-Token由简入难将其划分为三种模式: +### 🛠️ Sa-Token 介绍 -| 系统架构 | 采用模式 | 简介 | 文档链接 | -| :-------- | :-------- | :-------- | :-------- | -| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie同步会话 | [文档](http://sa-token.dev33.cn/doc/index.html#/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso1) | -| 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [文档](http://sa-token.dev33.cn/doc/index.html#/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso2-server) | -| 前端不同域 + 后端 不同Redis | 模式三 | Http请求获取会话 | [文档](http://sa-token.dev33.cn/doc/index.html#/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso3-server) | +Sa-Token 是一个轻量级 Java 权限认证框架,目前拥有五大核心模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。 +**你还在手搓以下功能?Stop ⚠️ 让 Sa-Token 来!** -1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com` -2. 后端同Redis:就是指多个系统可以连接同一个Redis,其它的缓存数据中心亦可。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案,详情戳:[Alone独立Redis插件](http://sa-token.dev33.cn/doc/index.html#/plugin/alone-redis) -3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成) -4. 技术选型一定要根据系统架构对症下药,切不可胡乱选择 +![sa-token-jss](https://sa-token.com/big-file/index/intro/sa-token-jss--tran.png) +要在 SpringBoot 项目中使用 Sa-Token,你只需要在 pom.xml 中引入依赖: -## Sa-Token-SSO 特性 -1. API简单易用,文档介绍详细,且提供直接可用的集成示例 -2. 支持三种模式,不论是否跨域、是否共享Redis,都可以完美解决 -3. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝`Ticket劫持`、`Token窃取`等常见攻击手段(文档讲述攻击原理和防御手段) -4. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:`http://a.com?id=1&name=2`,登录成功之后就变成了:`http://a.com?id=1`,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面 -5. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合…… -6. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发 +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + 1.45.0 + +``` +除了支持 SpringBoot2、Sa-Token 还为 SpringBoot3/4、Solon、JFinal 等常见 Web 框架提供集成包,做到真正的开箱即用。 -## Sa-Token-OAuth2.0 授权登录 -Sa-OAuth2 模块基于 [RFC-6749 标准](https://tools.ietf.org/html/rfc6749) 编写,通过Sa-OAuth2你可以非常轻松的实现系统的OAuth2.0授权认证 -1. 授权码(Authorization Code):OAuth2.0标准授权步骤,Server端向Client端下放Code码,Client端再用Code码换取授权Token -2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server端使用URL重定向方式直接将Token下放到Client端页面 -3. 密码式(Password):Client直接拿着用户的账号密码换取授权Token -4. 客户端凭证(Client Credentials):Server端针对Client级别的Token,代表应用自身的资源授权 +
+简单示例展示:(点击展开 / 折叠) -详细参考文档:[http://sa-token.dev33.cn/doc/index.html#/oauth2/readme](http://sa-token.dev33.cn/doc/index.html#/oauth2/readme) +Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分,以登录认证为例,你只需要: +``` java +// 会话登录,参数填登录人的账号id +StpUtil.login(10001); +``` -## 代码示例 +无需实现任何接口,无需创建任何配置文件,只需要这一句静态代码的调用,便可以完成会话登录认证。 -Sa-Token的API调用非常简单,有多简单呢?以登录验证为例,你只需要: +如果一个接口需要登录后才能访问,我们只需调用以下代码: ``` java -// 在登录时写入当前会话的账号id -StpUtil.login(10001); - -// 然后在任意需要校验登录处调用以下API -// 如果当前会话未登录,这句代码会抛出 `NotLoginException`异常 +// 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常 StpUtil.checkLogin(); ``` -至此,我们已经借助Sa-Token框架完成登录授权! -此时的你小脑袋可能飘满了问号,就这么简单?自定义Realm呢?全局过滤器呢?我不用写各种配置文件吗? +在 Sa-Token 中,大多数功能都可以一行代码解决: + +踢人下线: -事实上在此我可以负责的告诉你,在Sa-Token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权! +``` java +// 将账号id为 10077 的会话踢下线 +StpUtil.kickout(10077); +``` -当你受够Shiro、Spring Security等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token的API设计是多么的清爽! +权限认证: -权限认证示例 (只有具有`user:add`权限的会话才可以进入请求) ``` java +// 注解鉴权:只有具备 `user:add` 权限的会话才可以进入方法 @SaCheckPermission("user:add") -@RequestMapping("/user/insert") public String insert(SysUser user) { - // ... - return "用户增加"; + // ... + return "用户增加"; } ``` -将某个账号踢下线 (待到对方再次访问系统时会抛出`NotLoginException`异常) -``` java -// 使账号id为10001的会话注销登录 -StpUtil.logoutByLoginId(10001); -``` +路由拦截鉴权: -除了以上的示例,Sa-Token还可以一行代码完成以下功能: ``` java -StpUtil.login(10001); // 标记当前会话登录的账号id -StpUtil.getLoginId(); // 获取当前会话登录的账号id -StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false -StpUtil.logout(); // 当前会话注销登录 -StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线) -StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false -StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false -StpUtil.getSession(); // 获取当前账号id的Session -StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session -StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值 -StpUtil.login(10001, "PC"); // 指定设备标识登录 -StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响) -StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号 +// 根据路由划分模块,不同模块不同鉴权 +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + // 更多模块... +})).addPathPatterns("/**"); ``` -Sa-Token API 众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档 +**如果您曾经使用过 Shiro、SpringSecurity,在切换到 Sa-Token 后,您将体会到质的飞跃。** + + + +
+ + +
+ 核心模块一览:(点击展开 / 折叠) + +- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。 +- **权限认证** —— 权限认证、角色认证、会话二级认证。 +- **踢人下线** —— 根据账号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。 + +
+ + + +### 🍃 SSO 单点登录 + +Sa-Token SSO 分为三种模式,可解决:`同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目` 等架构下的 SSO 认证需求: + +![sa-token-jss](https://sa-token.com/big-file/doc/sso/sa-token-sso--white.png) + + +| 系统架构 | 采用模式 | 简介 | 文档链接 | +| :-------- | :-------- |:----------------| :-------- | +| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie同步会话 | [文档](https://sa-token.com/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.com/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.com/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,共享会话数据。 +3. 如果无法做到前端同域、后端同 Redis,可以走托底的模式三:Http请求校验 ticket 获取会话。 +4. 提供:NoSdk 模式示例 + sso-server 接口文档,非 Sa-Token 项目、非 java 项目也可以对接。 +5. 提供:多重安全校验:域名校验、ticket校验、参数签名校验,有效防 ticket 劫持,防请求重放等攻击。 +6. 提供:大量实战痛点教学:sso-server 前后端分离设计、sso-client 前后端分离设计、用户数据同步/迁移方案设计。 +7. 提供:直接可运行的 demo 示例,助你快速熟悉 SSO 大致登录流程。 +8. 提供:深度细节优化,参数防丢:笔者曾试验多个SSO框架,均有参数丢失情况,比如登录前是:`http://a.com?id=1&name=2`,登录成功后就变成了:`http://a.com?id=1`,Sa-Token-SSO 内有专门算法保证了参数不丢失,登录成功后精准原路返回。 + + + + +### 🍂 OAuth2 授权认证 +Sa-Token OAuth2 模块分为四种授权模式,解决不同场景下的授权需求 + +| 授权模式 | 简介 | +| :-------- | :-------- | +| 授权码式 | OAuth2 标准授权步骤,server 端下放 code,client 端获取 code 码兑换 access_token | +| 隐藏式 | 备用选择,server 端使用 URL 重定向方式直接将 access_token 下放到 client 端页面 | +| 密码式 | client 直接拿着用户的账号密码换取授权 access_token | +| 客户端凭证式 | server 端针对 client 级别的 client_token,代表应用自身的资源授权 | -## Star 趋势 -[![giteye-chart](https://chart.giteye.net/gitee/dromara/sa-token/77YQZ6UK.png 'Gitee')](https://giteye.net/chart/77YQZ6UK) +详细参考文档:[https://sa-token.com/doc.html#/oauth2/readme](https://sa-token.com/doc.html#/oauth2/readme) -[![github-chart](https://starchart.cc/dromara/sa-token.svg 'GitHub')](https://starchart.cc/dromara/sa-token) - +七年磨一剑:五大核心模块(登录、鉴权、SSO、OAuth2、微服务) + 众多实用插件 (短 token、jwt 集成、API 参数签名、API Key 秘钥授权...) 我们提供的不只是权限认证,我们提供的是一站式解决方案。 -## 使用Sa-Token的开源项目 -[**[ sa-plus ]** 一个基于springboot架构的快速开发框架,内置代码生成器](https://gitee.com/click33/sa-plus) +**2、Sa-Token 好不好学?** -[**[ jthink ]** 一个基于springboot+sa-token+thymeleaf的博客系统](https://gitee.com/wtsoftware/jthink) +中文文档 + 中文代码注释 + 中文交流社区 + 大量实战案例博客 + 多个视频教程 + 大量优秀开源项目集成案例。 -[**[ dcy-fast ]** 一个基于springboot+sa-token+mybatis-plus的后台管理系统,前端vue-element-admin,并且内置代码生成器](https://gitee.com/dcy421/dcy-fast) -[**[ helio-starters ]** 基于JDK15 + Spring Boot 2.4 + Sa-Token + Mybatis-Plus的单体Boot版脚手架和微服务Cloud版脚手架,带有配套后台管理前端模板及代码生成器](https://gitee.com/uncarbon97/helio-starters) +**3、Sa-Token 用的人多不多?** -[**[ sa-token-plugin ]** Sa-Token第三方插件实现,基于Sa-Token-Core,提供一些与官方不同实现机制的的插件集合,作为Sa-Token开源生态的补充,针对spring-boot特性进行针对优化,更加方便于spring-boot一起使用](https://gitee.com/bootx/sa-token-plugin) +截止统计日 (2026-1-25) 起,Sa-Token 在: +- Gitee 关注量达到 48627 Star,位列平台所有推荐项目排行榜第一名。 +- GitHub 关注量达到 18523 Star,是主要竞争框架 Spring Security 的 1.97 倍,Apache Shiro 的 4.19 倍。 +- 25+ 微信粉丝群 (500人),8+ QQ粉丝群 (1000人 or 2000人) ,在线文档访问量月PV 20万+。 -如果您的项目使用了Sa-Token,欢迎提交pr +这是众多开发者用脚投票的数据,相信这些数据比任何言语都能证明 Sa-Token 的热度。 -## 友情链接 -[**[ okhttps ]** 一个轻量级http通信框架,API设计无比优雅,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps) +**4、Sa-Token 有哪些权威认证?** -[**[ 小诺快速开发平台 ]** 基于SpringBoot2 + AntDesignVue全新快速开发平台,同时拥有三个版本](https://xiaonuo.vip/index#pricing) +曾获荣誉包括但不限于:Gitee GVP 最有价值开源项目、GitCode G-Star 优质开源项目、OSCHINA 2021 人气指数 TOP 30 开源项目、OSCHINA 2022 年度最火热中国开源项目社区之一、开放原子基金会2023快速成长开源项目、 Dromara 组织顶尖项目(之一)、可信开源社区共同体预备成员、所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。 Gitee High Star 计划项目(5000+star)。Gitee 2025年度开源项目 Web应用开发 Top 2。 -[**[ Jpom ]** 简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件](https://gitee.com/dromara/Jpom) -[**[ TLog ]** 一个轻量级的分布式日志标记追踪神器](https://gitee.com/dromara/TLog) +**5、Sa-Token 收费吗?** +Sa-Token 采用 Apache-2.0 开源协议,承诺框架本身与在线文档永久免费开放。当然如果您有心赞助 Sa-Token,我们也不回避:[赞助链接](https://sa-token.com/doc.html#/more/sa-token-donate)。 +我们将定期同步赞助者名单到在线文档展示。(您需要注意的一点是:该赞助仅为友情赞助,不提供任何商业交换) -## 交流群 -QQ交流群:1002350610 [点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM) + +**6、Sa-Token 是封装的 SpringSecurity 吗?是套壳 ApacheShiro 吗?** + +不是。Sa-Token 不是一个后台模板,也不是针对 xx 框架的二次封装套壳,而是从 0 开始的纯血自研框架,核心包零依赖,完全自主可控的架构内核 + 众多主流框架的集成适配。 + + + + +#### 证书 ⭐ 奖杯 🏆 荣誉展示 + + + + + + + + + + + + + + + + +
+ + + +### 🚀 优秀开源集成案例 + +- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy):国内首个国密前后分离快速开发平台,采用 Vue3 + Vite + SpringBoot + Mp + HuTool + SaToken。 +- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus):重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Xxl-Job、knife4j、OSS 定期同步。 +- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin):SmartAdmin 国内首个以「高质量代码」为核心,「简洁、高效、安全」中后台快速开发平台。 +- [[ 橙单 ]](https://gitee.com/orangeform/orange-admin): 橙单中台化低代码生成器。可完整支持多应用、多租户、多渠道、工作流、框架技术栈自由组合等。 +- [[ 灯灯 ]](https://gitee.com/dromara/lamp-cloud): 专注于多租户解决方案的中后台快速开发平台。支持独立数据库、共享数据架构 和 非租户模式 ✨ +- [[ 拾壹博客 ]](https://gitee.com/quequnlong/shiyi-blog):一款 vue + springboot 前后端分离的博客系统。 + + + +还有更多优秀开源案例无法逐一展示,请参考:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) + + +### 🌍 其它语言版本 + +Sa-Token 社区成员贡献了多语言实现版本: + +- Rust 版本:[https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust) +- Go 版本:[https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go) +- NodeJS 版本:[https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token) +- PHP 版本:[https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token) + +我们诚邀对上述语言较为熟练的开发者,一起建设相关版本。🤝 + + +### 🔗 友情链接 +- [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps):轻量级 http 通信框架,API无比优雅,支持 WebSocket、Stomp 协议 +- [[ Forest ]](https://gitee.com/dromara/forest):声明式与编程式双修,让天下没有难以发送的 HTTP 请求 +- [[ Bean Searcher ]](https://github.com/troyzhxu/bean-searcher):REST 版的 GraphQL — 实体定义边界,参数驱动查询,一行代码实现复杂查询! +- [[ 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) +- AtomGit:[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token) + + + +### 📚 示例大全 + +**我们为框架几乎所有技术点均单独制作了对应的集成示例,此压缩包共计 60+ Demo**:涵盖 Sa-Token 登录认证、权限认证、SSO 单点登录、OAUth2 统一认证、微服务鉴权、API Key 认证、JWT集成、跨系统调用参数签名校验 等鉴权认证的方方面面。 + +下载地址:[https://sa-token.com/doc.html#/more/download-demos](https://sa-token.com/doc.html#/more/download-demos) + + + + + +### 💬 交流群 + + + +QQ交流群:1098917026 [点击加入](https://qm.qq.com/q/26OFBGd2Zy) 微信交流群: -![微信群](https://dev33-test.oss-cn-beijing.aliyuncs.com/sa-token/i-wx-qr.png ':size=230') + + + -(扫码添加微信,备注:sa-token,邀您加入群聊) +PS:扫码添加微信 (备注:sa-token),邀您加入群聊。
+ +微信群 + + +加入群聊的好处: +- 第一时间收到框架更新通知。 +- 第一时间收到框架 bug 通知。 +- 第一时间收到新增开源案例通知。 +- 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。 + diff --git a/README_en.md b/README_en.md new file mode 100644 index 000000000..b22f3c78d --- /dev/null +++ b/README_en.md @@ -0,0 +1,344 @@ +[简体中文](./README.md) | [繁體中文](./README_zh_TW.md) | English | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) + +

+ logo +

+

Sa-Token v1.45.0

+

✨ Open source, free, one-stop java permission authentication framework, making authentication simple and elegant!

+

+ + + + + + + + +

+ +

+ Online documentation: https://sa-token.com +  |  +Request submission +

+ + +--- + +### 📝 Foreword: + +Looking back at the beginning of 2020, when I submitted the first line of code for Sa-Token, what Java on the market lacked at that time was not only a simple and easy-to-use authentication framework, but also a set of clear and self-consistent permission architecture design ideas. + +Therefore, in the past few years, I have devoted a lot of time to writing Sa-Token documentation. Almost every chapter, every sentence, and every word has been repeatedly revised and finely polished in order to achieve the clearest, most concise, and easy-to-understand expression. Read the documentation carefully, and you will learn not only the Sa-Token framework itself, but also the best practices for permission design in most scenarios. + + + +### 🛠️ Sa-Token Introduction + +Sa-Token is a lightweight Java permission authentication framework that currently has five core modules: login authentication, permission authentication, single sign-on, OAuth2.0, and microservice authentication. + +**Still hand-rolling these features? Stop ⚠️ Let Sa-Token handle them!** + +![sa-token-jss](https://sa-token.com/big-file/index/intro/sa-token-jss--tran.png) + +To use Sa-Token in a SpringBoot project, you only need to introduce dependencies in pom.xml: + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + 1.45.0 + +``` + +In addition to Spring Boot 2, Sa-Token also provides integration packages for common web frameworks such as Spring Boot 3/4, Solon, and JFinal — truly out of the box. + + +
+Simple examples: (click to expand / collapse) + +Sa-Token aims to complete the permission authentication part of the system in a simple and elegant way. Taking login authentication as an example, you only need: + +``` java +// Session login, the parameter is filled in the account id of the login person +StpUtil.login(10001); +``` + +There is no need to implement any interface or create any configuration file. You only need to call this static code to complete the session login authentication. + +If an interface requires login before accessing it, we only need to call the following code: + +``` java +// Verify whether the current client has logged in, if not, throw `NotLoginException` exception +StpUtil.checkLogin(); +``` + +In Sa-Token, most functions can be solved with one line of code: + +Kick people offline: + +``` java +// Kick the session with account ID 10077 offline +StpUtil.kickout(10077); +``` + +Permission authentication: + +``` java +// Annotation authentication: Only sessions with `user:add` permission can enter the method +@SaCheckPermission("user:add") +public String insert(SysUser user) { + // ... + return "用户增加"; +} +``` + +Route interception authentication: + +``` java +// Divide modules according to routes, and different modules have different authentication +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + // More modules... +})).addPathPatterns("/**"); +``` + +**If you have used Shiro or SpringSecurity before, you will experience a qualitative leap after switching to Sa-Token.** + + + +
+ + +
+Core module list: (click to expand/collapse) + +- **Login Authentication** - Single-end login, multi-end login, same-end mutually exclusive login, no login required within seven days. +- **Permission authentication**——Permission authentication, role authentication, session secondary authentication. +- **Kick people offline** - Kick people offline based on account ID and kick people offline based on Token value. +- **Annotation Authentication** - Elegantly separates authentication from business code. +- **Route interception authentication** - Based on route interception authentication, the restful mode can be adapted. +- **Session** - Shared Session for all ends, exclusive Session for one end, customized Session, convenient access to values. +- **Persistence layer extension** - Redis can be integrated, and data will not be lost after restarting. +- **Separation of front and backend** - APPs, applets and other terminals that do not support cookies can also be easily authenticated. +- **Token style customization** - six built-in Token styles, you can also customize the Token generation strategy. +- **Remember Me Mode** - Adapt to [Remember Me] mode and restart the browser without verification. +- **Second Level Authentication** - Authentication again based on logged in to ensure security. +- **Impersonate other accounts** - Manipulate any user status data in real time. +- **Temporary identity switch** - Temporarily switch the session identity to another account. +- **Same-end mutually exclusive login** - Like QQ, the mobile phone and computer are online at the same time, but login is mutually exclusive on both mobile phones. +- **Account Banning** - Login ban, business classification ban, and penalty ladder ban. +- **Password Encryption** - Provides basic encryption algorithms, which can quickly encrypt MD5, SHA1, SHA256, and AES. +- **Session Query** - Provides a convenient and flexible session query interface. +- **Http Basic Authentication** - One line of code to access Http Basic and Digest authentication. +- **Global Listener** - Perform some AOP operations during key operations such as user login, logout, and being kicked offline. +- **Global Filter** - Conveniently handle cross-domain, global setting of security response first and other operations. +- **Multiple Account System Authentication** - Multiple accounts in one system are authenticated separately (such as the User table and Admin table of the mall) +- **Single Sign-On** - Built-in three single sign-on modes: same domain, cross-domain, same Redis, cross-Redis, front-end and back-end separation and other architectures can be handled. +- **Single point logout** - Initiate a logout in any subsystem, and the entire system will be offline. +- **OAuth2.0 authentication** - Easily build OAuth2.0 service, supporting openid mode. +- **Distributed Session** - Provides a distributed session solution in a shared data center. +- **Microservice Gateway Authentication** - Adapted to route interception authentication of common gateways such as Gateway, ShenYu, Zuul and so on. +- **RPC call authentication** - Gateway forwarding authentication, RPC call authentication, so that service calls are no longer left unprotected +- **Temporary Token Authentication** - Solve the short-term Token authorization problem. +- **Independent Redis** - Separate permission cache and business cache. +- **Quick Login Authentication** - Inject a login page into the project with zero code. +- **Tag Dialect** - Provides Thymeleaf tag dialect integration package and provides beetl integration examples. +- **jwt integration** - Provides three modes of jwt integration solutions and provides token expansion parameter capabilities. +- **RPC call status transfer** - Provides integration packages such as dubbo and grpc, so that the login status is not lost when RPC is called. +- **Parameter Signature** - Provides a cross-system API call signature verification module to prevent parameter tampering and request replay. +- **Automatic Renewal** - Provides two Token expiration strategies, which can be used flexibly and automatically renewed. +- **Out-of-the-box** - Provides common framework integration packages such as SpringMVC, WebFlux, Solon, etc., ready-to-use. +- **Latest technology stack** - Adapt to the latest technology stack: support SpringBoot 3.x, jdk 17. + +
+ + + +### 🍃 SSO single sign-on + +Sa-Token SSO is divided into three modes, which can solve the SSO authentication requirements under `同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目` and other architectures: + +![sa-token-jss](https://sa-token.com/big-file/doc/sso/sa-token-sso--white.png) + + +| System Architecture | Adoption Patterns | Introduction | Documentation Links | +| :-------- | :-------- |:----------------| :-------- | +| Front-end in the same domain + back-end in the same domain as Redis | Mode 1 | Shared cookie synchronization session | [Document](https://sa-token.com/doc.html#/sso/sso-type1), [Example](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client) | +| Different domains on the front end + the same backend as Redis | Mode 2 | URL redirection propagation session | [Document](https://sa-token.com/doc.html#/sso/sso-type2), [Example](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso2-client) | +| Different front-end domains + different back-end Redis | Mode 3 | HTTP request to obtain session | [Document](https://sa-token.com/doc.html#/sso/sso-type3), [Example](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso3-client) | + + +1. Front-end same domain: means that multiple systems can be deployed under the same main domain name, such as: `c1.domain.com`, `c2.domain.com`, `c3.domain.com` +2. The backend is the same as Redis: This means that multiple systems can connect to the same Redis and share session data. +3. If it is impossible to ensure that the front-end is in the same domain and the back-end is in the same Redis, you can use the third mode of support: Http request verification ticket to obtain the session. +4. Provided: NoSdk mode example + sso-server interface document, non-Sa-Token projects and non-java projects can also be connected. +5. Provide: multiple security verification: domain name verification, ticket verification, parameter signature verification, effectively preventing ticket hijacking, request replay and other attacks. +6. Provide: A large number of practical pain point teachings: sso-server front-end and back-end separation design, sso-client front-end and front-end separation design, user data synchronization/migration solution design. +7. Provide: Directly runnable demo examples to help you quickly become familiar with the general SSO login process. +8. Provide: In-depth detail optimization, parameter loss prevention: The author has tested multiple SSO frameworks, and all parameters have been lost. For example, before logging in, it was: `http://a.com?id=1&name=2`, and after successful login, it became: `http://a.com?id=1`. Sa-Token-SSO has a special algorithm to ensure that parameters are not lost, and the original path is accurately returned after successful login. + + + + +### 🍂 OAuth2 authorization authentication +The Sa-Token OAuth2 module is divided into four authorization modes to solve the authorization needs in different scenarios. + +| Authorization Model | Introduction | +| :-------- | :-------- | +| Authorization code format | OAuth2 standard authorization steps, the server side releases the code, and the client side obtains the code code and redeems it for access_token | +| Hidden | Alternative option, the server uses URL redirection to directly transfer the access_token to the client page | +| Password type | The client directly exchanges the user's account password for authorization access_token | +| Client certificate type | The server-side client_token for the client level represents the resource authorization of the application itself | + +Detailed reference document: [https://sa-token.com/doc.html#/oauth2/readme](https://sa-token.com/doc.html#/oauth2/readme) + + +### 📖❓ Q&A + +**1. Does Sa-Token have a complete feature set?** + +Seven years of hard work: five core modules (login, authentication, SSO, OAuth2, microservices) + many practical plug-ins (short token, jwt integration, API parameter signature, API Key authorization...) We provide not only permission authentication, we provide a one-stop solution. + + +**2. Is Sa-Token easy to learn?** + +Chinese documentation + Chinese code comments + Chinese communication community + a large number of practical case blogs + multiple video tutorials + a large number of outstanding open source project integration cases. + + +**3. Are there many people using Sa-Token?** + +As of the statistics date (2026-1-25), Sa-Token is in: + +- Gitee followers reached 48627 Stars, ranking first in the list of all recommended projects on the platform. +- GitHub attention reached 18523 Stars, which is 1.97 times that of the main competing framework Spring Security and 4.19 times that of Apache Shiro. +- 25+ WeChat fan groups (500 people), 8+ QQ fan groups (1000 people or 2000 people), online document visits with monthly PV of 200,000+. + +This is the data that many developers voted with their feet. I believe these data can prove the popularity of Sa-Token better than any words. + + +**4. What authoritative certification does Sa-Token have?** + +Honors include but are not limited to: Gitee GVP Most Valuable Open Source Project, GitCode G-Star High-Quality Open Source Project, OSCHINA 2021 Popularity Index TOP 30 Open Source Project, OSCHINA 2022 One of the Most Popular Chinese Open Source Project Communities, Open Atomic Foundation 2023 Fast-Growing Open Source Project, Dromara Organization Top Project (One), Preparatory Member of the Trusted Open Source Community Community, and the open source community "Dromara" where he works Won the second prize in the "2024 China Internet Development Innovation and Investment Competition (Open Source)". Gitee High Star project (5000+star). Gitee's Top 2 Open Source Projects for Web Application Development in 2025. + + +**5. Is there any charge for Sa-Token?** + +Sa-Token adopts the Apache-2.0 open source protocol and promises that the framework itself and online documents will always be free and open. Of course, if you are interested in sponsoring Sa-Token, we will not shy away from it: [Sponsorship Link](https://sa-token.com/doc.html#/more/sa-token-donate). +We will regularly synchronize the sponsor list to the online document display. (One thing you need to note is: this sponsorship is only a friendly sponsorship and does not provide any commercial exchange) + + +**6. Is Sa-Token an encapsulated SpringSecurity? Is it a shell of ApacheShiro?** + +No. Sa-Token is not a backend template, nor is it a secondary encapsulation shell for the xx framework, but a pure self-developed framework starting from scratch, with zero dependencies on the core package, a completely independent and controllable architecture core + integrated adaptation of many mainstream frameworks. + + + + +#### Certificate ⭐ Trophy 🏆 Honor Display + + + + + + + + + + + + + + + + +
+ + + +### 🚀 Excellent open source integration cases + +- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy): China’s first rapid development platform that separates national secrets before and after, using Vue3 + Vite + SpringBoot + MP + HuTool + SaToken. +- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus): Rewrite all functions of RuoYi-Vue and integrate Sa-Token, Mybatis-Plus, Xxl-Job, knife4j, and OSS for regular synchronization. +- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin): SmartAdmin is the first domestic rapid development platform with "high-quality code" as the core and "simple, efficient and safe" middle and backend. +- [[ Chengdan ]](https://gitee.com/orangeform/orange-admin): Chengdan’s low-code generator in Taiwan. It can fully support multi-applications, multi-tenants, multi-channels, workflows, free combination of framework technology stacks, etc. +- [[ Deng Deng ]](https://gitee.com/dromara/lamp-cloud): A mid- and back-end rapid development platform focusing on multi-tenant solutions. Support independent database, shared data architecture and non-tenant mode ✨ +- [[ Shiyi Blog ]](https://gitee.com/quequnlong/shiyi-blog): A vue + springboot blog system that separates the front and back ends. + + + +There are more excellent open source cases that cannot be displayed one by one, please refer to: [Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) + + +### 🌍 Other language versions + +Sa-Token community members contributed multi-language implementation versions: + +- Rust version: [https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust) +- Go version: [https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go) +- NodeJS version: [https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token) +- PHP version: [https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token) + +We sincerely invite developers who are more proficient in the above languages to build relevant versions together. 🤝 + + +### 🔗 Friendly links +- [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps): lightweight http communication framework, extremely elegant API, supports WebSocket and Stomp protocols +- [[ Forest ]](https://gitee.com/dromara/forest): Declarative and programmatic dual cultivation, making the world no longer difficult to send HTTP requests +- [[ Bean Searcher ]](https://github.com/troyzhxu/bean-searcher): REST version of GraphQL — entity definition boundaries, parameter-driven queries, and one line of code to implement complex queries! +- [[ Jpom ]](https://gitee.com/dromara/Jpom): Simple and lightweight low-intrusive online construction, automatic deployment, daily operation and maintenance, and project monitoring software. +- [[ TLog ]](https://gitee.com/dromara/TLog): A lightweight distributed log tag tracking artifact. +- [[ hippo4j ]](https://gitee.com/agentart/hippo4j): A powerful dynamic thread pool framework with monitoring and alarm functions. +- [[ hertzbeat ]](https://gitee.com/dromara/hertzbeat): An easy-to-use and friendly open source real-time monitoring and alarm system, no Agent required, high-performance cluster, and powerful custom monitoring capabilities. +- [[ Solon ]](https://gitee.com/noear/solon): A more modern application development framework: faster, smaller, and more free. +- [[ Chat2DB ]](https://github.com/chat2db/Chat2DB): An AI-driven database management and BI tool that supports the management of 22 databases such as Mysql, pg, Oracle, and Redis. + + + +### 📦 Code hosting +- 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) +- AtomGit:[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token) + + + +### 📚 Collection of examples + +**We have separately produced corresponding integration examples for almost all technical points of the framework. This compressed package has a total of 60+ Demo**: covering all aspects of authentication and authentication such as Sa-Token login authentication, authority authentication, SSO single sign-on, OAUth2 unified authentication, microservice authentication, API Key authentication, JWT integration, cross-system call parameter signature verification, etc. + +Download address: [https://sa-token.com/doc.html#/more/download-demos](https://sa-token.com/doc.html#/more/download-demos) + + + + + +### 💬 Communication group + + + +QQ communication group: 1098917026 [Click to join](https://qm.qq.com/q/26OFBGd2Zy) + +WeChat communication group: + + + + + +PS: Scan the QR code to add WeChat (remark: sa-token) to invite you to join the group chat. + +
+ +微信群 + + +Benefits of joining a group chat: +- Receive framework update notifications as soon as possible. +- Receive framework bug notifications as soon as possible. +- Receive notifications of new open source cases as soon as possible. +- Communicate (huá shuǐ) with each other (mō yú) with many big guys 🖐️🐟️. + diff --git a/README_ja.md b/README_ja.md new file mode 100644 index 000000000..9cfbf4cff --- /dev/null +++ b/README_ja.md @@ -0,0 +1,344 @@ +[简体中文](./README.md) | [繁體中文](./README_zh_TW.md) | [English](./README_en.md) | 日本語 | [한국어](./README_ko.md) | [Русский](./README_ru.md) + +

+ logo +

+

Sa-Token v1.45.0

+

✨ オープンソース、無料、ワンストップの Java 権限認証フレームワークで、認証をシンプルかつエレガントにします。

+

+ + + + + + + + +

+ +

+ オンライン ドキュメント: https://sa-token.com +  |  +送信リクエスト +

+ + +--- + +### 📝 序文: + +私が Sa-Token のコードの最初の行を提出した 2020 年の初めを振り返ると、当時市場に出ていた Java に欠けていたのは、シンプルで使いやすい認証フレームワークだけでなく、明確で自己一貫性のあるパーミッション アーキテクチャの設計アイデアのセットでもありました。 + +したがって、ここ数年、私は Sa-Token ドキュメントの作成に多くの時間を費やしてきました。ほぼすべての章、すべての文、すべての単語が、最も明確で簡潔でわかりやすい表現を達成するために、繰り返し修正され、細かく磨き上げられています。ドキュメントを注意深く読むと、Sa-Token フレームワーク自体だけでなく、ほとんどのシナリオにおける権限設計のベスト プラクティスについても学ぶことができます。 + + + +### 🛠️ SAトークンの紹介 + +Sa-Token は、軽量の Java 権限認証フレームワークであり、現在、ログイン認証、権限認証、シングル サインオン、OAuth2.0、およびマイクロサービス認証の 5 つのコア モジュールを備えています。 + +**以下の機能をまだ使用していますか?Stop ⚠️ Sa-Token にお任せを!** + +![sa-token-jss](https://sa-token.com/big-file/index/intro/sa-token-jss--tran.png) + +SpringBoot プロジェクトで Sa-Token を使用するには、pom.xml に依存関係を導入するだけです。 + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + 1.45.0 + +``` + +SpringBoot2 と Sa-Token のサポートに加えて、SpringBoot3/4、Solon、JFinal などの一般的な Web フレームワークの統合パッケージも提供しており、すぐに使用できるようになります。 + + +
+簡単な表示例: (クリックして展開/折りたたみ) + +Sa-Token は、システムの許可認証部分をシンプルかつエレガントな方法で完了することを目的としています。ログイン認証を例に挙げると、必要なのは以下だけです。 + +``` java +// セッションログイン。パラメータにはログイン者のアカウントIDが入力されます。 +StpUtil.login(10001); +``` + +インターフェースを実装したり、構成ファイルを作成したりする必要はありません。セッションのログイン認証を完了するには、この静的コードを呼び出すだけです。 + +インターフェイスにアクセスする前にログインが必要な場合は、次のコードを呼び出すだけで済みます。 + +``` java +// 現在のクライアントがログインしているかどうかを確認し、ログインしていない場合は `NotLoginException` 例外をスローします +StpUtil.checkLogin(); +``` + +Sa-Token では、ほとんどの関数は 1 行のコードで解決できます。 + +人々をオフラインに追い出す: + +``` java +// アカウント ID 10077 のセッションをオフラインにキックします +StpUtil.kickout(10077); +``` + +権限認証: + +``` java +// アノテーション認証: `user:add` 権限を持つセッションのみがメソッドに入ることができます +@SaCheckPermission("user:add") +public String insert(SysUser user) { + // ... + return "用户增加"; +} +``` + +ルートインターセプト認証: + +``` java +// ルートに応じてモジュールを分割し、モジュールごとに認証が異なります +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + // さらにモジュール... +})).addPathPatterns("/**"); +``` + +**以前にShiroまたはSpringSecurityを使用したことがある場合は、Sa-Tokenに切り替えると質的な飛躍を経験するでしょう。** + + + +
+ + +
+コアモジュールリスト: (クリックして展開/折りたたみ) + +- **ログイン認証** - シングルエンド ログイン、マルチエンド ログイン、同一エンド相互排他ログイン、7 日以内はログイン不要。 +- **権限認証**——権限認証、ロール認証、セッション二次認証。 +- **ユーザーをオフラインにキックする** - アカウント ID に基づいてユーザーをオフラインにキックし、トークンの値に基づいてユーザーをオフラインにキックします。 +- **アノテーション認証** - 認証をビジネス コードからエレガントに分離します。 +- **ルート インターセプト認証** - ルート インターセプト認証に基づいて、Restful モードを適応させることができます。 +- **セッション** - すべてのエンドの共有セッション、一方のエンドの排他的セッション、カスタマイズされたセッション、値への便利なアクセス。 +- **永続層拡張機能** - Redis を統合でき、再起動後にデータが失われることはありません。 +- **フロントエンドとバックエンドの分離** - Cookie をサポートしていない APP、アプレット、その他の端末も簡単に認証できます。 +- **トークン スタイルのカスタマイズ** - 6 つの組み込みトークン スタイル。トークン生成戦略をカスタマイズすることもできます。 +- **Remember Me モード** - [Remember Me] モードに適応し、検証せずにブラウザを再起動します。 +- **第 2 レベルの認証** - セキュリティを確保するために、ログインに基づいて再度認証を行います。 +- **他の人のアカウントを模倣** - ユーザー ステータス データをリアルタイムで操作します。 +- **一時的な ID 切り替え** - セッション ID を別のアカウントに一時的に切り替えます。 +- **同一エンドの相互排他ログイン** - QQ と同様、携帯電話とコンピュータは同時にオンラインですが、ログインは両方の携帯電話で相互排他的です。 +- **アカウント禁止** - ログイン禁止、ビジネス分類禁止、およびペナルティラダー禁止。 +- **パスワード暗号化** - MD5、SHA1、SHA256、および AES を迅速に暗号化できる基本的な暗号化アルゴリズムを提供します。 +- **セッション クエリ** - 便利で柔軟なセッション クエリ インターフェイスを提供します。 +- **HTTP 基本認証** - HTTP 基本認証およびダイジェスト認証にアクセスするための 1 行のコード。 +- **グローバル リスナー** - ユーザーのログイン、ログアウト、オフラインへのキックなどの主要な操作中に、一部の AOP 操作を実行します。 +- **グローバル フィルター** - クロスドメイン、最初のセキュリティ応答のグローバル設定、およびその他の操作を便利に処理します。 +- **複数アカウント システム認証** - 1 つのシステム内の複数のアカウントは個別に認証されます (モールのユーザー テーブルと管理者テーブルなど) +- **シングル サインオン** - 3 つのシングル サインオン モードが組み込まれています: 同一ドメイン、クロスドメイン、同一 Redis、クロス Redis、フロントエンドとバックエンドの分離、およびその他のアーキテクチャを処理できます。 +- **シングル ポイント ログアウト** - 任意のサブシステムでログアウトを開始すると、システム全体がオフラインになります。 +- **OAuth2.0 認証** - openid モードをサポートする OAuth2.0 サービスを簡単に構築します。 +- **分散セッション** - 共有データセンターで分散セッション ソリューションを提供します。 +- **マイクロサービス ゲートウェイ認証** - Gateway、ShenYu、Zuul などの一般的なゲートウェイのルート インターセプト認証に適合します。 +- **RPC 呼び出し認証** - ゲートウェイ転送認証、RPC 呼び出し認証により、サービス呼び出しが無防備なままにならないようにします。 +- **一時的なトークン認証** - 短期的なトークン認証の問題を解決します。 +- **独立した Redis** - 権限キャッシュとビジネス キャッシュを分離します。 +- **Quick クイックログイン認証** - ゼロコードでログイン ページをプロジェクトに挿入します。 +- **タグ方言** - Thymeleaf タグ方言統合パッケージを提供し、beetl 統合サンプルを提供します。 +- **jwt 統合** - jwt 統合ソリューションの 3 つのモードを提供し、トークン拡張パラメータ機能を提供します。 +- **RPC 呼び出し状態の転送** - RPC 呼び出し時にログイン状態が失われないように、dubbo や grpc などの統合パッケージを提供します。 +- **パラメータ署名** - パラメータの改ざんとリクエストのリプレイを防止するために、クロスシステム API 呼び出し署名検証モジュールを提供します。 +- **自動更新** - 柔軟に使用でき、自動的に更新できる 2 つのトークン有効期限戦略を提供します。 +- **すぐに使える** - SpringMVC、WebFlux、Solon など、すぐに使用できる一般的なフレームワーク統合パッケージを提供します。 +- **最新のテクノロジー スタック** - 最新のテクノロジー スタックに適応: SpringBoot 3.x、jdk 17 をサポートします。 + +
+ + + +### 🍃 SSO シングル サインオン + +Sa-Token SSO は 3 つのモードに分かれており、`同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目` およびその他のアーキテクチャでの SSO 認証要件を解決できます。 + +![sa-token-jss](https://sa-token.com/big-file/doc/sso/sa-token-sso--white.png) + + +|システムアーキテクチャ |導入パターン |はじめに |ドキュメントへのリンク | +| :-------- | :-------- |:----------------| :-------- | +|同じドメイン内のフロントエンド + Redis と同じドメイン内のバックエンド |モード 1 |共有 Cookie 同期セッション | [文書](https://sa-token.com/doc.html#/sso/sso-type1)、[例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client) | +|フロントエンドの異なるドメイン + Redis と同じバックエンド |モード 2 | URL リダイレクト伝播セッション | [文書](https://sa-token.com/doc.html#/sso/sso-type2)、[例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso2-client) | +|異なるフロントエンド ドメイン + 異なるバックエンド Redis |モード 3 |セッションを取得するための HTTP リクエスト | [文書](https://sa-token.com/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 に接続してセッション データを共有できることを意味します。 +3. フロントエンドが同じドメイン内にあり、バックエンドが同じ Redis 内にあることを確認できない場合は、3 番目のサポート モードである Http リクエスト検証チケットを使用してセッションを取得できます。 +4. 提供: NoSdk モードのサンプル + sso サーバー インターフェイスのドキュメント、非 Sa-Token プロジェクトおよび非 Java プロジェクトも接続できます。 +5. 複数のセキュリティ検証を提供します: ドメイン名検証、チケット検証、パラメータ署名検証、チケットハイジャック、リクエストリプレイ、その他の攻撃を効果的に防止します。 +6. 提供: 実践的な問題点に関する多数の教示: sso サーバーのフロントエンドとバックエンドの分離設計、sso クライアントのフロントエンドとフロントエンドの分離設計、ユーザー データの同期/移行ソリューションの設計。 +7. 提供: 一般的な SSO ログイン プロセスにすぐに慣れるのに役立つ、直接実行可能なデモ サンプル。 +8. 提供: 詳細な最適化、パラメーター損失の防止: 著者は複数の SSO フレームワークをテストしましたが、すべてのパラメーターが失われました。たとえば、ログイン前は `http://a.com?id=1&name=2` でしたが、ログインに成功すると `http://a.com?id=1` になりました。 Sa-Token-SSO には、パラメーターが失われないようにする特別なアルゴリズムがあり、ログイン成功後に元のパスが正確に返されます。 + + + + +### 🍂 OAuth2認可認証 +Sa-Token OAuth2 モジュールは、さまざまなシナリオでの承認ニーズを解決するために 4 つの承認モードに分割されています。 + +|認可モデル |はじめに | +| :-------- | :-------- | +|認証コードの形式 | OAuth2 標準の認証手順では、サーバー側がコードを解放し、クライアント側がコード code を取得して、それを access_token | と引き換えます。 +|非表示 |代替オプションとして、サーバーは URL リダイレクトを使用して、access_token をクライアント ページに直接転送します。 +|パスワードの種類 |クライアントは、認証のためにユーザーのアカウントのパスワードを直接交換します。 +|クライアント証明書の種類 |クライアント レベルのサーバー側 client_token は、アプリケーション自体のリソース認可を表します。 + +詳細な参考資料: [https://sa-token.com/doc.html#/oauth2/readme](https://sa-token.com/doc.html#/oauth2/readme) + + +### 📖❓ Q&A + +**1. Sa-Token の機能は十分ですか?** + +7 年間の努力: 5 つのコア モジュール (ログイン、認証、SSO、OAuth2、マイクロサービス) + 多くの実用的なプラグイン (ショート トークン、jwt 統合、API パラメーターの署名、API キー認証...) 権限認証だけでなく、ワンストップ ソリューションを提供します。 + + +**2. Sa-Tokenは簡単に習得できますか?** + +中国語のドキュメント + 中国語のコード コメント + 中国語のコミュニケーション コミュニティ + 多数の実践的な事例ブログ + 複数のビデオ チュートリアル + 多数の優れたオープンソース プロジェクト統合事例。 + + +**3. Sa-Tokenを使っている人は多いのでしょうか?** + +統計日 (2026-1-25) の時点で、Sa-Token は次のとおりです。 + +- Gitee フォロワーは 48627 スターに達し、プラットフォーム上のすべての推奨プロジェクトのリストで 1 位にランクされました。 +- GitHub の注目度は 18523 スターに達し、これは主要な競合フレームワーク Spring Security の 1.97 倍、Apache Taro の 4.19 倍です。 +- 25 個以上の WeChat ファン グループ (500 人)、8 個以上の QQ ファン グループ (1000 人または 2000 人)、オンライン ドキュメント アクセス (月間 PV は 200,000 人以上)。 + +これは多くの開発者が自分の足で投票したデータです。これらのデータがSa-Tokenの人気をどんな言葉よりも証明できると信じています。 + + +**4. Sa-Token はどのような権威ある認証を持っていますか?** + +栄誉には、Gitee GVP Most Valuable オープンソース プロジェクト、GitCode G-Star 高品質オープンソース プロジェクト、OSCHINA 2021 人気指数 TOP 30 オープンソース プロジェクト、OSCHINA 2022 中国で最も人気のあるオープンソース プロジェクト コミュニティの 1 つ、Open Atomic Foundation 2023 急成長オープンソース プロジェクト、Dromara Organization トップ プロジェクト (One)、Trusted Open Source Community Community の準備メンバー、および自身が活動するオープンソースコミュニティ「Dromara」「2024年中国インターネット発展イノベーション・投資コンペティション(オープンソース)」で準優勝を受賞。 Gitee ハイスター プロジェクト (5000+スター)。 2025 年の Web アプリケーション開発における Gitee のトップ 2 オープンソース プロジェクト。 + + +**5. Sa-Tokenには料金がかかりますか?** + +Sa-Token は、Apache-2.0 オープン ソース プロトコルを採用し、フレームワーク自体とオンライン ドキュメントが常に無料でオープンであることを約束します。もちろん、Sa-Token のスポンサーに興味がある場合は、遠慮はしません: [スポンサーリンク](https://sa-token.com/doc.html#/more/sa-token-donate)。 +スポンサーリストをオンラインドキュメント表示と定期的に同期させます。 (注意すべき点が 1 つあります。このスポンサーシップは単なる友好的なスポンサーシップであり、商業的な交流を提供するものではありません) + + +**6. Sa-Token はカプセル化された SpringSecurity ですか? ApacheShiroのシェルでしょうか?** + +いいえ。 Sa-Token はバックエンド テンプレートでも、xx フレームワークの二次カプセル化シェルでもありません。コア パッケージへの依存関係がなく、完全に独立した制御可能なアーキテクチャ コア + 多くの主流フレームワークの統合適応を備えた、ゼロから開始する純粋な自社開発フレームワークです。 + + + + +#### 証明書 ⭐ トロフィー 🏆 栄誉の展示 + + + + + + + + + + + + + + + + +
+ + + +### 🚀 優れたオープンソース統合事例 + +- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy): Vue3 + Vite + SpringBoot + MP + HuTool + SaToken を使用して、国家機密を前後に分離する中国初の高速開発プラットフォーム。 +- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus): RuoYi-Vue のすべての機能を書き直し、定期的な同期のために Sa-Token、Mybatis-Plus、Xxl-Job、knife4j、OSS を統合します。 +- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin): SmartAdmin は、「高品質のコード」をコアとして、「シンプル、効率的、安全」なミドルエンドとバックエンドを備えた初の国内迅速開発プラットフォームです。 +- [[ Chengdan ]](https://gitee.com/orangeform/orange-admin): 台湾の Chengdan のローコード ジェネレーター。マルチアプリケーション、マルチテナント、マルチチャネル、ワークフロー、フレームワーク技術スタックの自由な組み合わせなどを完全にサポートできます。 +- [[ Deng Deng ]](https://gitee.com/dromara/lamp-cloud): マルチテナント ソリューションに焦点を当てたミッドエンドおよびバックエンドの迅速な開発プラットフォーム。独立したデータベース、共有データ アーキテクチャ、非テナント モードをサポートします ✨ +- [[ Shiyi Blog ]](https://gitee.com/quequnlong/shiyi-blog): フロントエンドとバックエンドを分離した vue + springboot ブログ システム。 + + + +1つずつ表示しきれない優れたオープンソース事例は他にもあります。[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token)を参照してください。 + + +### 🌍 他の言語バージョン + +Sa-Token コミュニティのメンバーは、多言語実装バージョンを提供しました。 + +- Rust バージョン: [https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust) +- Go バージョン: [https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go) +- NodeJS バージョン: [https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token) +- PHP バージョン: [https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token) + +上記の言語に精通した開発者を心から招待し、関連するバージョンを一緒に構築してください。 🤝 + + +### 🔗 フレンドリーリンク +- [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps): 軽量の http 通信フレームワーク、非常にエレガントな API、WebSocket および Stomp プロトコルをサポート +- [[ Forest ]](https://gitee.com/dromara/forest): 宣言型とプログラム型の二重栽培により、HTTP リクエストの送信が難しくなくなりました。 +- [[ Bean Searcher ]](https://github.com/troyzhxu/bean-searcher): GraphQL の REST バージョン — エンティティ定義境界、パラメーター駆動クエリ、および複雑なクエリを実装するための 1 行のコード! +- [[ Jpom ]](https://gitee.com/dromara/Jpom): シンプルで軽量、低侵入型のオンライン構築、自動展開、日常の運用と保守、およびプロジェクト監視ソフトウェア。 +- [[ TLog ]](https://gitee.com/dromara/TLog): 軽量の分散ログタグ追跡アーティファクト。 +- [[ hippo4j ]](https://gitee.com/agentart/hippo4j): 監視およびアラーム機能を備えた強力な動的スレッド プール フレームワーク。 +- [[ hertzbeat ]](https://gitee.com/dromara/hertzbeat): 使いやすくフレンドリーなオープンソースのリアルタイム監視および警報システム、エージェント不要、高性能クラスター、強力なカスタム監視機能。 +- [[ Solon ]](https://gitee.com/noear/solon): より現代的なアプリケーション開発フレームワーク: より速く、より小さく、より無料。 +- [[ Chat2DB ]](https://github.com/chat2db/Chat2DB): Mysql、pg、Oracle、Redis など 22 のデータベースの管理をサポートする AI 主導のデータベース管理および BI ツール。 + + + +### 📦 コードホスティング +- 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) +- AtomGit:[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token) + + + +### 📚 事例集 + +**フレームワークのほぼすべての技術点に対応する統合例を別途作成しました。この圧縮パッケージには合計 60 以上のデモ**が含まれており、Sa-Token ログイン認証、権限認証、SSO シングル サインオン、OAUth2 統合認証、マイクロサービス認証、API キー認証、JWT 統合、システム間呼び出しパラメータ署名検証など、認証と認証のあらゆる側面をカバーしています。 + +ダウンロードアドレス: [https://sa-token.com/doc.html#/more/download-demos](https://sa-token.com/doc.html#/more/download-demos) + + + + + +### 💬コミュニケーショングループ + + + +QQ コミュニケーション グループ: 1098917026 [クリックして参加](https://qm.qq.com/q/26OFBGd2Zy) + +WeChatコミュニケーショングループ: + + + + + +PS: QR コードをスキャンして WeChat (注釈: sa-token) を追加し、グループ チャットへの参加を招待します。 + +
+ +微信群 + + +グループチャットに参加するメリット: +- フレームワークの更新通知をできるだけ早く受け取ります。 +- フレームワークのバグ通知をできるだけ早く受け取ります。 +- 新しいオープンソース ケースの通知をできるだけ早く受け取ります。 +- 多くの偉い人たち 🖐️🐟️ とお互いに (mō yú) コミュニケーション (huá shuiq) しましょう。 + diff --git a/README_ko.md b/README_ko.md new file mode 100644 index 000000000..0f8b9b5a7 --- /dev/null +++ b/README_ko.md @@ -0,0 +1,344 @@ +[简体中文](./README.md) | [繁體中文](./README_zh_TW.md) | [English](./README_en.md) | [日本語](./README_ja.md) | 한국어 | [Русский](./README_ru.md) + +

+ logo +

+

Sa-Token v1.45.0

+

✨ 오픈 소스, 무료, 원스톱 Java 권한 인증 프레임워크로 인증을 간단하고 우아하게 만듭니다!

+

+ + + + + + + + +

+ +

+온라인 문서: https://sa-token.com +  |  +요구 사항 제출 +

+ + +--- + +### 📝 서문: + +2020년 초를 되돌아보면 Sa-Token의 첫 번째 코드 라인을 제출했을 때 당시 시장에 출시된 Java에는 간단하고 사용하기 쉬운 인증 프레임워크뿐 아니라 명확하고 일관된 권한 아키텍처 설계 아이디어가 부족했습니다. + +따라서 지난 몇 년 동안 저는 Sa-Token 문서를 작성하는 데 많은 시간을 투자했습니다. 가장 명확하고 간결하며 이해하기 쉬운 표현을 얻기 위해 거의 모든 장, 모든 문장, 모든 단어가 반복적으로 수정되고 세밀하게 다듬어졌습니다. 설명서를 주의 깊게 읽으면 Sa-Token 프레임워크 자체뿐만 아니라 대부분의 시나리오에서 권한 설계에 대한 모범 사례도 배울 수 있습니다. + + + +### 🛠️ Sa-Token 소개 + +Sa-Token은 현재 로그인 인증, 권한 인증, Single Sign-On, OAuth2.0 및 마이크로서비스 인증의 5가지 핵심 모듈이 있는 경량 Java 권한 인증 프레임워크입니다. + +**아직도 다음 기능을 사용하고 계시나요? 그만 ⚠️ Sa-Token을 오게 해주세요!** + +![sa-token-jss](https://sa-token.com/big-file/index/intro/sa-token-jss--tran.png) + +SpringBoot 프로젝트에서 Sa-Token을 사용하려면 pom.xml에 종속성을 도입하기만 하면 됩니다. + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + 1.45.0 + +``` + +SpringBoot2 및 Sa-Token을 지원하는 것 외에도 SpringBoot3/4, Solon 및 JFinal과 같은 일반적인 웹 프레임워크에 대한 통합 패키지도 제공하므로 즉시 사용할 수 있습니다. + + +
+간단한 디스플레이 예: (확장/축소하려면 클릭) + +Sa-Token은 간단하고 우아한 방식으로 시스템의 권한 인증 부분을 완성하는 것을 목표로 합니다. 로그인 인증을 예로 들면 다음만 필요합니다. + +``` java +// 세션 로그인, 매개변수는 로그인하는 사람의 계정 ID로 채워집니다. +StpUtil.login(10001); +``` + +인터페이스를 구현하거나 구성 파일을 만들 필요가 없습니다. 세션 로그인 인증을 완료하려면 이 정적 코드만 호출하면 됩니다. + +인터페이스에 액세스하기 전에 로그인이 필요한 경우 다음 코드만 호출하면 됩니다. + +``` java +// 현재 클라이언트가 로그인되어 있는지 확인하십시오. 로그인되어 있지 않으면 `NotLoginException` 예외가 발생합니다. +StpUtil.checkLogin(); +``` + +Sa-Token에서는 대부분의 기능을 한 줄의 코드로 해결할 수 있습니다. + +사람들을 오프라인으로 몰아내기: + +``` java +// 계정 ID 10077로 오프라인에서 세션 시작 +StpUtil.kickout(10077); +``` + +권한 인증: + +``` java +// 주석 인증: `user:add` 권한이 있는 세션만 메서드에 들어갈 수 있습니다. +@SaCheckPermission("user:add") +public String insert(SysUser user) { + // ... + return "用户增加"; +} +``` + +경로 차단 인증: + +``` java +// 경로에 따라 모듈을 나누고, 모듈마다 인증이 다릅니다. +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + // 더 많은 모듈... +})).addPathPatterns("/**"); +``` + +**이전에 Shiro 또는 SpringSecurity를 사용한 적이 있다면 Sa-Token으로 전환한 후 질적 도약을 경험하게 될 것입니다.** + + + +
+ + +
+핵심 모듈 개요: (확장/축소하려면 클릭) + +- **로그인 인증** - 단일 엔드 로그인, 멀티 엔드 로그인, 동일 엔드 상호 배타적 로그인, 7일 이내에는 로그인이 필요하지 않습니다. +- **권한 인증**——권한 인증, 역할 인증, 세션 보조 인증. +- **오프라인 추방** - 계정 ID를 기준으로 사람들을 오프라인으로 추방하고 토큰 가치를 기준으로 사람들을 오프라인으로 추방합니다. +- **주석 인증** - 인증을 비즈니스 코드와 우아하게 분리합니다. +- **경로차단 인증** - 경로차단 인증을 기반으로 Restful 모드를 적용할 수 있습니다. +- **세션** - 모든 끝을 위한 공유 세션, 한쪽 끝을 위한 독점 세션, 맞춤형 세션, 가치에 대한 편리한 접근. +- **지속성 레이어 확장** - Redis를 통합할 수 있으며 다시 시작해도 데이터가 손실되지 않습니다. +- **프런트엔드와 백엔드 분리** - 쿠키를 지원하지 않는 앱, 애플릿, 기타 단말기도 쉽게 인증할 수 있습니다. +- **토큰 스타일 사용자 정의** - 6가지 기본 제공 토큰 스타일, 토큰 생성 전략을 사용자 정의할 수도 있습니다. +- **기억하기 모드** - [기억하기] 모드에 적응하고 확인 없이 브라우저를 다시 시작합니다. +- **2차 인증** - 보안 확보를 위해 로그인 기반으로 다시 인증합니다. +- **다른 사람의 계정을 모방** - 사용자 상태 데이터를 실시간으로 조작합니다. +- **임시 ID 전환** - 세션 ID를 일시적으로 다른 계정으로 전환합니다. +- **동일 엔드 상호 배타적 로그인** - QQ와 마찬가지로 휴대폰과 컴퓨터가 동시에 온라인 상태이지만 로그인은 두 휴대폰 모두에서 상호 배타적입니다. +- **계정 차단** - 로그인 차단, 업종 분류 차단, 페널티 래더 차단. +- **비밀번호 암호화** - MD5, SHA1, SHA256, AES를 빠르게 암호화할 수 있는 기본 암호화 알고리즘을 제공합니다. +- **세션 쿼리** - 편리하고 유연한 세션 쿼리 인터페이스를 제공합니다. +- **Http 기본 인증** - Http 기본 및 다이제스트 인증에 액세스하기 위한 코드 한 줄입니다. +- **전역 리스너** - 사용자 로그인, 로그아웃, 오프라인 추방과 같은 주요 작업 중에 일부 AOP 작업을 수행합니다. +- **글로벌 필터** - 크로스 도메인, 보안 대응 우선 글로벌 설정 및 기타 작업을 편리하게 처리합니다. +- **다중 계정 시스템 인증** - 하나의 시스템에서 여러 계정을 별도로 인증합니다. (몰의 사용자 테이블, 관리 테이블 등) +- **Single Sign-On** - 3가지 Single Sign-On 모드 내장: 동일한 도메인, 교차 도메인, 동일한 Redis, 교차 Redis, 프런트엔드 및 백엔드 분리 및 기타 아키텍처를 처리할 수 있습니다. +- **단일 지점 로그아웃** - 모든 하위 시스템에서 로그아웃을 시작하면 전체 시스템이 오프라인이 됩니다. +- **OAuth2.0 인증** - openid 모드를 지원하는 OAuth2.0 서비스를 쉽게 구축할 수 있습니다. +- **분산 세션** - 공유 데이터 센터에서 분산 세션 솔루션을 제공합니다. +- **마이크로서비스 게이트웨이 인증** - Gateway, ShenYu, Zuul 등과 같은 일반 게이트웨이의 경로 차단 인증에 적합합니다. +- **RPC 호출 인증** - 게이트웨이 전달 인증, RPC 호출 인증을 통해 서비스 호출이 더 이상 무방비 상태가 되지 않도록 함 +- **임시 토큰 인증** - 단기 토큰 인증 문제를 해결합니다. +- **독립적인 Redis** - 권한 캐시와 비즈니스 캐시를 분리합니다. +- **Quick 빠른 로그인 인증** - 제로 코드로 프로젝트에 로그인 페이지를 삽입합니다. +- **Tag Dialect** - Thymeleaf 태그 Dialect 통합 패키지를 제공하고 beetl 통합 예제를 제공합니다. +- **jwt 통합** - jwt 통합 솔루션의 세 가지 모드를 제공하고 토큰 확장 매개변수 기능을 제공합니다. +- **RPC 호출 상태 전송** - RPC 호출 시 로그인 상태가 손실되지 않도록 dubbo, grpc 등의 통합 패키지를 제공합니다. +- **매개변수 서명** - 매개변수 변조를 방지하고 재생을 요청하기 위해 시스템 간 API 호출 서명 확인 모듈을 제공합니다. +- **자동 갱신** - 유연하게 자동으로 갱신할 수 있는 두 가지 토큰 만료 전략을 제공합니다. +- **즉시 사용 가능** - 즉시 사용 가능한 SpringMVC, WebFlux, Solon 등과 같은 공통 프레임워크 통합 패키지를 제공합니다. +- **최신 기술 스택** - 최신 기술 스택에 적응: SpringBoot 3.x, jdk 17을 지원합니다. + +
+ + + +### 🍃 SSO 싱글 사인온 + +Sa-Token SSO는 `同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目`과 같은 아키텍처에서 SSO 인증 요구 사항을 해결할 수 있는 세 가지 모드로 구분됩니다. + +![sa-token-jss](https://sa-token.com/big-file/doc/sso/sa-token-sso--white.png) + + +| 시스템 아키텍처 | 채택 패턴 | 소개 | 문서 링크 | +| :-------- | :-------- |:----------------| :-------- | +| Redis와 동일한 도메인의 프런트엔드 + 동일한 도메인의 백엔드 | 모드 1 | 공유 쿠키 동기화 세션 | [문서](https://sa-token.com/doc.html#/sso/sso-type1), [예제](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client) | +| 프런트 엔드의 다른 도메인 + Redis와 동일한 백엔드 | 모드 2 | URL 리디렉션 전파 세션 | [문서](https://sa-token.com/doc.html#/sso/sso-type2), [예제](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso2-client) | +| 다양한 프런트엔드 도메인 + 다양한 백엔드 Redis | 모드 3 | 세션을 얻기 위한 HTTP 요청 | [문서](https://sa-token.com/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에 연결하고 세션 데이터를 공유할 수 있음을 의미합니다. +3. 프런트엔드가 동일한 도메인에 있고 백엔드가 동일한 Redis에 있는지 확인할 수 없는 경우 세 번째 지원 모드인 Http 요청 확인 티켓을 사용하여 세션을 얻을 수 있습니다. +4. 제공 : NoSdk 모드 예시 + sso-server 인터페이스 문서, Sa-Token이 아닌 프로젝트, Java가 아닌 프로젝트도 연결 가능합니다. +5. 제공: 다중 보안 확인: 도메인 이름 확인, 티켓 확인, 매개변수 서명 확인, 티켓 하이재킹, 요청 재생 및 기타 공격을 효과적으로 방지합니다. +6. 제공: 다양한 실제 문제점 교육: sso-서버 프런트엔드 및 백엔드 분리 설계, sso-클라이언트 프런트엔드 및 프런트엔드 분리 설계, 사용자 데이터 동기화/마이그레이션 솔루션 설계. +7. 제공: 일반 SSO 로그인 프로세스에 빠르게 익숙해지는 데 도움이 되는 직접 실행 가능한 데모 예제입니다. +8. 제공: 심층적인 세부 최적화, 매개변수 손실 방지: 저자는 여러 SSO 프레임워크를 테스트했으며 모든 매개변수가 손실되었습니다. 예를 들어, 로그인하기 전에는 `http://a.com?id=1&name=2`이었고, 로그인에 성공한 후에는 `http://a.com?id=1`이 되었습니다. Sa-Token-SSO에는 매개변수가 손실되지 않고 로그인 성공 후 원래 경로가 정확하게 반환되도록 보장하는 특별한 알고리즘이 있습니다. + + + + +### 🍂 OAuth2 인증 인증 +Sa-Token OAuth2 모듈은 다양한 시나리오의 인증 요구 사항을 해결하기 위해 4가지 인증 모드로 구분됩니다. + +| 인증 모델 | 소개 | +| :-------- | :-------- | +| 인증 코드 형식 | OAuth2 표준 인증 단계에서 서버측은 코드를 릴리스하고 클라이언트측은 코드 코드를 획득하여 access_token | +| 숨겨진 | 대체 옵션으로, 서버는 URL 리디렉션을 사용하여 access_token을 클라이언트 페이지로 직접 전송합니다. | +| 비밀번호 유형 | 클라이언트는 인증을 위해 사용자의 계정 비밀번호를 직접 교환합니다 access_token | +| 클라이언트 인증서 유형 | 클라이언트 레벨의 서버측 client_token은 애플리케이션 자체의 자원 인증을 나타냅니다. | + +자세한 참조 문서: [https://sa-token.com/doc.html#/oauth2/readme](https://sa-token.com/doc.html#/oauth2/readme) + + +### 📖❓Q&A + +**1. Sa-Token 기능은 충분한가요?** + +7년간의 노력: 5개의 핵심 모듈(로그인, 인증, SSO, OAuth2, 마이크로서비스) + 많은 실용적인 플러그인(짧은 토큰, jwt 통합, API 매개변수 서명, API 키 인증...) 권한 인증뿐만 아니라 원스톱 솔루션을 제공합니다. + + +**2. Sa-Token은 배우기 쉬운가요?** + +중국어 문서 + 중국어 코드 주석 + 중국어 커뮤니케이션 커뮤니티 + 수많은 실제 사례 블로그 + 여러 비디오 튜토리얼 + 수많은 우수한 오픈 소스 프로젝트 통합 사례. + + +**3. Sa-Token을 사용하는 사람들이 많이 있나요?** + +통계일(2026-1-25) 현재 Sa-Token의 위치는 다음과 같습니다. + +- Gitee 팔로어는 48627개의 별에 도달하여 플랫폼의 모든 추천 프로젝트 목록에서 1위를 차지했습니다. +- GitHub 관심은 18523 Stars에 도달했는데, 이는 주요 경쟁 프레임워크인 Spring Security의 1.97배, Apache Shiro의 4.19배입니다. +- 25개 이상의 WeChat 팬 그룹(500명), 8개 이상의 QQ 팬 그룹(1000명 또는 2000명), 월간 PV 200,000+ 이상의 온라인 문서 방문. + +많은 개발자들이 발로 투표한 데이터입니다. 나는 이 데이터가 어떤 말보다 Sa-Token의 인기를 더 잘 증명할 수 있다고 믿습니다. + + +**4. Sa-Token은 어떤 권위 있는 인증을 보유하고 있나요?** + +명예에는 Gitee GVP 최우수 오픈 소스 프로젝트, GitCode G-Star 고품질 오픈 소스 프로젝트, OSCHINA 2021 인기 지수 TOP 30 오픈 소스 프로젝트, OSCHINA 2022 가장 인기 있는 중국 오픈 소스 프로젝트 커뮤니티 중 하나, Open Atomic Foundation 2023 빠르게 성장하는 오픈 소스 프로젝트, Dromara 조직 상위 프로젝트(1개), 신뢰할 수 있는 오픈 소스 커뮤니티의 예비 회원 및 그가 활동하고 있는 오픈소스 커뮤니티 'Dromara'가 '2024년 중국 인터넷 발전 혁신 및 투자 대회(오픈소스)'에서 2등상을 수상했습니다. Gitee High Star 프로젝트(5000+star). 2025년 웹 애플리케이션 개발을 위한 Gitee의 상위 2개 오픈 소스 프로젝트입니다. + + +**5. Sa-Token에는 요금이 부과되나요?** + +Sa-Token은 Apache-2.0 오픈 소스 프로토콜을 채택하고 프레임워크 자체와 온라인 문서가 항상 무료이고 개방적임을 약속합니다. 물론 Sa-Token 후원에 관심이 있으시면 [후원 링크](https://sa-token.com/doc.html#/more/sa-token-donate)를 주저하지 않으셔도 됩니다. +우리는 후원자 목록을 온라인 문서 표시와 정기적으로 동기화합니다. (주의하셔야 할 점은 본 후원은 우호적인 후원일 뿐 어떠한 상업적인 교류도 제공하지 않는다는 점입니다) + + +**6. Sa-Token은 캡슐화된 SpringSecurity입니까? ApacheShiro의 쉘입니까?** + +아니요. Sa-Token은 백엔드 템플릿도 아니고 xx 프레임워크를 위한 보조 캡슐화 셸도 아니지만 처음부터 시작하는 순수한 자체 개발 프레임워크이며 코어 패키지에 대한 종속성이 전혀 없으며 완전히 독립적이고 제어 가능한 아키텍처 코어 + 많은 주류 프레임워크의 통합 적응입니다. + + + + +#### 인증서 ⭐ 트로피 🏆 명예 전시 + + + + + + + + + + + + + + + + +
+ + + +### 🚀 우수한 오픈소스 통합 사례 + +- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy): Vue3 + Vite + SpringBoot + MP + HuTool + SaToken을 사용하여 국가 기밀 전후를 분리하는 중국 최초의 급속 개발 플랫폼입니다. +- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus): RuoYi-Vue의 모든 기능을 다시 작성하고 정기적인 동기화를 위해 Sa-Token, Mybatis-Plus, Xxl-Job, Knife4j 및 OSS를 통합합니다. +- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin) : SmartAdmin은 "고품질 코드"를 핵심으로 하고 "간단하고 효율적이며 안전한" 미들과 백엔드를 갖춘 국내 최초의 고속 개발 플랫폼입니다. +- [[ Orange Order ]](https://gitee.com/orangeform/orange-admin): Orange Order는 대만의 로우 코드 생성기입니다. 다중 애플리케이션, 다중 테넌트, 다중 채널, 워크플로우, 프레임워크 기술 스택의 자유로운 조합 등을 완벽하게 지원할 수 있습니다. +- [[ Deng Deng ]](https://gitee.com/dromara/lamp-cloud): 멀티 테넌트 솔루션에 초점을 맞춘 미드엔드 및 백엔드 신속한 개발 플랫폼입니다. 독립 데이터베이스, 공유 데이터 아키텍처 및 비테넌트 모드 지원 ✨ +- [[ Shiyi 블로그 ]](https://gitee.com/quequnlong/shiyi-blog): 프런트엔드와 백엔드를 분리하는 vue + springboot 블로그 시스템입니다. + + + +하나씩 표시할 수 없는 우수한 오픈소스 사례가 더 있습니다. [Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token)을 참조하세요. + + +### 🌍 다른 언어 버전 + +Sa-Token 커뮤니티 회원은 다국어 구현 버전에 기여했습니다. + +- Rust 버전: [https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust) +- Go 버전: [https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go) +- NodeJS 버전: [https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token) +- PHP 버전: [https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token) + +위의 언어에 좀 더 능숙한 개발자들이 함께 관련 버전을 만들어 보도록 진심으로 초대합니다. 🤝 + + +### 🔗 친절한 링크 +- [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps): 경량 http 통신 프레임워크, 매우 우아한 API, WebSocket 및 Stomp 프로토콜 지원 +- [[ Forest ]](https://gitee.com/dromara/forest): 선언적 및 프로그래밍적 이중 재배로 세상이 더 이상 HTTP 요청을 보내는 것이 어렵지 않게 만듭니다. +- [[ Bean Searcher ]](https://github.com/troyzhxu/bean-searcher): GraphQL의 REST 버전 - 엔터티는 경계를 정의하고 매개변수 기반 쿼리 및 복잡한 쿼리를 한 줄의 코드로 구현할 수 있습니다! +- [[ Jpom ]](https://gitee.com/dromara/Jpom): 간단하고 가벼운 저침해 온라인 구축, 자동 배포, 일일 운영 및 유지 관리, 프로젝트 모니터링 소프트웨어. +- [[ TLog ]](https://gitee.com/dromara/TLog): 경량 분산 로그 태그 추적 아티팩트입니다. +- [[ hippo4j ]](https://gitee.com/agentart/hippo4j): 모니터링 및 경보 기능을 갖춘 강력한 동적 스레드 풀 프레임워크입니다. +- [[ hertzbeat ]](https://gitee.com/dromara/hertzbeat): 사용하기 쉽고 친숙한 오픈 소스 실시간 모니터링 및 경보 시스템, 에이전트가 필요하지 않음, 고성능 클러스터 및 강력한 사용자 정의 모니터링 기능. +- [[ Solon ]](https://gitee.com/noear/solon): 보다 현대적인 애플리케이션 개발 프레임워크: 더 빠르고, 더 작고, 더 무료입니다. +- [[ Chat2DB ]](https://github.com/chat2db/Chat2DB): Mysql, pg, Oracle, Redis 등 22개의 데이터베이스 관리를 지원하는 AI 기반 데이터베이스 관리 및 BI 도구입니다. + + + +### 📦 코드 호스팅 +- 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) +- AtomGit:[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token) + + + +### 📚 예시 모음 + +**우리는 프레임워크의 거의 모든 기술적 사항에 대해 해당 통합 예제를 별도로 제작했습니다. 이 압축 패키지에는 Sa-Token 로그인 인증, 권한 인증, SSO 싱글 사인온, OAUth2 통합 인증, 마이크로서비스 인증, API 키 인증, JWT 통합, 시스템 간 호출 매개변수 서명 확인 등과 같은 인증 및 인증의 모든 측면을 다루는 총 60개 이상의 데모**가 있습니다. + +다운로드 주소: [https://sa-token.com/doc.html#/more/download-demos](https://sa-token.com/doc.html#/more/download-demos) + + + + + +### 💬 커뮤니케이션 그룹 + + + +QQ 커뮤니케이션 그룹: 1098917026 [가입하려면 클릭](https://qm.qq.com/q/26OFBGd2Zy) + +WeChat 커뮤니케이션 그룹: + + + + + +추신: QR 코드를 스캔하여 WeChat(비고: sa-token)을 추가하여 그룹 채팅에 참여하도록 초대하세요. + +
+ +微信群 + + +그룹 채팅에 참여하면 얻을 수 있는 이점: +- 가능한 한 빨리 프레임워크 업데이트 알림을 받으세요. +- 가능한 한 빨리 프레임워크 버그 알림을 받습니다. +- 새로운 오픈소스 사례에 대한 알림을 최대한 빨리 받아보세요. +- 많은 덩치🖐️🐟️와 서로(모유) 소통(화수)합니다. + diff --git a/README_ru.md b/README_ru.md new file mode 100644 index 000000000..2cc846400 --- /dev/null +++ b/README_ru.md @@ -0,0 +1,344 @@ +[简体中文](./README.md) | [繁體中文](./README_zh_TW.md) | [English](./README_en.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | Русский + +

+ logo +

+

Sa-Token v1.45.0

+

✨ Бесплатная универсальная платформа аутентификации Java-разрешений с открытым исходным кодом, делающая аутентификацию простой и элегантной!

+

+ + + + + + + + +

+ +

+ Онлайн-документация: https://sa-token.com +  |  +Отправка запроса +

+ + +--- + +### 📝 Предисловие: + +Оглядываясь назад на начало 2020 года, когда я представил первую строку кода для Sa-Token, я понимаю, что Java на рынке в то время не хватало не только простой и удобной в использовании инфраструктуры аутентификации, но и набора четких и самосогласованных идей проектирования архитектуры разрешений. + +Поэтому в последние несколько лет я посвятил много времени написанию документации Sa-Token. Почти каждая глава, каждое предложение и каждое слово были неоднократно переработаны и тщательно отшлифованы, чтобы добиться максимально ясного, лаконичного и легкого для понимания выражения. Внимательно прочитайте документацию, и вы изучите не только саму структуру Sa-Token, но и лучшие практики проектирования разрешений в большинстве сценариев. + + + +### 🛠️ Введение Sa-Token + +Sa-Token — это облегченная платформа аутентификации разрешений Java, которая в настоящее время имеет пять основных модулей: аутентификация входа, аутентификация разрешений, единый вход, OAuth2.0 и аутентификация микросервиса. + +**Вы все еще используете следующие функции? Стоп ⚠️ Пусть поможет Sa-Token!** + +![sa-token-jss](https://sa-token.com/big-file/index/intro/sa-token-jss--tran.png) + +Чтобы использовать Sa-Token в проекте SpringBoot, вам нужно всего лишь ввести зависимости в pom.xml: + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + 1.45.0 + +``` + +Помимо поддержки SpringBoot2 и Sa-Token, он также предоставляет пакеты интеграции для распространенных веб-платформ, таких как SpringBoot3/4, Solon и JFinal, что делает его по-настоящему пригодным для использования прямо из коробки. + + +
+Простой пример отображения: (нажмите, чтобы развернуть/свернуть) + +Sa-Token стремится завершить часть системы аутентификации разрешений простым и элегантным способом. Если взять в качестве примера аутентификацию при входе, вам нужно всего лишь: + +``` java +// Вход в сеанс, параметр заполняется идентификатором учетной записи пользователя, вошедшего в систему. +StpUtil.login(10001); +``` + +Нет необходимости реализовывать какой-либо интерфейс или создавать какой-либо файл конфигурации. Вам нужно только вызвать этот статический код для завершения аутентификации входа в сеанс. + +Если интерфейс требует входа в систему перед доступом к нему, нам нужно всего лишь вызвать следующий код: + +``` java +// Проверьте, вошел ли текущий клиент в систему, если нет, выдайте исключение `NotLoginException`. +StpUtil.checkLogin(); +``` + +В Sa-Token большинство функций можно решить одной строкой кода: + +Выкидывать людей из сети: + +``` java +// Отключить сеанс с идентификатором учетной записи 10077 в автономном режиме. +StpUtil.kickout(10077); +``` + +Аутентификация разрешения: + +``` java +// Аутентификация аннотаций: только сеансы с разрешением `user:add` могут войти в этот метод. +@SaCheckPermission("user:add") +public String insert(SysUser user) { + // ... + return "用户增加"; +} +``` + +Аутентификация перехвата маршрута: + +``` java +// Разделите модули по маршрутам, и разные модули имеют разную аутентификацию. +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + // Еще модули... +})).addPathPatterns("/**"); +``` + +**Если вы раньше использовали Shiro или SpringSecurity, после перехода на Sa-Token вы испытаете качественный скачок.** + + + +
+ + +
+Список основных модулей: (нажмите, чтобы развернуть/свернуть) + +- **Аутентификация при входе** - Односторонний вход, многосторонний вход, взаимоисключающий вход на одном конце, вход в систему не требуется в течение семи дней. +- **Аутентификация разрешений** ——Аутентификация разрешений, аутентификация ролей, вторичная аутентификация сеанса. +- **Выключать людей из сети**. Выключать людей из сети на основе идентификатора учетной записи и исключать людей из сети на основе значения токена. +- **Аутентификация по аннотациям** — элегантно отделяет аутентификацию от бизнес-кода. +- **Аутентификация перехвата маршрута** - На основе аутентификации перехвата маршрута можно адаптировать под RESTful-режим. +- **Сеанс** — общий сеанс для всех концов, эксклюзивный сеанс для одного конца, настраиваемый сеанс, удобный доступ к значениям. +- **Расширение уровня сохранения**. Redis можно интегрировать, и данные не будут потеряны после перезапуска. +- **Разделение внешней и внутренней части**. Приложения, апплеты и другие терминалы, не поддерживающие файлы cookie, также можно легко аутентифицировать. +- **Настройка стиля токена** — шесть встроенных стилей токенов, вы также можете настроить стратегию генерации токенов. +- **Режим «Запомнить меня»** — адаптируйтесь к режиму [Запомнить меня] и перезапустите браузер без проверки. +- **Аутентификация второго уровня** — Повторная аутентификация на основе входа в систему для обеспечения безопасности. +- **Имитация учетных записей других людей** - Манипулируйте любыми данными о статусе пользователя в режиме реального времени. +- **Временное переключение удостоверения** — Временное переключение удостоверения сеанса на другую учетную запись. +- **Взаимоисключающий вход с одного и того же конца** - Как и в случае с QQ, мобильный телефон и компьютер находятся в сети одновременно, но вход в систему является взаимоисключающим на обоих мобильных телефонах. +- **Блокировка учетной записи**. Блокировка входа в систему, блокировка бизнес-класса и блокировка по шкале штрафов. +- **Шифрование паролей**. Предоставляет базовые алгоритмы шифрования, позволяющие быстро шифровать MD5, SHA1, SHA256 и AES. +- **Запрос сеанса**. Предоставляет удобный и гибкий интерфейс запроса сеанса. +- **Базовая аутентификация HTTP** — одна строка кода для доступа к базовой и дайджест-аутентификации Http. +- **Глобальный прослушиватель**. Выполнение некоторых операций АОП во время ключевых операций, таких как вход пользователя в систему, выход из системы и выход из сети. +- **Глобальный фильтр**. Удобная обработка междоменных, глобальных настроек ответа безопасности в первую очередь и других операций. +- **Системная аутентификация с несколькими учетными записями**. Несколько учетных записей в одной системе аутентифицируются отдельно (например, таблица пользователей и таблица администраторов торгового центра). +- **Единый вход**. Встроены три режима единого входа: тот же домен, междоменный, тот же Redis, перекрестный Redis, разделение внешнего и внутреннего интерфейса, а также могут обрабатываться другие архитектуры. +- **Одноточечный выход** — инициируйте выход из системы в любой подсистеме, и вся система перейдет в автономный режим. +- **Аутентификация OAuth2.0** - Легко создавать службу OAuth2.0, поддерживающую режим openid. +- **Распределенный сеанс** — предоставляет решение для распределенного сеанса в общем центре обработки данных. +- **Аутентификация микросервисного шлюза** - Адаптировано для аутентификации перехвата маршрутов общих шлюзов, таких как Gateway, ShenYu, Zuul и т. д. +- **Аутентификация вызовов RPC** - Аутентификация пересылки шлюза, аутентификация вызовов RPC, чтобы вызовы сервисов больше не оставались без защиты +- **Временная аутентификация по токену** - Решите краткосрочную проблему авторизации по токену. +- **Независимый Redis** - Отдельный кеш разрешений и бизнес-кеш. +- **Quick — быстрая аутентификация входа** - Добавьте в проект страницу входа без кода. +- **Диалект тегов** — предоставляет пакет интеграции диалекта тегов Thymeleaf и примеры интеграции Beetl. +- **Интеграция jwt** — предоставляет три режима решений интеграции jwt и предоставляет возможности расширения параметров токена. +- **Передача статуса вызова RPC** - Предоставляет пакеты интеграции, такие как dubbo и grpc, чтобы статус входа не терялся при вызове RPC. +- **Подпись параметра**. Предоставляет межсистемный модуль проверки подписи вызова API для предотвращения подделки параметров и повторного запроса. +- **Автоматическое продление**. Обеспечивает две стратегии истечения срока действия токена, которые можно гибко использовать и автоматически продлевать. +- **Готово** - Предоставляет готовые к использованию общие пакеты интеграции с инфраструктурой, такие как SpringMVC, WebFlux, Solon и т. д. +- **Новейший стек технологий**. Адаптируйтесь к новейшему стеку технологий: поддержка SpringBoot 3.x, jdk 17. + +
+ + + +### 🍃 Единый вход + +Единый вход Sa-Token разделен на три режима, которые могут удовлетворить требования аутентификации единого входа в `同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目` и других архитектурах: + +![sa-token-jss](https://sa-token.com/big-file/doc/sso/sa-token-sso--white.png) + + +| Системная архитектура | Модели принятия | Введение | Ссылки на документацию | +| :-------- | :-------- |:----------------| :-------- | +| Интерфейсная часть в том же домене + серверная часть в том же домене, что и Redis | Режим 1 | Общий сеанс синхронизации файлов cookie | [Документ](https://sa-token.com/doc.html#/sso/sso-type1), [Пример](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client) | +| Разные домены во внешнем интерфейсе + тот же серверный интерфейс, что и у Redis | Режим 2 | Сеанс распространения перенаправления URL | [Документ](https://sa-token.com/doc.html#/sso/sso-type2), [Пример](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso2-client) | +| Разные внешние домены + разные внутренние Redis | Режим 3 | HTTP-запрос для получения сеанса | [Документ](https://sa-token.com/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 и совместно использовать данные сеанса. +3. Если невозможно гарантировать, что интерфейсная часть находится в одном домене, а серверная часть находится в одном Redis, вы можете использовать третий режим поддержки: HTTP-запрос проверочного билета для получения сеанса. +4. Предоставлено: пример режима NoSdk + документ интерфейса sso-сервера, также можно подключить проекты, не относящиеся к Sa-Token, и проекты, не относящиеся к Java. +5. Обеспечить: множественную проверку безопасности: проверку доменного имени, проверку билета, проверку подписи параметров, эффективное предотвращение перехвата билетов, повтора запроса и других атак. +6. Предоставить: Большое количество практических уроков по болевым точкам: проектирование разделения внешнего и внутреннего интерфейса sso-сервера, проектирование разделения внешнего и внешнего интерфейса sso-клиента, разработка решения для синхронизации/миграции пользовательских данных. +7. Предоставьте: Демонстрационные примеры, которые можно запускать напрямую, которые помогут вам быстро ознакомиться с общим процессом входа в систему единого входа. +8. Обеспечьте: Глубокую детальную оптимизацию, предотвращение потери параметров: автор протестировал несколько инфраструктур единого входа, и все параметры были потеряны. Например, до входа в систему это было: `http://a.com?id=1&name=2`, а после успешного входа стало: `http://a.com?id=1`. Sa-Token-SSO имеет специальный алгоритм, гарантирующий, что параметры не будут потеряны, а исходный путь будет точно возвращен после успешного входа в систему. + + + + +### 🍂 аутентификация авторизации OAuth2 +Модуль Sa-Token OAuth2 разделен на четыре режима авторизации для удовлетворения потребностей авторизации в различных сценариях. + +| Модель авторизации | Введение | +| :-------- | :-------- | +| Формат кода авторизации | Стандартные шаги авторизации OAuth2: серверная сторона выпускает код, а клиентская сторона получает код кода и выкупает его для access_token | +| Скрытый | Альтернативный вариант: сервер использует перенаправление URL-адресов для прямой передачи access_token на клиентскую страницу | +| Тип пароля | Клиент напрямую обменивает пароль учетной записи пользователя на авторизацию access_token | +| Тип сертификата клиента | Серверный client_token для уровня клиента представляет авторизацию ресурса самого приложения | + +Подробный справочный документ: [https://sa-token.com/doc.html#/oauth2/readme](https://sa-token.com/doc.html#/oauth2/readme) + + +### 📖❓ Вопросы и ответы + +**1. Достаточно ли функционален Sa-Token?** + +Семь лет напряженной работы: пять основных модулей (вход в систему, аутентификация, единый вход, OAuth2, микросервисы) + множество практических плагинов (короткий токен, интеграция jwt, подпись параметров API, авторизация ключа API...). Мы обеспечиваем не только аутентификацию разрешений, мы предоставляем универсальное решение. + + +**2. Легко ли освоить Sa-Token?** + +Документация на китайском языке + комментарии к коду на китайском языке + китайское коммуникационное сообщество + большое количество блогов с практическими примерами + множество видеоуроков + большое количество выдающихся примеров интеграции проектов с открытым исходным кодом. + + +**3. Много ли людей используют Sa-Token?** + +По состоянию на дату статистики (25 января 2026 г.) Sa-Token находится в: + +- Подписчики Gitee получили 48627 звезд, заняв первое место в списке всех рекомендуемых проектов на платформе. +— Внимание GitHub достигло 18523 звезд, что в 1,97 раза больше, чем у основного конкурирующего фреймворка Spring Security, и в 4,19 раза больше, чем у Apache Shiro. +- 25+ фан-групп WeChat (500 человек), 8+ фан-групп QQ (1000 или 2000 человек), онлайн-посещения документов с ежемесячным PV более 200 000 человек. + +Это данные, за которые многие разработчики проголосовали ногами. Я считаю, что эти данные могут доказать популярность Sa-Token лучше любых слов. + + +**4. Какую авторитетную сертификацию имеет Sa-Token?** + +Награды включают, помимо прочего: Самый ценный проект с открытым исходным кодом Gitee GVP, Высококачественный проект с открытым исходным кодом GitCode G-Star, Индекс популярности TOP 30 проектов с открытым исходным кодом OSCHINA 2021, Одно из самых популярных китайских сообществ проектов с открытым исходным кодом OSCHINA 2022, Быстрорастущий проект с открытым исходным кодом Open Atomic Foundation 2023, Лучший проект организации Dromara (один), Подготовительный член Trusted Open Source Сообщество сообщества и сообщество открытого исходного кода «Dromara», где он работает. Выиграли второй приз в «Конкурсе инноваций и инвестиций в области развития Интернета в Китае 2024 года (с открытым исходным кодом)». Проект Gitee High Star (5000+звезд). Два лучших проекта Gitee с открытым исходным кодом для разработки веб-приложений в 2025 году. + + +**5. Взимается ли плата за Sa-Token?** + +Sa-Token использует протокол с открытым исходным кодом Apache-2.0 и обещает, что сама платформа и онлайн-документы всегда будут бесплатными и открытыми. Конечно, если вы заинтересованы в спонсировании Sa-Token, мы не будем уклоняться от этого: [Спонсорская ссылка](https://sa-token.com/doc.html#/more/sa-token-donate). +Мы будем регулярно синхронизировать список спонсоров с онлайн-дисплеем документов. (Одна вещь, которую вам следует отметить: это спонсорство является только дружественным спонсорством и не предполагает какого-либо коммерческого обмена) + + +**6. Является ли Sa-Token инкапсулированным SpringSecurity? Это оболочка ApacheShiro?** + +нет. Sa-Token — это не бэкэнд-шаблон и не вторичная оболочка инкапсуляции для платформы xx, а чистый фреймворк собственной разработки, начинающийся с нуля, с нулевой зависимостью от основного пакета, полностью независимым и управляемым ядром архитектуры + интегрированной адаптацией многих основных фреймворков. + + + + +#### Сертификат ⭐ Трофей 🏆 Почетный знак + + + + + + + + + + + + + + + + +
+ + + +### 🚀 Отличные примеры интеграции с открытым исходным кодом + +- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy): первая в Китае платформа быстрой разработки, которая разделяет национальные секреты до и после, используя Vue3 + Vite + SpringBoot + MP + HuTool + SaToken. +- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus): переписать все функции RuoYi-Vue и интегрировать Sa-Token, Mybatis-Plus, Xxl-Job, нож4j и OSS для регулярной синхронизации. +- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin): SmartAdmin — это первая отечественная платформа быстрой разработки с «высококачественным кодом» в качестве ядра и «простым, эффективным и безопасным» промежуточным и серверным компонентами. +- [[ Chengdan ]](https://gitee.com/orangeform/orange-admin): низкокодовый генератор Chengdan на Тайване. Он может полностью поддерживать несколько приложений, несколько арендаторов, несколько каналов, рабочие процессы, свободное сочетание стеков технологий платформы и т. д. +- [[ Deng Deng ]](https://gitee.com/dromara/lamp-cloud): платформа быстрой разработки среднего и внутреннего уровня, ориентированная на многопользовательские решения. Поддержка независимой базы данных, общей архитектуры данных и режима без арендатора ✨ +- [[ Блог Shiyi ]](https://gitee.com/quequnlong/shiyi-blog): система блогов vue + Springboot, которая разделяет переднюю и заднюю части. + + + +Есть и другие отличные кейсы с открытым исходным кодом, которые невозможно отобразить по одному. См.: [Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) + + +### 🌍 Другие языковые версии + +Члены сообщества Sa-Token предоставили многоязычные версии реализации: + +- Версия Rust: [https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust) +- Версия Go: [https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go) +- Версия NodeJS: [https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token) +- Версия PHP: [https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token) + +Мы искренне приглашаем разработчиков, более владеющих вышеуказанными языками, вместе создавать соответствующие версии. 🤝 + + +### 🔗 Дружественные ссылки +- [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps): облегченная платформа HTTP-связи, чрезвычайно элегантный API, поддержка протоколов WebSocket и Stomp. +- [[ Forest ]](https://gitee.com/dromara/forest): декларативная и программная двойная обработка, благодаря которой отправка HTTP-запросов становится проще. +- [[ Bean Searcher ]](https://github.com/troyzhxu/bean-searcher): REST-версия GraphQL — границы определения сущностей, запросы на основе параметров и одна строка кода для реализации сложных запросов! +- [[ Jpom ]](https://gitee.com/dromara/Jpom): простое и легкое программное обеспечение для онлайн-сборки с низким уровнем вмешательства, автоматического развертывания, ежедневной эксплуатации и обслуживания, а также программного обеспечения для мониторинга проекта. +- [[ TLog ]](https://gitee.com/dromara/TLog): облегченный артефакт отслеживания тегов распределенного журнала. +- [[ hippo4j ]](https://gitee.com/agentart/hippo4j): мощная платформа динамического пула потоков с функциями мониторинга и оповещения. +- [[ hertzbeat ]](https://gitee.com/dromara/hertzbeat): простая в использовании и дружественная система мониторинга и сигнализации в реальном времени с открытым исходным кодом, не требующая агента, высокопроизводительный кластер и мощные возможности настраиваемого мониторинга. +- [[ Solon ]](https://gitee.com/noear/solon): более современная среда разработки приложений: более быстрая, компактная и бесплатная. +- [[ Chat2DB ]](https://github.com/chat2db/Chat2DB): инструмент управления базами данных и бизнес-аналитики на основе искусственного интеллекта, который поддерживает управление 22 базами данных, такими как Mysql, pg, Oracle и Redis. + + + +### 📦 Хостинг кода +- 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) +- AtomGit:[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token) + + + +### 📚 Сборник примеров + +**Мы отдельно подготовили соответствующие примеры интеграции почти для всех технических моментов платформы. Этот сжатый пакет содержит в общей сложности более 60 демо**: охватывающих все аспекты аутентификации и аутентификации, такие как аутентификация входа в систему Sa-Token, аутентификация органа, единый вход в систему, унифицированная аутентификация OAUth2, аутентификация микросервисов, аутентификация по ключу API, интеграция JWT, проверка подписи параметров межсистемного вызова и т. д. + +Адрес загрузки: [https://sa-token.com/doc.html#/more/download-demos](https://sa-token.com/doc.html#/more/download-demos) + + + + + +### 💬 Группа связи + + + +Группа связи QQ: 1098917026 [Нажмите, чтобы присоединиться](https://qm.qq.com/q/26OFBGd2Zy) + +Группа связи WeChat: + + + + + +PS: Отсканируйте QR-код, чтобы добавить WeChat (примечание: sa-token), чтобы пригласить вас присоединиться к групповому чату. + +
+ +微信群 + + +Преимущества присоединения к групповому чату: +- Получайте уведомления об обновлениях платформы как можно скорее. +- Получайте уведомления об ошибках фреймворка как можно скорее. +- Получайте уведомления о новых случаях с открытым исходным кодом как можно скорее. +- Общайтесь (хуа шуу) друг с другом (мо ю) со многими большими парнями 🖐️🐟️. + diff --git a/README_zh_TW.md b/README_zh_TW.md new file mode 100644 index 000000000..dbc8ff5d9 --- /dev/null +++ b/README_zh_TW.md @@ -0,0 +1,344 @@ +[简体中文](./README.md) | 繁體中文 | [English](./README_en.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) + +

+ logo +

+

Sa-Token v1.45.0

+

✨ 開源、免費、一站式 java 權限認證框架,讓鑑權變得簡單、優雅!

+

+ + + + + + + + +

+ +

+ 在線文檔:https://sa-token.com +  |  + 需求提交 +

+ + +--- + +### 📝 前言: + +回望 2020 年初,我為 Sa-Token 提交第一行代碼之際,彼時市面上 Java 缺少的不僅是一個簡潔好用的鑑權框架,更是一整套清晰、自洽的權限架構設計思想。 + +因此,這幾年間我將大量時間傾注在 Sa-Token 的文檔編寫,幾乎每一章節、每一句話、每一個字都經過反覆修改、精細打磨,以求做到最清晰、幹練、易懂的表述。用心閱讀文檔,你學習到的將不止是 Sa-Token 框架本身,更是絕大多數場景下權限設計的最佳實踐。 + + + +### 🛠️ Sa-Token 介紹 + +Sa-Token 是一個輕量級 Java 權限認證框架,目前擁有五大核心模塊:登錄認證、權限認證、單點登錄、OAuth2.0、微服務鑑權。 + +**你還在手搓以下功能?Stop ⚠️ 讓 Sa-Token 來!** + +![sa-token-jss](https://sa-token.com/big-file/index/intro/sa-token-jss--tran.png) + +要在 SpringBoot 項目中使用 Sa-Token,你只需要在 pom.xml 中引入依賴: + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + 1.45.0 + +``` + +除了支持 SpringBoot2、Sa-Token 還為 SpringBoot3/4、Solon、JFinal 等常見 Web 框架提供集成包,做到真正的開箱即用。 + + +
+簡單示例展示:(點擊展開 / 摺疊) + +Sa-Token 旨在以簡單、優雅的方式完成系統的權限認證部分,以登錄認證為例,你只需要: + +``` java +// 會話登錄,參數填登錄人的賬號id +StpUtil.login(10001); +``` + +無需實現任何接口,無需創建任何配置文件,只需要這一句靜態代碼的調用,便可以完成會話登錄認證。 + +如果一個接口需要登錄後才能訪問,我們只需調用以下代碼: + +``` java +// 校驗當前客戶端是否已經登錄,如果未登錄則拋出 `NotLoginException` 異常 +StpUtil.checkLogin(); +``` + +在 Sa-Token 中,大多數功能都可以一行代碼解決: + +踢人下線: + +``` java +// 將賬號id為 10077 的會話踢下線 +StpUtil.kickout(10077); +``` + +權限認證: + +``` java +// 註解鑑權:只有具備 `user:add` 權限的會話才可以進入方法 +@SaCheckPermission("user:add") +public String insert(SysUser user) { + // ... + return "用戶增加"; +} +``` + +路由攔截鑑權: + +``` java +// 根據路由劃分模塊,不同模塊不同鑑權 +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + // 更多模塊... +})).addPathPatterns("/**"); +``` + +**如果您曾經使用過 Shiro、SpringSecurity,在切換到 Sa-Token 後,您將體會到質的飛躍。** + + + +
+ + +
+ 核心模塊一覽:(點擊展開 / 摺疊) + +- **登錄認證** —— 單端登錄、多端登錄、同端互斥登錄、七天內免登錄。 +- **權限認證** —— 權限認證、角色認證、會話二級認證。 +- **踢人下線** —— 根據賬號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。 + +
+ + + +### 🍃 SSO 單點登錄 + +Sa-Token SSO 分為三種模式,可解決:`同域、跨域、共享Redis、跨Redis、前後端一體、前後端分離、純 js、vue2、vue3、java 項目、非 java 項目` 等架構下的 SSO 認證需求: + +![sa-token-jss](https://sa-token.com/big-file/doc/sso/sa-token-sso--white.png) + + +| 系統架構 | 採用模式 | 簡介 | 文檔鏈接 | +| :-------- | :-------- |:----------------| :-------- | +| 前端同域 + 後端同 Redis | 模式一 | 共享Cookie同步會話 | [文檔](https://sa-token.com/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.com/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.com/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,共享會話數據。 +3. 如果無法做到前端同域、後端同 Redis,可以走託底的模式三:Http請求校驗 ticket 獲取會話。 +4. 提供:NoSdk 模式示例 + sso-server 接口文檔,非 Sa-Token 項目、非 java 項目也可以對接。 +5. 提供:多重安全校驗:域名校驗、ticket校驗、參數簽名校驗,有效防 ticket 劫持,防請求重放等攻擊。 +6. 提供:大量實戰痛點教學:sso-server 前後端分離設計、sso-client 前後端分離設計、用戶數據同步/遷移方案設計。 +7. 提供:直接可運行的 demo 示例,助你快速熟悉 SSO 大致登錄流程。 +8. 提供:深度細節優化,參數防丟:筆者曾試驗多個SSO框架,均有參數丟失情況,比如登錄前是:`http://a.com?id=1&name=2`,登錄成功後就變成了:`http://a.com?id=1`,Sa-Token-SSO 內有專門算法保證了參數不丟失,登錄成功後精準原路返回。 + + + + +### 🍂 OAuth2 授權認證 +Sa-Token OAuth2 模塊分為四種授權模式,解決不同場景下的授權需求 + +| 授權模式 | 簡介 | +| :-------- | :-------- | +| 授權碼式 | OAuth2 標準授權步驟,server 端下放 code,client 端獲取 code 碼兌換 access_token | +| 隱藏式 | 備用選擇,server 端使用 URL 重定向方式直接將 access_token 下放到 client 端頁面 | +| 密碼式 | client 直接拿著用戶的賬號密碼換取授權 access_token | +| 客戶端憑證式 | server 端針對 client 級別的 client_token,代表應用自身的資源授權 | + +詳細參考文檔:[https://sa-token.com/doc.html#/oauth2/readme](https://sa-token.com/doc.html#/oauth2/readme) + + +### 📖❓ 疑問解答 + +**1、Sa-Token 功能全不全?** + +七年磨一劍:五大核心模塊(登錄、鑑權、SSO、OAuth2、微服務) + 眾多實用插件 (短 token、jwt 集成、API 參數簽名、API Key 秘鑰授權...) 我們提供的不只是權限認證,我們提供的是一站式解決方案。 + + +**2、Sa-Token 好不好學?** + +中文文檔 + 中文代碼註釋 + 中文交流社區 + 大量實戰案例博客 + 多個視頻教程 + 大量優秀開源項目集成案例。 + + +**3、Sa-Token 用的人多不多?** + +截止統計日 (2026-1-25) 起,Sa-Token 在: + +- Gitee 關注量達到 48627 Star,位列平臺所有推薦項目排行榜第一名。 +- GitHub 關注量達到 18523 Star,是主要競爭框架 Spring Security 的 1.97 倍,Apache Shiro 的 4.19 倍。 +- 25+ 微信粉絲群 (500人),8+ QQ粉絲群 (1000人 or 2000人) ,在線文檔訪問量月PV 20萬+。 + +這是眾多開發者用腳投票的數據,相信這些數據比任何言語都能證明 Sa-Token 的熱度。 + + +**4、Sa-Token 有哪些權威認證?** + +曾獲榮譽包括但不限於:Gitee GVP 最有價值開源項目、GitCode G-Star 優質開源項目、OSCHINA 2021 人氣指數 TOP 30 開源項目、OSCHINA 2022 年度最火熱中國開源項目社區之一、開放原子基金會2023快速成長開源項目、 Dromara 組織頂尖項目(之一)、可信開源社區共同體預備成員、所在開源社區 “Dromara” 榮獲《2024中國互聯網發展創新與投資大賽(開源)》二等獎。 Gitee High Star 計劃項目(5000+star)。Gitee 2025年度開源項目 Web應用開發 Top 2。 + + +**5、Sa-Token 收費嗎?** + +Sa-Token 採用 Apache-2.0 開源協議,承諾框架本身與在線文檔永久免費開放。當然如果您有心贊助 Sa-Token,我們也不迴避:[贊助鏈接](https://sa-token.com/doc.html#/more/sa-token-donate)。 +我們將定期同步贊助者名單到在線文檔展示。(您需要注意的一點是:該贊助僅為友情贊助,不提供任何商業交換) + + +**6、Sa-Token 是封裝的 SpringSecurity 嗎?是套殼 ApacheShiro 嗎?** + +不是。Sa-Token 不是一個後臺模板,也不是針對 xx 框架的二次封裝套殼,而是從 0 開始的純血自研框架,核心包零依賴,完全自主可控的架構內核 + 眾多主流框架的集成適配。 + + + + +#### 證書 ⭐ 獎盃 🏆 榮譽展示 + + + + + + + + + + + + + + + + +
+ + + +### 🚀 優秀開源集成案例 + +- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy):國內首個國密前後分離快速開發平臺,採用 Vue3 + Vite + SpringBoot + Mp + HuTool + SaToken。 +- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus):重寫RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Xxl-Job、knife4j、OSS 定期同步。 +- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin):SmartAdmin 國內首個以「高質量代碼」為核心,「簡潔、高效、安全」中後臺快速開發平臺。 +- [[ 橙單 ]](https://gitee.com/orangeform/orange-admin): 橙單中臺化低代碼生成器。可完整支持多應用、多租戶、多渠道、工作流、框架技術棧自由組合等。 +- [[ 燈燈 ]](https://gitee.com/dromara/lamp-cloud): 專注於多租戶解決方案的中後臺快速開發平臺。支持獨立數據庫、共享數據架構 和 非租戶模式 ✨ +- [[ 拾壹博客 ]](https://gitee.com/quequnlong/shiyi-blog):一款 vue + springboot 前後端分離的博客系統。 + + + +還有更多優秀開源案例無法逐一展示,請參考:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) + + +### 🌍 其它語言版本 + +Sa-Token 社區成員貢獻了多語言實現版本: + +- Rust 版本:[https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust) +- Go 版本:[https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go) +- NodeJS 版本:[https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token) +- PHP 版本:[https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token) + +我們誠邀對上述語言較為熟練的開發者,一起建設相關版本。🤝 + + +### 🔗 友情鏈接 +- [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps):輕量級 http 通信框架,API無比優雅,支持 WebSocket、Stomp 協議 +- [[ Forest ]](https://gitee.com/dromara/forest):聲明式與編程式雙修,讓天下沒有難以發送的 HTTP 請求 +- [[ Bean Searcher ]](https://github.com/troyzhxu/bean-searcher):REST 版的 GraphQL — 實體定義邊界,參數驅動查詢,一行代碼實現複雜查詢! +- [[ 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) +- AtomGit:[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token) + + + +### 📚 示例大全 + +**我們為框架幾乎所有技術點均單獨製作了對應的集成示例,此壓縮包共計 60+ Demo**:涵蓋 Sa-Token 登錄認證、權限認證、SSO 單點登錄、OAUth2 統一認證、微服務鑑權、API Key 認證、JWT集成、跨系統調用參數簽名校驗 等鑑權認證的方方面面。 + +下載地址:[https://sa-token.com/doc.html#/more/download-demos](https://sa-token.com/doc.html#/more/download-demos) + + + + + +### 💬 交流群 + + + +QQ交流群:1098917026 [點擊加入](https://qm.qq.com/q/26OFBGd2Zy) + +微信交流群: + + + + + +PS:掃碼添加微信 (備註:sa-token),邀您加入群聊。 + +
+ +微信群 + + +加入群聊的好處: +- 第一時間收到框架更新通知。 +- 第一時間收到框架 bug 通知。 +- 第一時間收到新增開源案例通知。 +- 和眾多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。 + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..0f3c29c2d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,62 @@ +# Security Policy + +Thanks for helping keep Sa-Token and the wider [dromara](https://github.com/dromara) ecosystem secure. + +## Reporting a vulnerability + +**Please do not open a public issue, discussion, or pull request that describes a suspected vulnerability** — that gives attackers a head-start before a fix lands. + +Use one of these private channels instead: + +1. **GitHub's Private Vulnerability Reporting (preferred).** + Open a private security advisory at . + GitHub automatically routes it to the maintainers, keeps the discussion private until you and the maintainers agree to publish, and (optionally) assigns a CVE on publish. + +2. **Email.** + If GitHub PVR is unavailable to you for any reason, email the project lead directly: [2393584716@qq.com](mailto:2393584716@qq.com). + +## What to include + +A good report contains: + +- A clear description of the vulnerability and its impact. +- Steps to reproduce against a specific commit SHA or release tag. +- Affected code locations (`file:line`) where useful. +- A suggested fix or mitigation if you have one. +- Whether you'd like credit in the published advisory and, if so, under what name. + +## Scope and supported versions + +Sa-Token is a Java authentication / authorization framework (login, permissions, session, SSO, OAuth2, JWT). The maintainers support security fixes on the latest minor release. Older releases will not generally receive backports. + +In-scope vulnerability classes include: + +- Authentication / authorization bypass +- Session fixation, token forgery, JWT signature / claims handling flaws +- SSO redirect / open-redirect / ticket exfiltration +- OAuth2 client / authorization-code flow flaws +- Cryptographic misuse in stored secrets / signing keys +- Insecure deserialization in token / cookie codecs + +Out-of-scope: + +- Findings against intentionally trusted-admin features (config-driven behavior that the framework explicitly delegates to the integrator). +- Issues that require an attacker to already have full server / database access. +- Best-practice complaints without a concrete impact (e.g. "this header should be set", "TLS version should be raised"). + +## Process + +After you submit: + +1. A maintainer will acknowledge receipt within roughly 1 week. +2. We'll triage the report: confirm severity, scope, and reproducibility. +3. We'll work with you on a fix and a coordinated disclosure timeline (typically up to 90 days, longer if the fix is structural). +4. On publication, we credit you in the advisory unless you ask not to be credited. + +## Hall of fame + +Reporters who have helped harden Sa-Token via responsible disclosure will be listed here once the corresponding advisory is published. + +--- + +This policy is suggested via [GitHub's "Suggest a security policy" workflow](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository). Maintainers can edit any section freely; the most important thing is that **a private reporting channel exists** so researchers can submit findings responsibly. diff --git a/mvn clean.bat b/mvn clean.bat index 6c5badfce..06d96c24f 100644 --- a/mvn clean.bat +++ b/mvn clean.bat @@ -2,68 +2,88 @@ :: 整体clean call mvn clean + :: demo模块clean cd sa-token-demo -cd sa-token-demo-jwt -call mvn clean +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-apikey & call mvn clean & cd .. +cd sa-token-demo-async & 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-device-lock & 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-caffeine & 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-quick-login-sb3 & 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-springboot4-redis & call mvn clean & cd .. +cd sa-token-demo-springboot-low-version & 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-sse & 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-freemarker & 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-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-springboot -call mvn clean +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-webflux -call mvn clean -cd .. - -cd sa-token-demo-solon -call mvn clean -cd .. - -cd sa-token-demo-oauth2-client -call mvn clean -cd .. - -cd sa-token-demo-oauth2-server -call mvn clean +cd sa-token-demo-remember-me +cd sa-token-demo-remember-me-server & call mvn clean & cd .. cd .. -cd sa-token-demo-quick-login -call mvn clean +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-nosdk & call mvn clean & cd .. +cd sa-token-demo-sso3-client-resdk & call mvn clean & cd .. +cd sa-token-demo-sso3-client-anon & call mvn clean & cd .. cd .. -cd sa-token-demo-alone-redis -call mvn clean +cd sa-token-demo-sso-for-solon +cd sa-token-demo-sso1-client-solon & call mvn clean & cd .. +cd sa-token-demo-sso2-client-solon & call mvn clean & cd .. +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-sso1 -call mvn clean -cd .. -cd sa-token-demo-sso2-server -call mvn clean -cd .. -cd sa-token-demo-sso2-client -call mvn clean cd .. -cd sa-token-demo-sso3-server -call mvn clean -cd .. +:: test clean -cd sa-token-demo-sso3-client +cd sa-token-test call mvn clean cd .. - - -cd .. - :: 最后打印 echo; echo; diff --git a/mvn test.bat b/mvn test.bat new file mode 100644 index 000000000..acda1e6e7 --- /dev/null +++ b/mvn test.bat @@ -0,0 +1,11 @@ + +:: 整体test +call mvn clean test + + +:: 最后打印 +echo; +echo; +echo ----------- test end ----------- +echo; +pause \ No newline at end of file diff --git a/pom.xml b/pom.xml index a045ae8e4..bafbe4e85 100644 --- a/pom.xml +++ b/pom.xml @@ -8,22 +8,24 @@ cn.dev33 sa-token-parent pom - 1.24.0 + ${revision} sa-token - A Java Web lightweight authority authentication framework, comprehensive function, easy to use + An open-source, free, and one-stop Java authentication framework that makes authentication simple and elegant! https://github.com/dromara/sa-token + sa-token-dependencies + sa-token-special-dependencies + sa-token-bom sa-token-core sa-token-starter sa-token-plugin - - + @@ -36,10 +38,14 @@ - 1.24.0 + 1.45.0 1.8 utf-8 utf-8 + + + 3.2.8 + 0.10.0 @@ -53,40 +59,163 @@ - shengzhang + click33 2393584716@qq.com - - - - org.sonatype.oss - oss-parent - 7 - - - + + + + + + cn.dev33 + sa-token-dependencies + ${project.version} + pom + import + + + + + - - - org.apache.maven.plugins - maven-compiler-plugin - 3.2 - - 1.8 - 1.8 - UTF-8 - - - - + + + + + org.apache.maven.plugins + maven-source-plugin + 3.4.0 + + true + + + + compile + + jar + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + 1.8 + 1.8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + + + false + false + -Xdoclint:none + false + + + + package + + aggregate + + + + attach-javadocs + + jar + + + none + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.7.3 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + + + org.sonatype.central + central-publishing-maven-plugin + ${central.publishing.maven.version} + true + + + central + + + + + + + + + + org.eclipse.m2e @@ -113,7 +242,8 @@ - + + diff --git a/preview-doc.bat b/preview-doc.bat new file mode 100644 index 000000000..ac59063c1 --- /dev/null +++ b/preview-doc.bat @@ -0,0 +1,4 @@ +:: 运行前需要安装 browser-sync: +:: npm install -g browser-sync + +cd sa-token-doc & browser-sync start --server --files "" diff --git a/sa-token-bom/pom.xml b/sa-token-bom/pom.xml new file mode 100644 index 000000000..85c1d4da1 --- /dev/null +++ b/sa-token-bom/pom.xml @@ -0,0 +1,414 @@ + + + 4.0.0 + + cn.dev33 + sa-token-bom + ${revision} + pom + sa-token-bom + Sa-Token Bom + https://github.com/dromara/sa-token + + + 1.45.0 + + + 3.2.8 + 0.10.0 + + + + + + Apache 2 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + + + master + https://github.com/dromara/sa-token.git + scm:git:https://github.com/dromara/sa-token.git + scm:git:https://github.com/dromara/sa-token.git + + + + + + click33 + 2393584716@qq.com + + + + + + + + cn.dev33 + sa-token-core + ${revision} + + + + + cn.dev33 + sa-token-starter + ${revision} + + + cn.dev33 + sa-token-jboot-plugin + ${revision} + + + cn.dev33 + sa-token-jfinal-plugin + ${revision} + + + cn.dev33 + sa-token-reactor-spring-boot-starter + ${revision} + + + cn.dev33 + sa-token-reactor-spring-boot3-starter + ${revision} + + + cn.dev33 + sa-token-reactor-spring-boot4-starter + ${revision} + + + cn.dev33 + sa-token-servlet + ${revision} + + + cn.dev33 + sa-token-jakarta-servlet + ${revision} + + + cn.dev33 + sa-token-solon-plugin + ${revision} + + + cn.dev33 + sa-token-spring-boot-webmvc-reactor-v2v3v4-common + ${revision} + + + cn.dev33 + sa-token-spring-boot-reactor-v3v4-common + ${revision} + + + cn.dev33 + sa-token-spring-boot-webmvc-v3v4-common + ${revision} + + + cn.dev33 + sa-token-spring-boot-starter + ${revision} + + + cn.dev33 + sa-token-spring-boot3-starter + ${revision} + + + cn.dev33 + sa-token-spring-boot4-starter + ${revision} + + + + + + cn.dev33 + sa-token-plugin + ${revision} + + + cn.dev33 + sa-token-alone-redis + ${revision} + + + cn.dev33 + sa-token-alone-redis-by-spring-boot4 + ${revision} + + + cn.dev33 + sa-token-alone-redisson + ${revision} + + + cn.dev33 + sa-token-dubbo + ${revision} + + + cn.dev33 + sa-token-dubbo3 + ${revision} + + + cn.dev33 + sa-token-grpc + ${revision} + + + cn.dev33 + sa-token-redis-template + ${revision} + + + cn.dev33 + sa-token-jackson + ${revision} + + + cn.dev33 + sa-token-jackson3 + ${revision} + + + cn.dev33 + sa-token-fastjson + ${revision} + + + cn.dev33 + sa-token-fastjson2 + ${revision} + + + cn.dev33 + sa-token-snack3 + ${revision} + + + cn.dev33 + sa-token-redis-jackson + ${revision} + + + cn.dev33 + sa-token-forest + ${revision} + + + cn.dev33 + sa-token-okhttps + ${revision} + + + cn.dev33 + sa-token-redisson-spring-boot-starter + ${revision} + + + cn.dev33 + sa-token-redisson + ${revision} + + + cn.dev33 + sa-token-redisx + ${revision} + + + cn.dev33 + sa-token-hutool-timed-cache + ${revision} + + + cn.dev33 + sa-token-thymeleaf + ${revision} + + + cn.dev33 + sa-token-freemarker + ${revision} + + + cn.dev33 + sa-token-jwt + ${revision} + + + cn.dev33 + sa-token-oauth2 + ${revision} + + + cn.dev33 + sa-token-apikey + ${revision} + + + cn.dev33 + sa-token-sign + ${revision} + + + cn.dev33 + sa-token-quick-login + ${revision} + + + cn.dev33 + sa-token-spring-aop + ${revision} + + + cn.dev33 + sa-token-spring-el + ${revision} + + + cn.dev33 + sa-token-sso + ${revision} + + + cn.dev33 + sa-token-temp-jwt + ${revision} + + + cn.dev33 + sa-token-redis-template-jdk-serializer + ${revision} + + + cn.dev33 + sa-token-serializer-features + ${revision} + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + 1.8 + 1.8 + UTF-8 + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.7.3 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + + + org.sonatype.central + central-publishing-maven-plugin + ${central.publishing.maven.version} + true + + + central + + + + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + [1.0.0,) + + enforce + + + + + + + + + + + + + + + diff --git a/sa-token-core/.gitignore b/sa-token-core/.gitignore deleted file mode 100644 index 8122f47c0..000000000 --- a/sa-token-core/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ -.iml \ No newline at end of file diff --git a/sa-token-core/pom.xml b/sa-token-core/pom.xml index 09b250f5e..efb3f356f 100644 --- a/sa-token-core/pom.xml +++ b/sa-token-core/pom.xml @@ -3,20 +3,20 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - cn.dev33 sa-token-parent - 1.24.0 + ${revision} + ../pom.xml jar sa-token-core sa-token-core - A Java Web lightweight authority authentication framework, comprehensive function, easy to use + An open-source, free, and one-stop Java authentication framework that makes authentication simple and elegant! - + diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java b/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java index 4074d5b3a..a008aa8d8 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java @@ -1,51 +1,94 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; -import java.util.HashMap; -import java.util.Map; - -import cn.dev33.satoken.action.SaTokenAction; -import cn.dev33.satoken.action.SaTokenActionDefaultImpl; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.config.SaTokenConfigFactory; import cn.dev33.satoken.context.SaTokenContext; -import cn.dev33.satoken.context.SaTokenContextDefaultImpl; +import cn.dev33.satoken.context.SaTokenContextForThreadLocal; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; +import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.listener.SaTokenListener; -import cn.dev33.satoken.listener.SaTokenListenerDefaultImpl; +import cn.dev33.satoken.http.SaHttpTemplate; +import cn.dev33.satoken.http.SaHttpTemplateDefaultImpl; +import cn.dev33.satoken.json.SaJsonTemplate; +import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.log.SaLog; +import cn.dev33.satoken.log.SaLogForConsole; +import cn.dev33.satoken.same.SaSameTemplate; +import cn.dev33.satoken.secure.totp.SaTotpTemplate; +import cn.dev33.satoken.serializer.SaSerializerTemplate; +import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJson; import cn.dev33.satoken.stp.StpInterface; import cn.dev33.satoken.stp.StpInterfaceDefaultImpl; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.temp.SaTempInterface; -import cn.dev33.satoken.temp.SaTempDefaultImpl; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.temp.SaTempTemplate; import cn.dev33.satoken.util.SaFoxUtil; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + /** - * 管理 Sa-Token 所有接口对象 - * @author kong + * 管理 Sa-Token 所有全局组件,可通过此类快速获取、写入各种全局组件对象 * + * @author click33 + * @since 1.18.0 */ public class SaManager { /** - * 配置文件 Bean + * 全局配置对象 */ - public static SaTokenConfig config; + public volatile static SaTokenConfig config; public static void setConfig(SaTokenConfig config) { - SaManager.config = config; - if(config.getIsPrint()) { + setConfigMethod(config); + + // 打印 banner + if(config !=null && config.getIsPrint()) { SaFoxUtil.printSaToken(); } - // 调用一次StpUtil中的方法,保证其可以尽早的初始化 StpLogic + + // 如果此 config 对象没有配置 isColorLog 的值,则框架为它自动判断一下 + if(config != null && config.getIsLog() != null && config.getIsLog() && config.getIsColorLog() == null) { + config.setIsColorLog(SaFoxUtil.isCanColorLog()); + } + + // $$ 全局事件 + SaTokenEventCenter.doSetConfig(config); + + // 调用一次 StpUtil 中的方法,保证其可以尽早的初始化 StpLogic StpUtil.getLoginType(); } + private static void setConfigMethod(SaTokenConfig config) { + SaManager.config = config; + } + + /** + * 获取 Sa-Token 的全局配置信息 + * @return 全局配置信息 + */ public static SaTokenConfig getConfig() { if (config == null) { synchronized (SaManager.class) { if (config == null) { - setConfig(SaTokenConfigFactory.createConfig()); + setConfigMethod(SaTokenConfigFactory.createConfig()); } } } @@ -53,20 +96,27 @@ public static SaTokenConfig getConfig() { } /** - * 持久化 Bean + * 持久化组件 */ - private static SaTokenDao saTokenDao; + private volatile static SaTokenDao saTokenDao; public static void setSaTokenDao(SaTokenDao saTokenDao) { - if((SaManager.saTokenDao instanceof SaTokenDaoDefaultImpl)) { - ((SaTokenDaoDefaultImpl)SaManager.saTokenDao).endRefreshThread(); + setSaTokenDaoMethod(saTokenDao); + SaTokenEventCenter.doRegisterComponent("SaTokenDao", saTokenDao); + } + private static void setSaTokenDaoMethod(SaTokenDao saTokenDao) { + if (SaManager.saTokenDao != null) { + SaManager.saTokenDao.destroy(); } SaManager.saTokenDao = saTokenDao; + if (SaManager.saTokenDao != null) { + SaManager.saTokenDao.init(); + } } public static SaTokenDao getSaTokenDao() { if (saTokenDao == null) { synchronized (SaManager.class) { if (saTokenDao == null) { - setSaTokenDao(new SaTokenDaoDefaultImpl()); + setSaTokenDaoMethod(new SaTokenDaoDefaultImpl()); } } } @@ -74,17 +124,18 @@ public static SaTokenDao getSaTokenDao() { } /** - * 权限认证 Bean + * 权限数据源组件 */ - private static StpInterface stpInterface; + private volatile static StpInterface stpInterface; public static void setStpInterface(StpInterface stpInterface) { SaManager.stpInterface = stpInterface; + SaTokenEventCenter.doRegisterComponent("StpInterface", stpInterface); } public static StpInterface getStpInterface() { if (stpInterface == null) { synchronized (SaManager.class) { if (stpInterface == null) { - setStpInterface(new StpInterfaceDefaultImpl()); + SaManager.stpInterface = new StpInterfaceDefaultImpl(); } } } @@ -92,84 +143,160 @@ public static StpInterface getStpInterface() { } /** - * 框架行为 Bean + * 上下文 SaTokenContext */ - private static SaTokenAction saTokenAction; - public static void setSaTokenAction(SaTokenAction saTokenAction) { - SaManager.saTokenAction = saTokenAction; + private volatile static SaTokenContext saTokenContext; + public static void setSaTokenContext(SaTokenContext saTokenContext) { + SaManager.saTokenContext = saTokenContext; + SaTokenEventCenter.doRegisterComponent("SaTokenContext", saTokenContext); } - public static SaTokenAction getSaTokenAction() { - if (saTokenAction == null) { + public static SaTokenContext getSaTokenContext() { + if (saTokenContext == null) { synchronized (SaManager.class) { - if (saTokenAction == null) { - setSaTokenAction(new SaTokenActionDefaultImpl()); + if (saTokenContext == null) { + SaManager.saTokenContext = new SaTokenContextForThreadLocal(); } } } - return saTokenAction; + return saTokenContext; } - + /** - * 容器操作 Bean + * 临时 token 认证模块 */ - private static SaTokenContext saTokenContext; - public static void setSaTokenContext(SaTokenContext saTokenContext) { - SaManager.saTokenContext = saTokenContext; + private volatile static SaTempTemplate saTempTemplate; + public static void setSaTempTemplate(SaTempTemplate saTempTemplate) { + SaManager.saTempTemplate = saTempTemplate; + SaTokenEventCenter.doRegisterComponent("SaTempTemplate", saTempTemplate); } - public static SaTokenContext getSaTokenContext() { - if (saTokenContext == null) { + public static SaTempTemplate getSaTempTemplate() { + if (saTempTemplate == null) { synchronized (SaManager.class) { - if (saTokenContext == null) { - setSaTokenContext(new SaTokenContextDefaultImpl()); + if (saTempTemplate == null) { + SaManager.saTempTemplate = new SaTempTemplate(); } } } - return saTokenContext; + return saTempTemplate; } /** - * 侦听器 Bean + * JSON 转换器 */ - private static SaTokenListener saTokenListener; - public static void setSaTokenListener(SaTokenListener saTokenListener) { - SaManager.saTokenListener = saTokenListener; + private volatile static SaJsonTemplate saJsonTemplate; + public static void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) { + SaManager.saJsonTemplate = saJsonTemplate; + SaTokenEventCenter.doRegisterComponent("SaJsonTemplate", saJsonTemplate); } - public static SaTokenListener getSaTokenListener() { - if (saTokenListener == null) { + public static SaJsonTemplate getSaJsonTemplate() { + if (saJsonTemplate == null) { synchronized (SaManager.class) { - if (saTokenListener == null) { - setSaTokenListener(new SaTokenListenerDefaultImpl()); + if (saJsonTemplate == null) { + SaManager.saJsonTemplate = new SaJsonTemplateDefaultImpl(); } } } - return saTokenListener; + return saJsonTemplate; } /** - * 临时令牌验证模块 Bean + * HTTP 转换器 */ - private static SaTempInterface saTemp; - public static void setSaTemp(SaTempInterface saTemp) { - SaManager.saTemp = saTemp; + private volatile static SaHttpTemplate saHttpTemplate; + public static void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) { + SaManager.saHttpTemplate = saHttpTemplate; + SaTokenEventCenter.doRegisterComponent("SaHttpTemplate", saHttpTemplate); } - public static SaTempInterface getSaTemp() { - if (saTemp == null) { + public static SaHttpTemplate getSaHttpTemplate() { + if (saHttpTemplate == null) { synchronized (SaManager.class) { - if (saTemp == null) { - setSaTemp(new SaTempDefaultImpl()); + if (saHttpTemplate == null) { + SaManager.saHttpTemplate = new SaHttpTemplateDefaultImpl(); } } } - return saTemp; + return saHttpTemplate; } - + + /** + * 序列化器 + */ + private volatile static SaSerializerTemplate saSerializerTemplate; + public static void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) { + SaManager.saSerializerTemplate = saSerializerTemplate; + SaTokenEventCenter.doRegisterComponent("SaSerializerTemplate", saSerializerTemplate); + } + public static SaSerializerTemplate getSaSerializerTemplate() { + if (saSerializerTemplate == null) { + synchronized (SaManager.class) { + if (saSerializerTemplate == null) { + SaManager.saSerializerTemplate = new SaSerializerTemplateForJson(); + } + } + } + return saSerializerTemplate; + } + + /** + * Same-Token 同源系统认证模块 + */ + private volatile static SaSameTemplate saSameTemplate; + public static void setSaSameTemplate(SaSameTemplate saSameTemplate) { + SaManager.saSameTemplate = saSameTemplate; + SaTokenEventCenter.doRegisterComponent("SaSameTemplate", saSameTemplate); + } + public static SaSameTemplate getSaSameTemplate() { + if (saSameTemplate == null) { + synchronized (SaManager.class) { + if (saSameTemplate == null) { + SaManager.saSameTemplate = new SaSameTemplate(); + } + } + } + return saSameTemplate; + } + + /** + * 日志输出器 + */ + public volatile static SaLog log = new SaLogForConsole(); + public static void setLog(SaLog log) { + SaManager.log = log; + SaTokenEventCenter.doRegisterComponent("SaLog", log); + } + public static SaLog getLog() { + return SaManager.log; + } + + /** + * TOTP 算法类,支持 生成/验证 动态一次性密码 + */ + private volatile static SaTotpTemplate totpTemplate; + public static void setSaTotpTemplate(SaTotpTemplate totpTemplate) { + SaManager.totpTemplate = totpTemplate; + SaTokenEventCenter.doRegisterComponent("SaTotpTemplate", totpTemplate); + } + public static SaTotpTemplate getSaTotpTemplate() { + if (totpTemplate == null) { + synchronized (SaManager.class) { + if (totpTemplate == null) { + SaManager.totpTemplate = new SaTotpTemplate(); + } + } + } + return totpTemplate; + } + + + // ------------------- StpLogic 相关 ------------------- + /** - * StpLogic集合, 记录框架所有成功初始化的StpLogic + * StpLogic 集合, 记录框架所有成功初始化的 StpLogic */ - public static Map stpLogicMap = new HashMap(); + public static Map stpLogicMap = new ConcurrentHashMap<>(); /** - * 向集合中 put 一个 StpLogic + * 向全局集合中 put 一个 StpLogic * @param stpLogic StpLogic */ public static void putStpLogic(StpLogic stpLogic) { @@ -177,32 +304,61 @@ public static void putStpLogic(StpLogic stpLogic) { } /** - * 根据 LoginType 获取对应的StpLogic,如果不存在则抛出异常 + * 在全局集合中 移除 一个 StpLogic + */ + public static void removeStpLogic(String loginType) { + stpLogicMap.remove(loginType); + } + + /** + * 根据 LoginType 获取对应的StpLogic,如果不存在则新建并返回 * @param loginType 对应的账号类型 * @return 对应的StpLogic */ public static StpLogic getStpLogic(String loginType) { - // 如果type为空则返回框架内置的 + return getStpLogic(loginType, true); + } + + /** + * 根据 LoginType 获取对应的StpLogic,如果不存在,isCreate = 是否自动创建并返回 + * @param loginType 对应的账号类型 + * @param isCreate 在 StpLogic 不存在时,true=新建并返回,false=抛出异常 + * @return 对应的StpLogic + */ + public static StpLogic getStpLogic(String loginType, boolean isCreate) { + // 如果type为空则返回框架默认内置的 if(loginType == null || loginType.isEmpty()) { return StpUtil.stpLogic; } - // 从SaManager中获取 + // 从集合中获取 StpLogic stpLogic = stpLogicMap.get(loginType); if(stpLogic == null) { - /* - * 此时有两种情况会造成 StpLogic == null - * 1. loginType拼写错误,请改正 (建议使用常量) - * 2. 自定义StpUtil尚未初始化(静态类中的属性至少一次调用后才会初始化),解决方法两种 - * (1) 从main方法里调用一次 - * (2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 - */ - throw new SaTokenException("未能获取对应StpLogic,type="+ loginType); + + // isCreate=true时,自创建模式:自动创建并返回 + if(isCreate) { + synchronized (SaManager.class) { + stpLogic = stpLogicMap.get(loginType); + if(stpLogic == null) { + stpLogic = SaStrategy.instance.createStpLogic.apply(loginType); + } + } + } + // isCreate=false时,严格校验模式:抛出异常 + else { + /* + * 此时有两种情况会造成 StpLogic == null + * 1. loginType拼写错误,请改正 (建议使用常量) + * 2. 自定义StpUtil尚未初始化(静态类中的属性至少一次调用后才会初始化),解决方法两种 + * (1) 从main方法里调用一次 + * (2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 + */ + throw new SaTokenException("未能获取对应StpLogic,type="+ loginType).setCode(SaErrorCode.CODE_10002); + } } // 返回 return stpLogic; } - } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/action/SaTokenAction.java b/sa-token-core/src/main/java/cn/dev33/satoken/action/SaTokenAction.java deleted file mode 100644 index 0936d6eaf..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/action/SaTokenAction.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.dev33.satoken.action; - -import java.lang.reflect.Method; -import java.util.List; - -import cn.dev33.satoken.session.SaSession; - -/** - * Sa-Token 逻辑代理接口 - *

此接口将会代理框架内部的一些关键性逻辑,方便开发者进行按需重写

- * @author kong - * - */ -public interface SaTokenAction { - - /** - * 创建一个Token - * @param loginId 账号id - * @param loginType 账号类型 - * @return token - */ - public String createToken(Object loginId, String loginType); - - /** - * 创建一个Session - * @param sessionId Session的Id - * @return 创建后的Session - */ - public SaSession createSession(String sessionId); - - /** - * 判断:集合中是否包含指定元素(模糊匹配) - * @param list 集合 - * @param element 元素 - * @return 是否包含 - */ - public boolean hasElement(List list, String element); - - /** - * 对一个Method对象进行注解检查(注解鉴权内部实现) - * @param method Method对象 - */ - public void checkMethodAnnotation(Method method); - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/action/SaTokenActionDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/action/SaTokenActionDefaultImpl.java deleted file mode 100644 index 3595de88a..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/action/SaTokenActionDefaultImpl.java +++ /dev/null @@ -1,138 +0,0 @@ -package cn.dev33.satoken.action; - -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Method; -import java.util.List; -import java.util.UUID; - -import cn.dev33.satoken.SaManager; -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.session.SaSession; -import cn.dev33.satoken.util.SaFoxUtil; -import cn.dev33.satoken.util.SaTokenConsts; - -/** - * Sa-Token 逻辑代理接口 [默认实现类] - * @author kong - * - */ -public class SaTokenActionDefaultImpl implements SaTokenAction { - - /** - * 创建一个Token - */ - @Override - public String createToken(Object loginId, String loginType) { - // 根据配置的tokenStyle生成不同风格的token - String tokenStyle = SaManager.getConfig().getTokenStyle(); - // uuid - if(SaTokenConsts.TOKEN_STYLE_UUID.equals(tokenStyle)) { - return UUID.randomUUID().toString(); - } - // 简单uuid (不带下划线) - if(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID.equals(tokenStyle)) { - return UUID.randomUUID().toString().replaceAll("-", ""); - } - // 32位随机字符串 - if(SaTokenConsts.TOKEN_STYLE_RANDOM_32.equals(tokenStyle)) { - return SaFoxUtil.getRandomString(32); - } - // 64位随机字符串 - if(SaTokenConsts.TOKEN_STYLE_RANDOM_64.equals(tokenStyle)) { - return SaFoxUtil.getRandomString(64); - } - // 128位随机字符串 - if(SaTokenConsts.TOKEN_STYLE_RANDOM_128.equals(tokenStyle)) { - return SaFoxUtil.getRandomString(128); - } - // tik风格 (2_14_16) - if(SaTokenConsts.TOKEN_STYLE_TIK.equals(tokenStyle)) { - return SaFoxUtil.getRandomString(2) + "_" + SaFoxUtil.getRandomString(14) + "_" + SaFoxUtil.getRandomString(16) + "__"; - } - // 默认,还是uuid - return UUID.randomUUID().toString(); - } - - /** - * 创建一个Session - */ - @Override - public SaSession createSession(String sessionId) { - return new SaSession(sessionId); - } - - /** - * 判断:集合中是否包含指定元素(模糊匹配) - */ - @Override - public boolean hasElement(List list, String element) { - - // 空集合直接返回false - if(list == null || list.size() == 0) { - return false; - } - - // 先尝试一下简单匹配,如果可以匹配成功则无需继续模糊匹配 - if (list.contains(element)) { - return true; - } - - // 开始模糊匹配 - for (String patt : list) { - if(SaFoxUtil.vagueMatch(patt, element)) { - return true; - } - } - - // 走出for循环说明没有一个元素可以匹配成功 - return false; - } - - /** - * 对一个Method对象进行注解检查(注解鉴权内部实现) - */ - @Override - public void checkMethodAnnotation(Method method) { - - // 先校验 Method 所属 Class 上的注解 - validateAnnotation(method.getDeclaringClass()); - - // 再校验 Method 上的注解 - validateAnnotation(method); - } - - /** - * 从指定元素校验注解 - * @param target see note - */ - protected void validateAnnotation(AnnotatedElement target) { - - // 校验 @SaCheckLogin 注解 - if(target.isAnnotationPresent(SaCheckLogin.class)) { - SaCheckLogin at = target.getAnnotation(SaCheckLogin.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckRole 注解 - if(target.isAnnotationPresent(SaCheckRole.class)) { - SaCheckRole at = target.getAnnotation(SaCheckRole.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckPermission 注解 - if(target.isAnnotationPresent(SaCheckPermission.class)) { - SaCheckPermission at = target.getAnnotation(SaCheckPermission.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckSafe 注解 - if(target.isAnnotationPresent(SaCheckSafe.class)) { - SaCheckSafe at = target.getAnnotation(SaCheckSafe.class); - SaManager.getStpLogic(null).checkByAnnotation(at); - } - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckDisable.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckDisable.java new file mode 100644 index 000000000..3af6e3ab5 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckDisable.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; + +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; + +/** + * 服务禁用校验:判断当前账号是否被禁用了指定服务,如果被禁用,会抛出异常,没有被禁用才能进入方法。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author videomonster + * @since 1.31.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaCheckDisable { + + /** + * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 + * + * @return / + */ + String type() default ""; + + /** + * 服务标识 (具体你要校验是否禁用的服务名称) + * + * @return / + */ + String[] value() default { SaTokenConsts.DEFAULT_DISABLE_SERVICE }; + + /** + * 封禁等级(如果当前账号的被封禁等级 ≥ 此值,请求就无法进入方法) + * + * @return / + */ + int level() default SaTokenConsts.DEFAULT_DISABLE_LEVEL; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpBasic.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpBasic.java new file mode 100644 index 000000000..265b85edc --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpBasic.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; + +import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Http Basic 认证校验:只有通过 Http Basic 认证后才能进入该方法,否则抛出异常。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.26.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaCheckHttpBasic { + + /** + * 领域 + * @return / + */ + String realm() default SaHttpBasicTemplate.DEFAULT_REALM; + + /** + * 需要校验的账号密码,格式形如 sa:123456 + * @return / + */ + String account() default ""; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java new file mode 100644 index 000000000..816b3e36c --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; + +import cn.dev33.satoken.httpauth.digest.SaHttpDigestModel; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Http Digest 认证校验:只有通过 Http Digest 认证后才能进入该方法,否则抛出异常。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.38.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaCheckHttpDigest { + + /** + * 用户名 + * @return / + */ + String username() default ""; + + /** + * 密码 + * @return / + */ + String password() default ""; + + /** + * 领域 + * @return / + */ + String realm() default SaHttpDigestModel.DEFAULT_REALM; + + /** + * 需要校验的用户名和密码,格式形如 sa:123456 + * @return / + */ + String value() default ""; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckLogin.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckLogin.java index a2cf62e64..f0e12e467 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckLogin.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckLogin.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; import java.lang.annotation.ElementType; @@ -6,18 +21,21 @@ import java.lang.annotation.Target; /** - * 登录认证:只有登录之后才能进入该方法 - *

可标注在函数、类上(效果等同于标注在此类的所有方法上) - * @author kong + * 登录认证校验:只有登录之后才能进入该方法。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * + * @author click33 + * @since 1.10.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckLogin { /** - * 多账号体系下所属的账号体系标识 - * @return see note + * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 + * + * @return / */ String type() default ""; 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 new file mode 100644 index 000000000..f5c970cf0 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckOr.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; + +import java.lang.annotation.*; + +/** + * 批量注解鉴权:只要满足其中一个注解即可通过验证 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.35.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaCheckOr { + + /** + * 设定 @SaCheckLogin,参考 {@link SaCheckLogin} + * + * @return / + */ + SaCheckLogin[] login() default {}; + + /** + * 设定 @SaCheckRole,参考 {@link SaCheckRole} + * + * @return / + */ + SaCheckRole[] role() default {}; + + /** + * 设定 @SaCheckPermission,参考 {@link SaCheckPermission} + * + * @return / + */ + SaCheckPermission[] permission() default {}; + + /** + * 设定 @SaCheckSafe,参考 {@link SaCheckSafe} + * + * @return / + */ + SaCheckSafe[] safe() default {}; + + /** + * 设定 @SaCheckHttpBasic,参考 {@link SaCheckHttpBasic} + * + * @return / + */ + SaCheckHttpBasic[] httpBasic() default {}; + + /** + * 设定 @SaCheckBasic,参考 {@link SaCheckHttpDigest} + * + * @return / + */ + SaCheckHttpDigest[] httpDigest() default {}; + + /** + * 设定 @SaCheckDisable,参考 {@link SaCheckDisable} + * + * @return / + */ + SaCheckDisable[] disable() default {}; + + /** + * 需要追加抓取的注解 Class (只能填写 Sa-Token 相关注解类型) + * + * @return / + */ + Class[] append() default {}; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckPermission.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckPermission.java index c97883b93..4c73cead8 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckPermission.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckPermission.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; import java.lang.annotation.ElementType; @@ -6,31 +21,53 @@ import java.lang.annotation.Target; /** - * 权限认证:必须具有指定权限才能进入该方法 - *

可标注在函数、类上(效果等同于标注在此类的所有方法上) - * @author kong + * 权限认证校验:必须具有指定权限才能进入该方法。 * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.10.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckPermission { /** - * 需要校验的权限码 - * @return 需要校验的权限码 + * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 + * + * @return / + */ + String type() default ""; + + /** + * 需要校验的权限码 [ 数组 ] + * + * @return / */ String [] value() default {}; /** * 验证模式:AND | OR,默认AND - * @return 验证模式 + * + * @return / */ SaMode mode() default SaMode.AND; - /** - * 多账号体系下所属的账号体系标识 - * @return see note - */ - String type() default ""; - + /** + * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验 + * + *

+ * 例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-core/src/main/java/cn/dev33/satoken/annotation/SaCheckRole.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckRole.java index 79ebe55de..dc99130b7 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckRole.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckRole.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; import java.lang.annotation.ElementType; @@ -6,32 +21,36 @@ import java.lang.annotation.Target; /** - * 角色认证:必须具有指定角色标识才能进入该方法 - *

可标注在函数、类上(效果等同于标注在此类的所有方法上) - * @author kong + * 角色认证校验:必须具有指定角色标识才能进入该方法。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * + * @author click33 + * @since 1.10.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckRole { /** - * 需要校验的角色标识 - * @return 需要校验的角色标识 + * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 + * + * @return / + */ + String type() default ""; + + /** + * 需要校验的角色标识 [ 数组 ] + * + * @return / */ String [] value() default {}; /** * 验证模式:AND | OR,默认AND - * @return 验证模式 + * + * @return / */ SaMode mode() default SaMode.AND; - /** - * 账号类型 - *

建议使用常量,避免因错误拼写带来的bug - * @return see note - */ - String type() default ""; - } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckSafe.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckSafe.java index ba559d789..e6c9aaf32 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckSafe.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckSafe.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; import java.lang.annotation.ElementType; @@ -5,14 +20,32 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import cn.dev33.satoken.util.SaTokenConsts; + /** - * 二级认证校验:必须二级认证之后才能进入该方法 - *

可标注在函数、类上(效果等同于标注在此类的所有方法上) - * @author kong + * 二级认证校验:客户端必须完成二级认证之后,才能进入该方法,否则将被抛出异常。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上)。 * + * @author click33 + * @since 1.21.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckSafe { + /** + * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 + * + * @return / + */ + String type() default ""; + + /** + * 要校验的服务 + * + * @return / + */ + String value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE; + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaIgnore.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaIgnore.java new file mode 100644 index 000000000..71920d9ef --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaIgnore.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 忽略认证:表示被修饰的方法或类无需进行注解认证和路由拦截认证。 + * + *

请注意:此注解的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效,对自定义拦截器与过滤器不生效。

+ * + * @author click33 + * @since 1.31.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaIgnore { + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaMode.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaMode.java index 684bcce3f..1c460c78c 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaMode.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaMode.java @@ -1,9 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; /** - * 注解鉴权的验证模式 - * @author kong + * 注解鉴权的验证模式 * + * @author click33 + * @since 1.10.0 */ public enum SaMode { 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 000000000..3a1f26096 --- /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.com + * + * 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.AnnotatedElement; + +/** + * 所有注解处理器的父接口 + * + * @author click33 + * @since 2024/8/2 + */ +public interface SaAnnotationHandlerInterface { + + /** + * 获取所要处理的注解类型 + * @return / + */ + Class getHandlerAnnotationClass(); + + /** + * 所需要执行的校验方法 + * @param at 注解对象 + * @param element 被标注的注解的元素(方法/类)引用 + */ + @SuppressWarnings("unchecked") + default void check(Annotation at, AnnotatedElement element) { + checkMethod((T) at, element); + } + + /** + * 所需要执行的校验方法(转换类型后) + * @param at 注解对象 + * @param element 被标注的注解的元素(方法/类)引用 + */ + void checkMethod(T at, AnnotatedElement element); + +} \ 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 000000000..e8487fdce --- /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.com + * + * 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.AnnotatedElement; + +/** + * 注解 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, AnnotatedElement element) { + _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 000000000..f882ef138 --- /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.com + * + * 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.AnnotatedElement; + +/** + * 注解 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, AnnotatedElement element) { + _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 000000000..c2e3c07f5 --- /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.com + * + * 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.AnnotatedElement; + +/** + * 注解 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, AnnotatedElement element) { + _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 000000000..392bfe54d --- /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.com + * + * 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.AnnotatedElement; + +/** + * 注解 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, AnnotatedElement element) { + _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 000000000..b4cf2019b --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckOrHandler.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.AnnotatedElement; +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, AnnotatedElement element) { + _checkMethod(at.login(), at.role(), at.permission(), at.safe(), at.httpBasic(), at.httpDigest(), at.disable(), at.append(), element); + } + + public static void _checkMethod( + SaCheckLogin[] login, + SaCheckRole[] role, + SaCheckPermission[] permission, + SaCheckSafe[] safe, + SaCheckHttpBasic[] httpBasic, + SaCheckHttpDigest[] httpDigest, + SaCheckDisable[] disable, + Class[] append, + AnnotatedElement element + ) { + // 先把所有注解塞到一个 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)); + for (Class annotationClass : append) { + Annotation annotation = SaAnnotationStrategy.instance.getAnnotation.apply(element, annotationClass); + if(annotation != null) { + annotationList.add(annotation); + } + } + + // 如果 atList 为空,说明 SaCheckOr 上不包含任何注解校验,我们直接跳过即可 + if(annotationList.isEmpty()) { + return; + } + + // 逐个开始校验 >>> + List errorList = new ArrayList<>(); + for (Annotation item : annotationList) { + try { + SaAnnotationStrategy.instance.annotationHandlerMap.get(item.annotationType()).check(item, element); + // 只要有一个校验通过,就可以直接返回了 + 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 000000000..b82d3e082 --- /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.com + * + * 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.AnnotatedElement; + +/** + * 注解 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, AnnotatedElement element) { + _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 000000000..ea9880475 --- /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.com + * + * 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.AnnotatedElement; + +/** + * 注解 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, AnnotatedElement element) { + _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 000000000..403dec7fc --- /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.com + * + * 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.AnnotatedElement; + +/** + * 注解 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, AnnotatedElement element) { + _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 000000000..c1e4b7416 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaIgnoreHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.AnnotatedElement; + +/** + * 注解 SaIgnore 的处理器 + *

v1.43.0 版本起,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, AnnotatedElement element) { + _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/application/ApplicationInfo.java b/sa-token-core/src/main/java/cn/dev33/satoken/application/ApplicationInfo.java new file mode 100644 index 000000000..be9347381 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/application/ApplicationInfo.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.application; + +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 应用全局信息 + * + * @author click33 + * @since 1.31.0 + */ +public class ApplicationInfo { + + /** + * 应用前缀 + */ + public static String routePrefix; + + /** + * 为指定 path 裁剪掉 routePrefix 前缀 + * @param path 指定 path + * @return / + */ + public static String cutPathPrefix(String path) { + if(! SaFoxUtil.isEmpty(routePrefix) && ! routePrefix.equals("/") && path.startsWith(routePrefix)){ + path = path.substring(routePrefix.length()); + } + return path; + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/application/SaApplication.java b/sa-token-core/src/main/java/cn/dev33/satoken/application/SaApplication.java new file mode 100644 index 000000000..21e778c15 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/application/SaApplication.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.application; + +import java.util.ArrayList; +import java.util.List; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; + +/** + * Application Model,全局作用域的读取值对象。 + * + *

在应用全局范围内: 存值、取值。数据在应用重启后失效,如果集成了 Redis,则在 Redis 重启后失效。 + * + * @author click33 + * @since 1.31.0 + */ +public class SaApplication implements SaSetValueInterface { + + /** + * 默认实例 + */ + public static SaApplication defaultInstance = new SaApplication(); + + // ---- 实现接口存取值方法 + + /** 取值 */ + @Override + public Object get(String key) { + return SaManager.getSaTokenDao().getObject(splicingDataKey(key)); + } + + /** 写值 */ + @Override + public SaApplication set(String key, Object value) { + return set(key, value, SaTokenDao.NEVER_EXPIRE); + } + + /** 删值 */ + @Override + public SaApplication delete(String key) { + SaManager.getSaTokenDao().deleteObject(splicingDataKey(key)); + return this; + } + + + // ---- 其它方法 + + /** + * 写值 + * @param key 名称 + * @param value 值 + * @param ttl 有效时间(单位:秒) + * @return 对象自身 + */ + public SaApplication set(String key, Object value, long ttl) { + SaManager.getSaTokenDao().setObject(splicingDataKey(key), value, ttl); + return this; + } + + /** + * 返回当前存入的所有 key + * @return / + */ + public List keys() { + // 从缓存中查询出所有此前缀的 key + String prefix = splicingDataKey(""); + List list = SaManager.getSaTokenDao().searchData(prefix, "", 0, -1, true); + + // 裁减掉固定前缀,保留 key 名称,塞入新集合 + int prefixLength = prefix.length(); + List list2 = new ArrayList<>(); + if(list != null) { + for (String key : list) { + list2.add(key.substring(prefixLength)); + } + } + + // 返回 + return list2; + } + + /** + * 清空当前存入的所有 key + */ + public void clear() { + List keys = keys(); + for (String key : keys) { + delete(key); + } + } + + /** + * 拼接key:当存入一个变量时,应该使用的 key + * + * @param key 原始 key + * @return 拼接后的 key 值 + */ + public String splicingDataKey(String key) { + return SaManager.getConfig().getTokenName() + ":var:" + key; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/application/SaGetValueInterface.java b/sa-token-core/src/main/java/cn/dev33/satoken/application/SaGetValueInterface.java new file mode 100644 index 000000000..7e44f9842 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/application/SaGetValueInterface.java @@ -0,0 +1,169 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.application; + +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 对取值的一组方法封装 + *

封装 SaStorage、SaSession、SaApplication 等存取值的一些固定方法,减少重复编码

+ * + * @author click33 + * @since 1.31.0 + */ +public interface SaGetValueInterface { + + // --------- 需要子类实现的方法 + + /** + * 取值 + * @param key key + * @return 值 + */ + Object get(String key); + + + // --------- 接口提供封装的方法 + + /** + * 取值 (指定默认值) + * + * @param 默认值的类型 + * @param key key + * @param defaultValue 取不到值时返回的默认值 + * @return 值 + */ + default T get(String key, T defaultValue) { + return getValueByDefaultValue(get(key), defaultValue); + } + + /** + * 取值 (转String类型) + * @param key key + * @return 值 + */ + default String getString(String key) { + Object value = get(key); + if(value == null) { + return null; + } + return String.valueOf(value); + } + + /** + * 取值 (转int类型) + * @param key key + * @return 值 + */ + default int getInt(String key) { + return getValueByDefaultValue(get(key), 0); + } + + /** + * 取值 (转long类型) + * @param key key + * @return 值 + */ + default long getLong(String key) { + return getValueByDefaultValue(get(key), 0L); + } + + /** + * 取值 (转double类型) + * @param key key + * @return 值 + */ + default double getDouble(String key) { + return getValueByDefaultValue(get(key), 0.0); + } + + /** + * 取值 (转float类型) + * @param key key + * @return 值 + */ + default float getFloat(String key) { + return getValueByDefaultValue(get(key), 0.0f); + } + + /** + * 取值 (指定转换类型) + * @param 泛型 + * @param key key + * @param cs 指定转换类型 + * @return 值 + */ + default T getModel(String key, Class cs) { + return SaFoxUtil.getValueByType(get(key), cs); + } + + /** + * 取值 (指定转换类型, 并指定值为 null 时返回的默认值) + * @param 泛型 + * @param key key + * @param cs 指定转换类型 + * @param defaultValue 值为Null时返回的默认值 + * @return 值 + */ + @SuppressWarnings("unchecked") + default T getModel(String key, Class cs, Object defaultValue) { + T model = getModel(key, cs); + return valueIsNull(model) ? (T)defaultValue : model; + } + + /** + * 是否含有某个 key + * @param key 指定 key + * @return 是否含有 + */ + default boolean has(String key) { + return !valueIsNull(get(key)); + } + + + // --------- 内部工具方法 + + /** + * 判断一个值是否为null + * @param value 指定值 + * @return 此value是否为null + */ + default boolean valueIsNull(Object value) { + return value == null || value.equals(""); + } + + /** + * 根据默认值来获取值 + * @param 泛型 + * @param value 值 + * @param defaultValue 默认值 + * @return 转换后的值 + */ + @SuppressWarnings("unchecked") + default T getValueByDefaultValue(Object value, T defaultValue) { + + // 如果 obj 为 null,则直接返回默认值 + if(valueIsNull(value)) { + return defaultValue; + } + + // 开始转换类型 + Class cs = (Class) defaultValue.getClass(); + return SaFoxUtil.getValueByType(value, cs); + } + + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java b/sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java new file mode 100644 index 000000000..74d43eb02 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.application; + +import cn.dev33.satoken.fun.SaRetGenericFunction; + +/** + * 对写值的一组方法封装 + *

封装 SaStorage、SaSession、SaApplication 等存取值的一些固定方法,减少重复编码

+ * + * @author click33 + * @since 1.31.0 + */ +public interface SaSetValueInterface extends SaGetValueInterface { + + // --------- 需要子类实现的方法 + + /** + * 写值 + * @param key 名称 + * @param value 值 + * @return 对象自身 + */ + SaSetValueInterface set(String key, Object value); + + /** + * 删值 + * @param key 要删除的key + * @return 对象自身 + */ + SaSetValueInterface delete(String key); + + + // --------- 接口提供封装的方法 + + /** + * + * 取值 (如果值为 null,则执行 fun 函数获取值,并把函数返回值写入缓存) + * @param 返回值的类型 + * @param key key + * @param fun 值为null时执行的函数 + * @return 值 + */ + @SuppressWarnings("unchecked") + default T get(String key, SaRetGenericFunction fun) { + Object value = get(key); + if(value == null) { + value = fun.run(); + set(key, value); + } + return (T) value; + } + + /** + * 写值 (只有在此 key 原本无值的情况下才会写入) + * @param key 名称 + * @param value 值 + * @return 对象自身 + */ + default SaSetValueInterface setByNull(String key, Object value) { + if( ! has(key)) { + set(key, value); + } + return this; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaCookieConfig.java b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaCookieConfig.java new file mode 100644 index 000000000..90cdd87f2 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaCookieConfig.java @@ -0,0 +1,217 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sa-Token Cookie写入 相关配置 + * + * @author click33 + * @since 1.27.0 + */ +public class SaCookieConfig { + + /* + Cookie 功能为浏览器通用标准,建议大家自行搜索文章了解各个属性的功能含义,此处源码仅做简单解释。 + */ + + /** + * 作用域 + *

写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。

+ *

一般情况下你不需要设置此值,因为浏览器默认会把 Cookie 写到当前域名下。

+ */ + private String domain; + + /** + * 路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。) + */ + private String path; + + /** + * 是否只在 https 协议下有效 + */ + private Boolean secure = false; + + /** + * 是否禁止 js 操作 Cookie + */ + private Boolean httpOnly = false; + + /** + * 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) + */ + private String sameSite; + + /** + * 额外扩展属性 + */ + private Map extraAttrs = new LinkedHashMap<>(); + + + /** + * 获取:Cookie 作用域 + *

写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。

+ *

一般情况下你不需要设置此值,因为浏览器默认会把 Cookie 写到当前域名下。

+ * @return / + */ + public String getDomain() { + return domain; + } + + /** + * 写入:Cookie 作用域 + *

写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。

+ *

一般情况下你不需要设置此值,因为浏览器默认会把 Cookie 写到当前域名下。

+ * @param domain / + * @return 对象自身 + */ + public SaCookieConfig setDomain(String domain) { + this.domain = domain; + return this; + } + + /** + * @return 路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。) + */ + public String getPath() { + return path; + } + + /** + * @param path 路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。) + * @return 对象自身 + */ + public SaCookieConfig setPath(String path) { + this.path = path; + return this; + } + + /** + * @return 是否只在 https 协议下有效 + */ + public Boolean getSecure() { + return secure; + } + + /** + * @param secure 是否只在 https 协议下有效 + * @return 对象自身 + */ + public SaCookieConfig setSecure(Boolean secure) { + this.secure = secure; + return this; + } + + /** + * @return 是否禁止 js 操作 Cookie + */ + public Boolean getHttpOnly() { + return httpOnly; + } + + /** + * @param httpOnly 是否禁止 js 操作 Cookie + * @return 对象自身 + */ + public SaCookieConfig setHttpOnly(Boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + /** + * @return 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) + */ + public String getSameSite() { + return sameSite; + } + + /** + * @param sameSite 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) + * @return 对象自身 + */ + public SaCookieConfig setSameSite(String sameSite) { + this.sameSite = sameSite; + return this; + } + + /** + * @return 获取额外扩展属性 + */ + public Map getExtraAttrs() { + return extraAttrs; + } + + /** + * 写入额外扩展属性 + * @param extraAttrs / + * @return 对象自身 + */ + public SaCookieConfig setExtraAttrs(Map extraAttrs) { + this.extraAttrs = extraAttrs; + return this; + } + + /** + * 追加扩展属性 + * @param name / + * @param value / + * @return 对象自身 + */ + public SaCookieConfig addExtraAttr(String name, String value) { + if (extraAttrs == null) { + extraAttrs = new LinkedHashMap<>(); + } + this.extraAttrs.put(name, value); + return this; + } + + /** + * 追加扩展属性 + * @param name / + * @return 对象自身 + */ + public SaCookieConfig addExtraAttr(String name) { + return this.addExtraAttr(name, null); + } + + /** + * 移除指定扩展属性 + * @param name / + * @return 对象自身 + */ + public SaCookieConfig removeExtraAttr(String name) { + if(extraAttrs != null) { + this.extraAttrs.remove(name); + } + return this; + } + + // toString + @Override + public String toString() { + return "SaCookieConfig [" + + "domain=" + domain + + ", path=" + path + + ", secure=" + secure + + ", httpOnly=" + httpOnly + + ", sameSite=" + sameSite + + ", extraAttrs=" + extraAttrs + + "]"; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSsoConfig.java b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSsoConfig.java deleted file mode 100644 index 1937dafc3..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaSsoConfig.java +++ /dev/null @@ -1,314 +0,0 @@ -package cn.dev33.satoken.config; - -import java.io.Serializable; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; - -import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.util.SaFoxUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-Token-SSO 单点登录模块 配置Model - * @author kong - * - */ -public class SaSsoConfig implements Serializable { - - private static final long serialVersionUID = -6541180061782004705L; - - /** - * Ticket有效期 (单位: 秒) - */ - public long ticketTimeout = 60 * 5; - - /** - * 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) - */ - public String allowUrl = "*"; - - /** - * 接口调用秘钥 (用于SSO模式三单点注销的接口通信身份校验) - */ - public String secretkey; - - /** - * SSO-Server端 单点登录地址 - */ - public String authUrl; - - /** - * SSO-Server端 Ticket校验地址 - */ - public String checkTicketUrl; - - /** - * SSO-Server端 单点注销地址 - */ - public String sloUrl; - - /** - * SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取) - */ - public String ssoLogoutCall; - - /** - * SSO-Server端 账号资料查询地址 - */ - public String userinfoUrl; - - - - /** - * @return Ticket有效期 (单位: 秒) - */ - public long getTicketTimeout() { - return ticketTimeout; - } - - /** - * @param ticketTimeout Ticket有效期 (单位: 秒) - * @return 对象自身 - */ - public SaSsoConfig setTicketTimeout(long ticketTimeout) { - this.ticketTimeout = ticketTimeout; - return this; - } - - /** - * @return 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) - */ - public String getAllowUrl() { - return allowUrl; - } - - /** - * @param allowUrl 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) - * @return 对象自身 - */ - public SaSsoConfig setAllowUrl(String allowUrl) { - this.allowUrl = allowUrl; - return this; - } - - /** - * @return 调用秘钥 (用于SSO模式三单点注销的接口通信身份校验) - */ - public String getSecretkey() { - return secretkey; - } - - /** - * @param secretkey 调用秘钥 (用于SSO模式三单点注销的接口通信身份校验) - * @return 对象自身 - */ - public SaSsoConfig setSecretkey(String secretkey) { - this.secretkey = secretkey; - return this; - } - - /** - * @return SSO-Server端 单点登录地址 - */ - public String getAuthUrl() { - return authUrl; - } - - /** - * @param authUrl SSO-Server端 单点登录地址 - * @return 对象自身 - */ - public SaSsoConfig setAuthUrl(String authUrl) { - this.authUrl = authUrl; - return this; - } - - /** - * @return SSO-Server端Ticket校验地址 - */ - public String getCheckTicketUrl() { - return checkTicketUrl; - } - - /** - * @param checkTicketUrl SSO-Server端Ticket校验地址 - */ - public SaSsoConfig setCheckTicketUrl(String checkTicketUrl) { - this.checkTicketUrl = checkTicketUrl; - return this; - } - - /** - * @return SSO-Server端单点注销地址 - */ - public String getSloUrl() { - return sloUrl; - } - - /** - * @param sloUrl SSO-Server端单点注销地址 - * @return 对象自身 - */ - public SaSsoConfig setSloUrl(String sloUrl) { - this.sloUrl = sloUrl; - return this; - } - - /** - * @return SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取) - */ - public String getSsoLogoutCall() { - return ssoLogoutCall; - } - - /** - * @param ssoLogoutCall SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取) - * @return 对象自身 - */ - public SaSsoConfig setSsoLogoutCall(String ssoLogoutCall) { - this.ssoLogoutCall = ssoLogoutCall; - return this; - } - - /** - * @return SSO-Server端 账号资料查询地址 - */ - public String getUserinfoUrl() { - return userinfoUrl; - } - - /** - * @param userinfoUrl SSO-Server端 账号资料查询地址 - * @return 对象自身 - */ - public SaSsoConfig setUserinfoUrl(String userinfoUrl) { - this.userinfoUrl = userinfoUrl; - return this; - } - - @Override - public String toString() { - return "SaSsoConfig [ticketTimeout=" + ticketTimeout + ", allowUrl=" + allowUrl + ", secretkey=" + secretkey - + ", authUrl=" + authUrl + ", checkTicketUrl=" + checkTicketUrl + ", sloUrl=" + sloUrl - + ", ssoLogoutCall=" + ssoLogoutCall + ", userinfoUrl=" + userinfoUrl + ", isHttp=" + isHttp + ", isSlo=" + isSlo + "]"; - } - - - /** - * 以数组形式写入允许的授权回调地址 - * @param url 所有集合 - * @return 对象自身 - */ - public SaSsoConfig setAllow(String ...url) { - this.allowUrl = SaFoxUtil.arrayJoin(url); - return this; - } - - // -------------------- SaSsoHandle 相关配置 -------------------- - - /** - * 是否使用http请求校验ticket值 - */ - public Boolean isHttp = false; - - /** - * 是否打开单点注销 - */ - public Boolean isSlo = false; - - - /** - * @return isHttp 是否使用http请求校验ticket值 - */ - public Boolean getIsHttp() { - return isHttp; - } - - /** - * @param isHttp 是否使用http请求校验ticket值 - * @return 对象自身 - */ - public SaSsoConfig setIsHttp(Boolean isHttp) { - this.isHttp = isHttp; - return this; - } - - /** - * @return 是否打开单点注销 - */ - public Boolean getIsSlo() { - return isSlo; - } - - /** - * @param isSlo 是否打开单点注销 - * @return 对象自身 - */ - public SaSsoConfig setIsSlo(Boolean isSlo) { - this.isSlo = isSlo; - return this; - } - - - // -------------------- SaSsoHandle 所有回调函数 -------------------- - - - /** - * SSO-Server端:未登录时返回的View - */ - public Supplier notLoginView = () -> "当前会话在SSO-Server认证中心尚未登录"; - - /** - * SSO-Server端:登录函数 - */ - public BiFunction doLoginHandle = (name, pwd) -> SaResult.error(); - - /** - * SSO-Client端:自定义校验Ticket返回值的处理逻辑 (每次从认证中心获取校验Ticket的结果后调用) - */ - public BiFunction ticketResultHandle = null; - - /** - * SSO-Client端:发送Http请求的处理函数 - */ - public Function sendHttp = url -> {throw new SaTokenException("请配置Http处理器");}; - - - /** - * @param notLoginView SSO-Server端:未登录时返回的View - * @return 对象自身 - */ - public SaSsoConfig setNotLoginView(Supplier notLoginView) { - this.notLoginView = notLoginView; - return this; - } - - /** - * @param doLoginHandle SSO-Server端:登录函数 - * @return 对象自身 - */ - public SaSsoConfig setDoLoginHandle(BiFunction doLoginHandle) { - this.doLoginHandle = doLoginHandle; - return this; - } - - /** - * @param SSO-Client端:自定义校验Ticket返回值的处理逻辑 (每次从认证中心获取校验Ticket的结果后调用) - * @return 对象自身 - */ - public SaSsoConfig setTicketResultHandle(BiFunction ticketResultHandle) { - this.ticketResultHandle = ticketResultHandle; - return this; - } - - /** - * @param sendHttp SSO-Client端:发送Http请求的处理函数 - * @return 对象自身 - */ - public SaSsoConfig setSendHttp(Function sendHttp) { - this.sendHttp = sendHttp; - return this; - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java index 16af53cad..df40f1298 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java @@ -1,96 +1,241 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.config; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutRange; +import cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode; +import cn.dev33.satoken.stp.parameter.enums.SaReplacedRange; +import cn.dev33.satoken.util.SaFoxUtil; + import java.io.Serializable; /** * Sa-Token 配置类 Model + * *

- * 你可以通过yml、properties、java代码等形式配置本类参数,具体请查阅官方文档: http://sa-token.dev33.cn/ - * - * @author kong + * 你可以通过yml、properties、java代码等形式配置本类参数,具体请查阅官方文档: + * https://sa-token.com + *

* + * @author click33 + * @since 1.10.0 */ public class SaTokenConfig implements Serializable { private static final long serialVersionUID = -6541180061782004705L; - /** token名称 (同时也是cookie名称) */ + /** token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀) */ private String tokenName = "satoken"; - /** token的长久有效期(单位:秒) 默认30天, -1代表永久 */ + /** token 有效期(单位:秒) 默认30天,-1 代表永久有效 */ private long timeout = 60 * 60 * 24 * 30; /** - * token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制 - * (例如可以设置为1800代表30分钟内无操作就过期) + * token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) */ - private long activityTimeout = -1; + private long activeTimeout = -1; - /** 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) */ + /** + * 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数 + */ + private Boolean dynamicActiveTimeout = false; + + /** + * 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + */ private Boolean isConcurrent = true; - /** 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) */ - private Boolean isShare = true; + /** + * 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + */ + private Boolean isShare = false; + + /** + * 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) + */ + private SaReplacedLoginExitMode replacedLoginExitMode = SaReplacedLoginExitMode.OLD_DEVICE; + + /** + * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) + */ + private SaReplacedRange replacedRange = SaReplacedRange.CURR_DEVICE_TYPE; - /** 是否尝试从请求体里读取token */ + /** + * 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + */ + private int maxLoginCount = 12; + + /** + * 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + */ + private SaLogoutMode overflowLogoutMode = SaLogoutMode.LOGOUT; + + /** + * 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) + */ + private int maxTryTimes = 12; + + /** + * 是否尝试从请求体里读取 token + */ private Boolean isReadBody = true; - /** 是否尝试从header里读取token */ - private Boolean isReadHead = true; + /** + * 是否尝试从 header 里读取 token + */ + private Boolean isReadHeader = true; - /** 是否尝试从cookie里读取token */ + /** + * 是否尝试从 cookie 里读取 token + */ private Boolean isReadCookie = true; - /** token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */ + /** + * 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + */ + private Boolean isLastingCookie = true; + + /** + * 是否在登录后将 token 写入到响应头 + */ + private Boolean isWriteHeader = false; + + /** + * 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) + *
(此参数只在调用 StpUtil.logout() 时有效) + */ + private SaLogoutRange logoutRange = SaLogoutRange.TOKEN; + + /** + * 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) + *
(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token") 时有效) + */ + private Boolean isLogoutKeepFreezeOps = false; + + /** + * 在注销 token 后,是否保留其对应的 Token-Session + */ + private Boolean isLogoutKeepTokenSession = false; + + /** + * 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) + */ + private Boolean rightNowCreateTokenSession = false; + + /** + * token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + */ private String tokenStyle = "uuid"; - /** 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理 */ + /** + * 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理 + */ private int dataRefreshPeriod = 30; - /** 获取[token专属session]时是否必须登录 (如果配置为true,会在每次获取[token-session]时校验是否登录) */ + /** + * 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录) + */ private Boolean tokenSessionCheckLogin = true; - /** 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作) */ + /** + * 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作) + */ private Boolean autoRenew = true; - /** 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 */ - private String cookieDomain; - - /** token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) */ + /** + * token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) + */ private String tokenPrefix; - /** 是否在初始化配置时打印版本字符画 */ + /** + * cookie 模式是否自动填充 token 前缀 + */ + private Boolean cookieAutoFillPrefix = false; + + /** + * 是否在初始化配置时在控制台打印版本字符画 + */ private Boolean isPrint = true; - /** 是否打印操作日志 */ + /** + * 是否打印操作日志 + */ private Boolean isLog = false; /** - * jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) + * 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动 + */ + private String logLevel = "trace"; + + /** + * 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动 + */ + private int logLevelInt = 1; + + /** + * 是否打印彩色日志 + */ + private Boolean isColorLog = null; + + /** + * jwt秘钥(只有集成 jwt 相关模块时此参数才会生效) */ private String jwtSecretKey; - + + /** + * Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456 + */ + private String httpBasic = ""; + + /** + * Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 + */ + private String httpDigest = ""; + + /** + * 配置当前项目的网络访问地址 + */ + private String currDomain; + /** - * Id-Token的有效期 (单位: 秒) + * Same-Token 的有效期 (单位: 秒) */ - private long idTokenTimeout = 60 * 60 * 24; - + private long sameTokenTimeout = 60 * 60 * 24; /** - * SSO单点登录配置对象 + * 是否校验 Same-Token(部分rpc插件有效) */ - public SaSsoConfig sso = new SaSsoConfig(); - + private Boolean checkSameToken = false; /** - * @return token名称 (同时也是cookie名称) + * Cookie配置对象 + */ + public SaCookieConfig cookie = new SaCookieConfig(); + + /** + * @return token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀) */ public String getTokenName() { return tokenName; } /** - * @param tokenName token名称 (同时也是cookie名称) + * @param tokenName token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀) * @return 对象自身 */ public SaTokenConfig setTokenName(String tokenName) { @@ -99,14 +244,14 @@ public SaTokenConfig setTokenName(String tokenName) { } /** - * @return token的长久有效期(单位:秒) 默认30天, -1代表永久 + * @return token 有效期(单位:秒) 默认30天,-1 代表永久有效 */ public long getTimeout() { return timeout; } /** - * @param timeout token的长久有效期(单位:秒) 默认30天, -1代表永久 + * @param timeout token 有效期(单位:秒) 默认30天,-1 代表永久有效 * @return 对象自身 */ public SaTokenConfig setTimeout(long timeout) { @@ -115,32 +260,48 @@ public SaTokenConfig setTimeout(long timeout) { } /** - * @return token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制 - * (例如可以设置为1800代表30分钟内无操作就过期) + * @return token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) */ - public long getActivityTimeout() { - return activityTimeout; + public long getActiveTimeout() { + return activeTimeout; } /** - * @param activityTimeout token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制 - * (例如可以设置为1800代表30分钟内无操作就过期) + * @param activeTimeout token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) * @return 对象自身 */ - public SaTokenConfig setActivityTimeout(long activityTimeout) { - this.activityTimeout = activityTimeout; + public SaTokenConfig setActiveTimeout(long activeTimeout) { + this.activeTimeout = activeTimeout; return this; } /** - * @return 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) + * @return 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数 + */ + public Boolean getDynamicActiveTimeout() { + return dynamicActiveTimeout; + } + + /** + * @param dynamicActiveTimeout 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数 + * @return 对象自身 + */ + public SaTokenConfig setDynamicActiveTimeout(Boolean dynamicActiveTimeout) { + this.dynamicActiveTimeout = dynamicActiveTimeout; + return this; + } + + /** + * @return 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) */ public Boolean getIsConcurrent() { return isConcurrent; } /** - * @param isConcurrent 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) + * @param isConcurrent 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) * @return 对象自身 */ public SaTokenConfig setIsConcurrent(Boolean isConcurrent) { @@ -149,14 +310,14 @@ public SaTokenConfig setIsConcurrent(Boolean isConcurrent) { } /** - * @return 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) + * @return 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) */ public Boolean getIsShare() { return isShare; } /** - * @param isShare 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) + * @param isShare 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) * @return 对象自身 */ public SaTokenConfig setIsShare(Boolean isShare) { @@ -165,14 +326,46 @@ public SaTokenConfig setIsShare(Boolean isShare) { } /** - * @return 是否尝试从请求体里读取token + * @return 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + */ + public int getMaxLoginCount() { + return maxLoginCount; + } + + /** + * @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + * @return 对象自身 + */ + public SaTokenConfig setMaxLoginCount(int maxLoginCount) { + this.maxLoginCount = maxLoginCount; + return this; + } + + /** + * @return 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) + */ + public int getMaxTryTimes() { + return maxTryTimes; + } + + /** + * @param maxTryTimes 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) + * @return 对象自身 + */ + public SaTokenConfig setMaxTryTimes(int maxTryTimes) { + this.maxTryTimes = maxTryTimes; + return this; + } + + /** + * @return 是否尝试从请求体里读取 token */ public Boolean getIsReadBody() { return isReadBody; } /** - * @param isReadBody 是否尝试从请求体里读取token + * @param isReadBody 是否尝试从请求体里读取 token * @return 对象自身 */ public SaTokenConfig setIsReadBody(Boolean isReadBody) { @@ -181,30 +374,30 @@ public SaTokenConfig setIsReadBody(Boolean isReadBody) { } /** - * @return 是否尝试从header里读取token + * @return 是否尝试从 header 里读取 token */ - public Boolean getIsReadHead() { - return isReadHead; + public Boolean getIsReadHeader() { + return isReadHeader; } /** - * @param isReadHead 是否尝试从header里读取token + * @param isReadHeader 是否尝试从 header 里读取 token * @return 对象自身 */ - public SaTokenConfig setIsReadHead(Boolean isReadHead) { - this.isReadHead = isReadHead; + public SaTokenConfig setIsReadHeader(Boolean isReadHeader) { + this.isReadHeader = isReadHeader; return this; } /** - * @return 是否尝试从cookie里读取token + * @return 是否尝试从 cookie 里读取 token */ public Boolean getIsReadCookie() { return isReadCookie; } /** - * @param isReadCookie 是否尝试从cookie里读取token + * @param isReadCookie 是否尝试从 cookie 里读取 token * @return 对象自身 */ public SaTokenConfig setIsReadCookie(Boolean isReadCookie) { @@ -213,14 +406,50 @@ public SaTokenConfig setIsReadCookie(Boolean isReadCookie) { } /** - * @return token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + * 获取 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + * + * @return isLastingCookie / + */ + public Boolean getIsLastingCookie() { + return this.isLastingCookie; + } + + /** + * 设置 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + * + * @param isLastingCookie / + * @return 对象自身 + */ + public SaTokenConfig setIsLastingCookie(Boolean isLastingCookie) { + this.isLastingCookie = isLastingCookie; + return this; + } + + /** + * @return 是否在登录后将 token 写入到响应头 + */ + public Boolean getIsWriteHeader() { + return isWriteHeader; + } + + /** + * @param isWriteHeader 是否在登录后将 token 写入到响应头 + * @return 对象自身 + */ + public SaTokenConfig setIsWriteHeader(Boolean isWriteHeader) { + this.isWriteHeader = isWriteHeader; + return this; + } + + /** + * @return token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */ public String getTokenStyle() { return tokenStyle; } /** - * @param tokenStyle token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + * @param tokenStyle token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) * @return 对象自身 */ public SaTokenConfig setTokenStyle(String tokenStyle) { @@ -229,15 +458,14 @@ public SaTokenConfig setTokenStyle(String tokenStyle) { } /** - * @return 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理 + * @return 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理 */ public int getDataRefreshPeriod() { return dataRefreshPeriod; } /** - * @param dataRefreshPeriod 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) - * ,默认值30秒,设置为-1代表不启动定时清理 + * @param dataRefreshPeriod 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理 * @return 对象自身 */ public SaTokenConfig setDataRefreshPeriod(int dataRefreshPeriod) { @@ -246,15 +474,14 @@ public SaTokenConfig setDataRefreshPeriod(int dataRefreshPeriod) { } /** - * @return 获取[token专属session]时是否必须登录 (如果配置为true,会在每次获取[token-session]时校验是否登录) + * @return 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录) */ public Boolean getTokenSessionCheckLogin() { return tokenSessionCheckLogin; } /** - * @param tokenSessionCheckLogin 获取[token专属session]时是否必须登录 - * (如果配置为true,会在每次获取[token-session]时校验是否登录) + * @param tokenSessionCheckLogin 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录) * @return 对象自身 */ public SaTokenConfig setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) { @@ -263,14 +490,14 @@ public SaTokenConfig setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) { } /** - * @return 是否打开了自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作) + * @return 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作) */ public Boolean getAutoRenew() { return autoRenew; } /** - * @param autoRenew 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作) + * @param autoRenew 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作) * @return 对象自身 */ public SaTokenConfig setAutoRenew(Boolean autoRenew) { @@ -279,46 +506,46 @@ public SaTokenConfig setAutoRenew(Boolean autoRenew) { } /** - * @return 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 + * @return token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) */ - public String getCookieDomain() { - return cookieDomain; + public String getTokenPrefix() { + return tokenPrefix; } /** - * @param cookieDomain 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 + * @param tokenPrefix token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) * @return 对象自身 */ - public SaTokenConfig setCookieDomain(String cookieDomain) { - this.cookieDomain = cookieDomain; + public SaTokenConfig setTokenPrefix(String tokenPrefix) { + this.tokenPrefix = tokenPrefix; return this; } /** - * @return token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) + * @return cookie 模式是否自动填充 token 前缀 */ - public String getTokenPrefix() { - return tokenPrefix; + public Boolean getCookieAutoFillPrefix() { + return cookieAutoFillPrefix; } /** - * @param tokenPrefix token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) + * @param cookieAutoFillPrefix cookie 模式是否自动填充 token 前缀 * @return 对象自身 */ - public SaTokenConfig setTokenPrefix(String tokenPrefix) { - this.tokenPrefix = tokenPrefix; + public SaTokenConfig setCookieAutoFillPrefix(Boolean cookieAutoFillPrefix) { + this.cookieAutoFillPrefix = cookieAutoFillPrefix; return this; } - + /** - * @return 是否在初始化配置时打印版本字符画 + * @return 是否在初始化配置时在控制台打印版本字符画 */ public Boolean getIsPrint() { return isPrint; } /** - * @param isPrint 是否在初始化配置时打印版本字符画 + * @param isPrint 是否在初始化配置时在控制台打印版本字符画 * @return 对象自身 */ public SaTokenConfig setIsPrint(Boolean isPrint) { @@ -343,14 +570,68 @@ public SaTokenConfig setIsLog(Boolean isLog) { } /** - * @return jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) + * @return 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动 + */ + public String getLogLevel() { + return logLevel; + } + + /** + * @param logLevel 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动 + * @return 对象自身 + */ + public SaTokenConfig setLogLevel(String logLevel) { + this.logLevel = logLevel; + this.logLevelInt = SaFoxUtil.translateLogLevelToInt(logLevel); + return this; + } + + /** + * @return 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动 + */ + public int getLogLevelInt() { + return logLevelInt; + } + + /** + * @param logLevelInt 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动 + * @return 对象自身 + */ + public SaTokenConfig setLogLevelInt(int logLevelInt) { + this.logLevelInt = logLevelInt; + this.logLevel = SaFoxUtil.translateLogLevelToString(logLevelInt); + return this; + } + + /** + * 获取:是否打印彩色日志 + * + * @return isColorLog 是否打印彩色日志 + */ + public Boolean getIsColorLog() { + return this.isColorLog; + } + + /** + * 设置:是否打印彩色日志 + * + * @param isColorLog 是否打印彩色日志 + * @return 对象自身 + */ + public SaTokenConfig setIsColorLog(Boolean isColorLog) { + this.isColorLog = isColorLog; + return this; + } + + /** + * @return jwt秘钥(只有集成 jwt 相关模块时此参数才会生效) */ public String getJwtSecretKey() { return jwtSecretKey; } /** - * @param jwtSecretKey jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) + * @param jwtSecretKey jwt秘钥(只有集成 jwt 相关模块时此参数才会生效) * @return 对象自身 */ public SaTokenConfig setJwtSecretKey(String jwtSecretKey) { @@ -359,69 +640,328 @@ public SaTokenConfig setJwtSecretKey(String jwtSecretKey) { } /** - * @return Id-Token的有效期 (单位: 秒) + * @return Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456 + */ + public String getHttpBasic() { + return httpBasic; + } + + /** + * @param httpBasic Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456 + * @return 对象自身 + */ + public SaTokenConfig setHttpBasic(String httpBasic) { + this.httpBasic = httpBasic; + return this; + } + + /** + * @return Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 + */ + public String getHttpDigest() { + return httpDigest; + } + + /** + * @param httpDigest Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 + * @return 对象自身 + */ + public SaTokenConfig setHttpDigest(String httpDigest) { + this.httpDigest = httpDigest; + return this; + } + + /** + * @return 配置当前项目的网络访问地址 + */ + public String getCurrDomain() { + return currDomain; + } + + /** + * @param currDomain 配置当前项目的网络访问地址 + * @return 对象自身 + */ + public SaTokenConfig setCurrDomain(String currDomain) { + this.currDomain = currDomain; + return this; + } + + /** + * @return Same-Token 的有效期 (单位: 秒) + */ + public long getSameTokenTimeout() { + return sameTokenTimeout; + } + + /** + * @param sameTokenTimeout Same-Token 的有效期 (单位: 秒) + * @return 对象自身 + */ + public SaTokenConfig setSameTokenTimeout(long sameTokenTimeout) { + this.sameTokenTimeout = sameTokenTimeout; + return this; + } + + /** + * @return 是否校验Same-Token(部分rpc插件有效) + */ + public Boolean getCheckSameToken() { + return checkSameToken; + } + + /** + * @param checkSameToken 是否校验Same-Token(部分rpc插件有效) + * @return 对象自身 + */ + public SaTokenConfig setCheckSameToken(Boolean checkSameToken) { + this.checkSameToken = checkSameToken; + return this; + } + + /** + * @return 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) + */ + public SaReplacedLoginExitMode getReplacedLoginExitMode() { + return replacedLoginExitMode; + } + + /** + * @param replacedLoginExitMode 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) + * @return 对象自身 + */ + public SaTokenConfig setReplacedLoginExitMode(SaReplacedLoginExitMode replacedLoginExitMode) { + this.replacedLoginExitMode = replacedLoginExitMode; + return this; + } + + /** + * 获取 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端 ALL_DEVICE_TYPE=所有设备类型端) + * + * @return / */ - public long getIdTokenTimeout() { - return idTokenTimeout; + public SaReplacedRange getReplacedRange() { + return this.replacedRange; } /** - * @param idTokenTimeout Id-Token的有效期 (单位: 秒) + * 设置 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端 ALL_DEVICE_TYPE=所有设备类型端) + * + * @param replacedRange / * @return 对象自身 */ - public SaTokenConfig setIdTokenTimeout(long idTokenTimeout) { - this.idTokenTimeout = idTokenTimeout; + public SaTokenConfig setReplacedRange(SaReplacedRange replacedRange) { + this.replacedRange = replacedRange; return this; } - + + /** + * 获取 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + * + * @return / + */ + public SaLogoutMode getOverflowLogoutMode() { + return this.overflowLogoutMode; + } + /** - * @return SSO单点登录配置对象 + * 设置 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + * + * @param overflowLogoutMode / + * @return 对象自身 */ - public SaSsoConfig getSso() { - return sso; + public SaTokenConfig setOverflowLogoutMode(SaLogoutMode overflowLogoutMode) { + this.overflowLogoutMode = overflowLogoutMode; + return this; } - + /** - * @param sso SSO单点登录配置对象 + * 获取 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)
(此参数只在调用 StpUtil.logout() 时有效) + * + * @return / */ - public void setSso(SaSsoConfig sso) { - this.sso = sso; + public SaLogoutRange getLogoutRange() { + return this.logoutRange; } - + + /** + * 设置 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)
(此参数只在调用 StpUtil.logout() 时有效) + * + * @param logoutRange / + * @return 对象自身 + */ + public SaTokenConfig setLogoutRange(SaLogoutRange logoutRange) { + this.logoutRange = logoutRange; + return this; + } + + /** + * 获取 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API)
(此参数只在调用 StpUtil.[logoutkickoutreplaced]ByTokenValue("token") 时有效) + * + * @return isLogoutKeepFreezeOps / + */ + public Boolean getIsLogoutKeepFreezeOps() { + return this.isLogoutKeepFreezeOps; + } + + /** + * 设置 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API)
(此参数只在调用 StpUtil.[logoutkickoutreplaced]ByTokenValue("token") 时有效) + * + * @param isLogoutKeepFreezeOps / + * @return 对象自身 + */ + public SaTokenConfig setIsLogoutKeepFreezeOps(Boolean isLogoutKeepFreezeOps) { + this.isLogoutKeepFreezeOps = isLogoutKeepFreezeOps; + return this; + } + + /** + * 获取 在注销 token 后,是否保留其对应的 Token-Session + * + * @return isLogoutKeepTokenSession / + */ + public Boolean getIsLogoutKeepTokenSession() { + return this.isLogoutKeepTokenSession; + } + + /** + * 设置 在注销 token 后,是否保留其对应的 Token-Session + * + * @param isLogoutKeepTokenSession / + * @return 对象自身 + */ + public SaTokenConfig setIsLogoutKeepTokenSession(Boolean isLogoutKeepTokenSession) { + this.isLogoutKeepTokenSession = isLogoutKeepTokenSession; + return this; + } + + /** + * 获取 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) + * + * @return / + */ + public Boolean getRightNowCreateTokenSession() { + return this.rightNowCreateTokenSession; + } + + /** + * 设置 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) + * + * @param rightNowCreateTokenSession / + * @return 对象自身 + */ + public SaTokenConfig setRightNowCreateTokenSession(Boolean rightNowCreateTokenSession) { + this.rightNowCreateTokenSession = rightNowCreateTokenSession; + return this; + } + + /** + * @return Cookie 全局配置对象 + */ + public SaCookieConfig getCookie() { + return cookie; + } + + /** + * @param cookie Cookie 全局配置对象 + * @return 对象自身 + */ + public SaTokenConfig setCookie(SaCookieConfig cookie) { + this.cookie = cookie; + return this; + } + + @Override public String toString() { - return "SaTokenConfig [tokenName=" + tokenName + ", timeout=" + timeout + ", activityTimeout=" + activityTimeout - + ", isConcurrent=" + isConcurrent + ", isShare=" + isShare + ", isReadBody=" + isReadBody - + ", isReadHead=" + isReadHead + ", isReadCookie=" + isReadCookie + ", tokenStyle=" + tokenStyle - + ", dataRefreshPeriod=" + dataRefreshPeriod + ", tokenSessionCheckLogin=" + tokenSessionCheckLogin - + ", autoRenew=" + autoRenew + ", cookieDomain=" + cookieDomain + ", tokenPrefix=" + tokenPrefix - + ", isPrint=" + isPrint + ", isLog=" + isLog + ", jwtSecretKey=" + jwtSecretKey + ", idTokenTimeout=" - + idTokenTimeout + ", sso=" + sso + "]"; + return "SaTokenConfig [" + + "tokenName=" + tokenName + + ", timeout=" + timeout + + ", activeTimeout=" + activeTimeout + + ", dynamicActiveTimeout=" + dynamicActiveTimeout + + ", isConcurrent=" + isConcurrent + + ", isShare=" + isShare + + ", replacedRange=" + replacedRange + + ", replacedLoginExitMode=" + replacedLoginExitMode + + ", maxLoginCount=" + maxLoginCount + + ", overflowLogoutMode=" + overflowLogoutMode + + ", maxTryTimes=" + maxTryTimes + + ", isReadBody=" + isReadBody + + ", isReadHeader=" + isReadHeader + + ", isReadCookie=" + isReadCookie + + ", isLastingCookie=" + isLastingCookie + + ", isWriteHeader=" + isWriteHeader + + ", logoutRange=" + logoutRange + + ", isLogoutKeepFreezeOps=" + isLogoutKeepFreezeOps + + ", isLogoutKeepTokenSession=" + isLogoutKeepTokenSession + + ", rightNowCreateTokenSession=" + rightNowCreateTokenSession + + ", tokenStyle=" + tokenStyle + + ", dataRefreshPeriod=" + dataRefreshPeriod + + ", tokenSessionCheckLogin=" + tokenSessionCheckLogin + + ", autoRenew=" + autoRenew + + ", tokenPrefix=" + tokenPrefix + + ", cookieAutoFillPrefix=" + cookieAutoFillPrefix + + ", isPrint=" + isPrint + + ", isLog=" + isLog + + ", logLevel=" + logLevel + + ", logLevelInt=" + logLevelInt + + ", isColorLog=" + isColorLog + + ", jwtSecretKey=" + jwtSecretKey + + ", httpBasic=" + httpBasic + + ", httpDigest=" + httpDigest + + ", currDomain=" + currDomain + + ", sameTokenTimeout=" + sameTokenTimeout + + ", checkSameToken=" + checkSameToken + + ", cookie=" + cookie + + "]"; + } + + + + // ------------------- 过期方法 ------------------- + + /** + *

请更改为 getActiveTimeout()

+ * @return token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) + */ + @Deprecated + public long getActivityTimeout() { +// System.err.println("配置项已过期,请更换:sa-token.activity-timeout -> sa-token.active-timeout"); + return activeTimeout; } - - - /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 setIsConcurrent() ,使用方式保持不变

- * @param allowConcurrentLogin see note - * @return see note + *

请更改为 setActiveTimeout()

+ * @param activityTimeout token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) + * @return 对象自身 */ @Deprecated - public SaTokenConfig setAllowConcurrentLogin(Boolean allowConcurrentLogin) { - this.isConcurrent = allowConcurrentLogin; + public SaTokenConfig setActivityTimeout(long activityTimeout) { + System.err.println("配置项已过期,请更换:sa-token.activity-timeout -> sa-token.active-timeout"); + this.activeTimeout = activityTimeout; return this; } /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 setIsConcurrent() ,使用方式保持不变

- * @param isV see note - * @return see note + *

请更改为 getHttpBasic()

+ * @return Http Basic 认证的默认账号和密码 */ - public SaTokenConfig setIsV(Boolean isV) { - this.isPrint = isV; - return this; + @Deprecated + public String getBasic() { + return httpBasic; } + /** + *

请更改为 setHttpBasic()

+ * @param basic Http Basic 认证的默认账号和密码 + * @return 对象自身 + */ + @Deprecated + public SaTokenConfig setBasic(String basic) { + this.httpBasic = basic; + return this; + } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfigFactory.java b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfigFactory.java index 1c7f195a2..a82e28bb5 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfigFactory.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfigFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.config; import java.io.IOException; @@ -7,31 +22,48 @@ import java.util.Map; import java.util.Properties; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.util.SaFoxUtil; + /** * Sa-Token配置文件的构建工厂类 - *

- * 只有在非IOC环境下才会用到此类 - * - * @author kong * + *

用于手动读取配置文件初始化 SaTokenConfig 对象,只有在非IOC环境下你才会用到此类

+ * + * @author click33 + * @since 1.10.0 */ public class SaTokenConfigFactory { + private SaTokenConfigFactory() { + } + /** * 配置文件地址 */ public static String configPath = "sa-token.properties"; /** - * 根据configPath路径获取配置信息 + * 根据 configPath 路径获取配置信息 * * @return 一个SaTokenConfig对象 */ public static SaTokenConfig createConfig() { - Map map = readPropToMap(configPath); - if (map == null) { + return createConfig(configPath); + } + + /** + * 根据指定路径路径获取配置信息 + * + * @param path 配置文件路径 + * @return 一个 SaTokenConfig 对象 + */ + public static SaTokenConfig createConfig(String path) { + Map map = readPropToMap(path); + // if (map == null) { // throw new RuntimeException("找不到配置文件:" + configPath, null); - } + // } return (SaTokenConfig) initPropByMap(map, new SaTokenConfig()); } @@ -42,7 +74,7 @@ public static SaTokenConfig createConfig() { * @return 一个Map */ private static Map readPropToMap(String propertiesPath) { - Map map = new HashMap(16); + Map map = new HashMap<>(16); try { InputStream is = SaTokenConfigFactory.class.getClassLoader().getResourceAsStream(propertiesPath); if (is == null) { @@ -54,7 +86,7 @@ private static Map readPropToMap(String propertiesPath) { map.put(key, prop.getProperty(key)); } } catch (IOException e) { - throw new RuntimeException("配置文件(" + propertiesPath + ")加载失败", e); + throw new SaTokenException("配置文件(" + propertiesPath + ")加载失败", e).setCode(SaErrorCode.CODE_10021); } return map; } @@ -69,11 +101,11 @@ private static Map readPropToMap(String propertiesPath) { private static Object initPropByMap(Map map, Object obj) { if (map == null) { - map = new HashMap(16); + map = new HashMap<>(16); } // 1、取出类型 - Class cs = null; + Class cs; if (obj instanceof Class) { // 如果是一个类型,则将obj=null,以便完成静态属性反射赋值 cs = (Class) obj; @@ -91,46 +123,14 @@ private static Object initPropByMap(Map map, Object obj) { continue; } try { - Object valueConvert = getObjectByClass(value, field.getType()); + Object valueConvert = SaFoxUtil.getValueByType(value, field.getType()); field.setAccessible(true); field.set(obj, valueConvert); } catch (IllegalArgumentException | IllegalAccessException e) { - throw new RuntimeException("属性赋值出错:" + field.getName(), e); + throw new SaTokenException("属性赋值出错:" + field.getName(), e).setCode(SaErrorCode.CODE_10022); } } return obj; } - /** - * 工具方法: 将字符串转化为指定数据类型 - * - * @param str 值 - * @param cs 要转换的类型 - * @return 转化好的结果 - */ - @SuppressWarnings("unchecked") - private static T getObjectByClass(String str, Class cs) { - Object value; - if (str == null) { - value = null; - } else if (cs.equals(String.class)) { - value = str; - } else if (cs.equals(int.class) || cs.equals(Integer.class)) { - value = Integer.valueOf(str); - } else if (cs.equals(long.class) || cs.equals(Long.class)) { - value = Long.valueOf(str); - } else if (cs.equals(short.class) || cs.equals(Short.class)) { - value = Short.valueOf(str); - } else if (cs.equals(float.class) || cs.equals(Float.class)) { - value = Float.valueOf(str); - } else if (cs.equals(double.class) || cs.equals(Double.class)) { - value = Double.valueOf(str); - } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) { - value = Boolean.valueOf(str); - } else { - throw new RuntimeException("未能将值:" + str + ",转换类型为:" + cs, null); - } - return (T) value; - } - } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaHolder.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaHolder.java index 10d537728..c6a2e2b10 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaHolder.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaHolder.java @@ -1,42 +1,82 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context; import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.SaApplication; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; /** - * Sa-Token 上下文持有类 - * @author kong + * Sa-Token 上下文持有类,你可以通过此类快速获取当前环境下的 SaRequest、SaResponse、SaStorage、SaApplication 对象。 * + * @author click33 + * @since 1.18.0 */ public class SaHolder { + + /** + * 获取当前请求的 SaTokenContext 上下文对象 + * @see SaTokenContext + * + * @return / + */ + public static SaTokenContext getContext() { + return SaManager.getSaTokenContext(); + } /** - * 获取当前请求的 [Request] 对象 + * 获取当前请求的 Request 包装对象 + * @see SaRequest * - * @return see note + * @return / */ public static SaRequest getRequest() { return SaManager.getSaTokenContext().getRequest(); } /** - * 获取当前请求的 [Response] 对象 + * 获取当前请求的 Response 包装对象 + * @see SaResponse * - * @return see note + * @return / */ public static SaResponse getResponse() { return SaManager.getSaTokenContext().getResponse(); } /** - * 获取当前请求的 [存储器] 对象 - * - * @return see note + * 获取当前请求的 Storage 包装对象 + * @see SaStorage + * + * @return / */ public static SaStorage getStorage() { return SaManager.getSaTokenContext().getStorage(); } + /** + * 获取全局 SaApplication 对象 + * @see SaApplication + * + * @return / + */ + public static SaApplication getApplication() { + return SaApplication.defaultInstance; + } + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContext.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContext.java index 2d4ec90b1..3a7a2b059 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContext.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContext.java @@ -1,44 +1,89 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context; import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; /** * Sa-Token 上下文处理器 - * @author kong * + *

上下文处理器封装了当前应用环境的底层操作,是 Sa-Token 对接不同 web 框架的关键,详细可参考在线文档 “自定义 SaTokenContext 指南”章节

+ * + * @author click33 + * @since 1.16.0 */ public interface SaTokenContext { /** - * 获取当前请求的 [Request] 对象 - * - * @return see note + * 初始化上下文 + * + * @param req / + * @param res / + * @param stg / + */ + void setContext(SaRequest req, SaResponse res, SaStorage stg); + + /** + * 清除化上下文 + */ + void clearContext(); + + /** + * 判断当前上下文是否可用 + * + * @return / + */ + boolean isValid(); + + /** + * 获取 Box 对象 */ - public SaRequest getRequest(); + SaTokenContextModelBox getModelBox(); /** - * 获取当前请求的 [Response] 对象 + * 获取当前上下文的 Request 包装对象 + * @see SaRequest * - * @return see note + * @return / */ - public SaResponse getResponse(); + default SaRequest getRequest() { + return getModelBox().getRequest(); + } /** - * 获取当前请求的 [存储器] 对象 + * 获取当前上下文的 Response 包装对象 + * @see SaResponse * - * @return see note + * @return / */ - public SaStorage getStorage(); + default SaResponse getResponse(){ + return getModelBox().getResponse(); + } /** - * 校验指定路由匹配符是否可以匹配成功指定路径 + * 获取当前上下文的 Storage 包装对象 + * @see SaStorage * - * @param pattern 路由匹配符 - * @param path 需要匹配的路径 - * @return see note + * @return / */ - public boolean matchPath(String pattern, String path); + default SaStorage getStorage(){ + return getModelBox().getStorage(); + } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextDefaultImpl.java index 45aab5040..9cc57705a 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextDefaultImpl.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextDefaultImpl.java @@ -1,54 +1,83 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context; import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaResponse; -import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaTokenContextException; /** - * Sa-Token 上下文处理器 [默认实现类] - * @author kong - * + * Sa-Token 上下文处理器 [ 默认实现类 ] + * + *

+ * 一般情况下框架会为你自动注入合适的上下文处理器,如果代码断点走到了此默认实现类, + * 说明你引入的依赖有问题或者错误的调用了 Sa-Token 的API, 请在 [ 在线开发文档 → 附录 → 常见问题排查 ] 中按照提示进行排查 + *

+ * + * @author click33 + * @since 1.16.0 */ public class SaTokenContextDefaultImpl implements SaTokenContext { - + /** - * 默认的错误提示语 + * 默认的上下文处理器对象 */ - public static final String ERROR_MESSAGE = "未初始化任何有效上下文处理器"; - + public static SaTokenContextDefaultImpl defaultContext = new SaTokenContextDefaultImpl(); + /** - * 获取当前请求的 [Request] 对象 + * 错误提示语 */ + public static final String ERROR_MESSAGE = "未能获取有效的上下文处理器"; + @Override - public SaRequest getRequest() { - throw new SaTokenException(ERROR_MESSAGE); + public void setContext(SaRequest req, SaResponse res, SaStorage stg) { + throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } - - /** - * 获取当前请求的 [Response] 对象 - */ + @Override - public SaResponse getResponse() { - throw new SaTokenException(ERROR_MESSAGE); + public void clearContext() { + throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } - /** - * 获取当前请求的 [存储器] 对象 - */ @Override - public SaStorage getStorage() { - throw new SaTokenException(ERROR_MESSAGE); + public boolean isValid() { + throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } - - /** - * 校验指定路由匹配符是否可以匹配成功指定路径 - */ + @Override - public boolean matchPath(String pattern, String path) { - throw new SaTokenException(ERROR_MESSAGE); + public SaTokenContextModelBox getModelBox() { + throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } - + @Override + public SaRequest getRequest() { + throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); + } + + @Override + public SaResponse getResponse() { + throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); + } + + @Override + public SaStorage getStorage() { + throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); + } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForReadOnly.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForReadOnly.java new file mode 100644 index 000000000..895ab8c57 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForReadOnly.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; + +/** + * Sa-Token 上下文处理器次级实现:只读上下文 + * + * @author click33 + * @since 1.42.0 + */ +public interface SaTokenContextForReadOnly extends SaTokenContext { + + @Override + default void setContext(SaRequest req, SaResponse res, SaStorage stg) { + + } + + @Override + default void clearContext() { + + } + + @Override + default SaTokenContextModelBox getModelBox() { + return null; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocal.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocal.java index 02dd2c681..6f862e8c6 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocal.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocal.java @@ -1,34 +1,58 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; /** - * Sa-Token 上下文处理器 [ThreadLocal版本] - * @author kong + * Sa-Token 上下文处理器 [ ThreadLocal 版本 ] + * + *

+ * 使用 [ ThreadLocal 版本 ] 上下文处理器需要在全局过滤器或者拦截器内率先调用 + * SaTokenContextForThreadLocalStaff.setBox(req, res, sto) 初始化上下文 + *

+ * + *

一般情况下你不需要直接操作此类,因为框架的 starter 集成包里已经封装了完整的上下文操作

* + * @author click33 + * @since 1.16.0 */ public class SaTokenContextForThreadLocal implements SaTokenContext { @Override - public SaRequest getRequest() { - return SaTokenContextForThreadLocalStorage.getRequest(); + public void setContext(SaRequest req, SaResponse res, SaStorage stg) { + SaTokenContextForThreadLocalStaff.setModelBox(req, res, stg); } @Override - public SaResponse getResponse() { - return SaTokenContextForThreadLocalStorage.getResponse(); + public void clearContext() { + SaTokenContextForThreadLocalStaff.clearModelBox(); } @Override - public SaStorage getStorage() { - return SaTokenContextForThreadLocalStorage.getStorage(); + public boolean isValid() { + return SaTokenContextForThreadLocalStaff.getModelBoxOrNull() != null; } @Override - public boolean matchPath(String pattern, String path) { - return false; + public SaTokenContextModelBox getModelBox() { + return SaTokenContextForThreadLocalStaff.getModelBox(); } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStaff.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStaff.java new file mode 100644 index 000000000..bb5962cdb --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStaff.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaTokenContextException; + +/** + * Sa-Token 上下文处理器 [ThreadLocal 版本] ---- 对象存储器 + * + *

一般情况下你不需要直接操作此类,因为框架的 starter 集成包里已经封装了完整的上下文操作

+ * + * @author click33 + * @since 1.16.0 + */ +public class SaTokenContextForThreadLocalStaff { + + /** + * 基于 ThreadLocal 的 [ Box 存储器 ] + */ + public static ThreadLocal modelBoxThreadLocal = new ThreadLocal<>(); + + /** + * 初始化当前线程的 [ Box 存储器 ] + * @param request {@link SaRequest} + * @param response {@link SaResponse} + * @param storage {@link SaStorage} + */ + public static void setModelBox(SaRequest request, SaResponse response, SaStorage storage) { + SaTokenContextModelBox bok = new SaTokenContextModelBox(request, response, storage); + modelBoxThreadLocal.set(bok); + } + + /** + * 清除当前线程的 [ Box 存储器 ] + */ + public static void clearModelBox() { + modelBoxThreadLocal.remove(); + } + + /** + * 获取当前线程的 [ Box 存储器 ] + * @return / + */ + public static SaTokenContextModelBox getModelBoxOrNull() { + return modelBoxThreadLocal.get(); + } + + /** + * 获取当前线程的 [ Box 存储器 ], 如果为空则抛出异常 + * @return / + */ + public static SaTokenContextModelBox getModelBox() { + SaTokenContextModelBox box = modelBoxThreadLocal.get(); + if(box == null) { + throw new SaTokenContextException("SaTokenContext 上下文尚未初始化").setCode(SaErrorCode.CODE_10002); + } + return box; + } + + /** + * 在当前线程的 SaRequest 包装对象 + * + * @return / + */ + public static SaRequest getRequest() { + return getModelBox().getRequest(); + } + + /** + * 在当前线程的 SaResponse 包装对象 + * + * @return / + */ + public static SaResponse getResponse() { + return getModelBox().getResponse(); + } + + /** + * 在当前线程的 SaStorage 存储器包装对象 + * + * @return / + */ + public static SaStorage getStorage() { + return getModelBox().getStorage(); + } + + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStorage.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStorage.java deleted file mode 100644 index 4c4813bc9..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStorage.java +++ /dev/null @@ -1,139 +0,0 @@ -package cn.dev33.satoken.context; - -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.context.model.SaResponse; -import cn.dev33.satoken.context.model.SaStorage; -import cn.dev33.satoken.exception.SaTokenException; - -/** - * Sa-Token 上下文处理器 [ThreadLocal版本] ---- 对象存储器 - * @author kong - * - */ -public class SaTokenContextForThreadLocalStorage { - - /** - * 基于 ThreadLocal 的 [Box存储器] - */ - public static ThreadLocal boxThreadLocal = new InheritableThreadLocal(); - - /** - * 初始化 [Box存储器] - * @param request {@link SaRequest} - * @param response {@link SaResponse} - * @param storage {@link SaStorage} - */ - public static void setBox(SaRequest request, SaResponse response, SaStorage storage) { - Box bok = new Box(request, response, storage); - boxThreadLocal.set(bok); - }; - - /** - * 清除 [Box存储器] - */ - public static void clearBox() { - boxThreadLocal.remove(); - }; - - /** - * 获取 [Box存储器] - * @return see note - */ - public static Box getBox() { - return boxThreadLocal.get(); - }; - - /** - * 获取 [Box存储器], 如果为空则抛出异常 - * @return see note - */ - public static Box getBoxNotNull() { - Box box = boxThreadLocal.get(); - if(box == null) { - throw new SaTokenException("未成功初始化上下文"); - } - return box; - }; - - /** - * 在 [Box存储器] 获取 [Request] 对象 - * - * @return see note - */ - public static SaRequest getRequest() { - return getBoxNotNull().getRequest(); - } - - /** - * 在 [Box存储器] 获取 [Response] 对象 - * - * @return see note - */ - public static SaResponse getResponse() { - return getBoxNotNull().getResponse(); - } - - /** - * 在 [Box存储器] 获取 [存储器] 对象 - * - * @return see note - */ - public static SaStorage getStorage() { - return getBoxNotNull().getStorage(); - } - - - /** - * 临时内部类,用于存储[request、response、storage]三个对象 - * @author kong - */ - /** - * @author kong - * - */ - public static class Box { - - public SaRequest request; - - public SaResponse response; - - public SaStorage storage; - - public Box(SaRequest request, SaResponse response, SaStorage storage){ - this.request = request; - this.response = response; - this.storage = storage; - } - - public SaRequest getRequest() { - return request; - } - - public void setRequest(SaRequest request) { - this.request = request; - } - - public SaResponse getResponse() { - return response; - } - - public void setResponse(SaResponse response) { - this.response = response; - } - - public SaStorage getStorage() { - return storage; - } - - public void setStorage(SaStorage storage) { - this.storage = storage; - } - - @Override - public String toString() { - return "Box [request=" + request + ", response=" + response + ", storage=" + storage + "]"; - } - - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaRequestForMock.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaRequestForMock.java new file mode 100644 index 000000000..0e8d55bcc --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaRequestForMock.java @@ -0,0 +1,151 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.mock; + +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.context.model.SaRequest; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 对 SaRequest 包装类的实现(Mock 版) + * + * @author click33 + * @since 1.42.0 + */ +public class SaRequestForMock implements SaRequest { + + public Map parameterMap = new LinkedHashMap<>(); + public Map headerMap = new LinkedHashMap<>(); + public Map cookieMap = new LinkedHashMap<>(); + public String requestPath; + public String url; + public String method; + public String host; + public String forwardTo; + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return null; + } + + /** + * 在 [请求体] 里获取一个值 + */ + @Override + public String getParam(String name) { + return parameterMap.get(name); + } + + /** + * 获取 [请求体] 里提交的所有参数名称 + * @return 参数名称列表 + */ + @Override + public Collection getParamNames(){ + return parameterMap.keySet(); + } + + /** + * 获取 [请求体] 里提交的所有参数 + * @return 参数列表 + */ + @Override + public Map getParamMap(){ + return parameterMap; + } + + /** + * 在 [请求头] 里获取一个值 + */ + @Override + public String getHeader(String name) { + return headerMap.get(name); + } + + /** + * 在 [Cookie作用域] 里获取一个值 + */ + @Override + public String getCookieValue(String name) { + return getCookieLastValue(name); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name){ + return cookieMap.get(name); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name){ + return cookieMap.get(name); + } + + /** + * 返回当前请求path (不包括上下文名称) + */ + @Override + public String getRequestPath() { + return ApplicationInfo.cutPathPrefix(requestPath); + } + + /** + * 返回当前请求的url,例:http://xxx.com/test + * @return see note + */ + public String getUrl() { + return url; + } + + /** + * 返回当前请求的类型 + */ + @Override + public String getMethod() { + return method; + } + + /** + * 查询请求 host + */ + @Override + public String getHost() { + return host; + } + + /** + * 转发请求 + */ + @Override + public Object forward(String path) { + this.forwardTo = path; + return null; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaResponseForMock.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaResponseForMock.java new file mode 100644 index 000000000..20f394389 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaResponseForMock.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.mock; + +import cn.dev33.satoken.context.model.SaResponse; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 对 SaResponse 包装类的实现(Mock 版) + * + * @author click33 + * @since 1.42.0 + */ +public class SaResponseForMock implements SaResponse { + + public int status; + public Map headerMap = new LinkedHashMap<>(); + public String redirectTo; + + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return null; + } + + /** + * 设置响应状态码 + */ + @Override + public SaResponse setStatus(int sc) { + this.status = sc; + return this; + } + + /** + * 在响应头里写入一个值 + */ + @Override + public SaResponse setHeader(String name, String value) { + headerMap.put(name, value); + return this; + } + + /** + * 在响应头里添加一个值 + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + public SaResponse addHeader(String name, String value) { + headerMap.put(name, value); + return this; + } + + /** + * 重定向 + */ + @Override + public Object redirect(String url) { + this.redirectTo = url; + return null; + } + + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaStorageForMock.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaStorageForMock.java new file mode 100644 index 000000000..e793a82c0 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaStorageForMock.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.mock; + +import cn.dev33.satoken.context.model.SaStorage; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 对 SaStorage 包装类的实现(Mock 版) + * + * @author click33 + * @since 1.42.0 + */ +public class SaStorageForMock implements SaStorage { + + public Map dataMap = new LinkedHashMap<>(); + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return null; + } + + /** + * 在 [Request作用域] 里写入一个值 + */ + @Override + public SaStorageForMock set(String key, Object value) { + dataMap.put(key, value); + return this; + } + + /** + * 在 [Request作用域] 里获取一个值 + */ + @Override + public Object get(String key) { + return dataMap.get(key); + } + + /** + * 在 [Request作用域] 里删除一个值 + */ + @Override + public SaStorageForMock delete(String key) { + dataMap.remove(key); + return this; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaTokenContextMockUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaTokenContextMockUtil.java new file mode 100644 index 000000000..0fedc0efb --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaTokenContextMockUtil.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.mock; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaRetGenericFunction; + +/** + * Sa-Token Mock 上下文 操作工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenContextMockUtil { + + /** + * 写入 Mock 上下文 + */ + public static void setMockContext() { + SaRequestForMock request = new SaRequestForMock(); + SaResponseForMock response = new SaResponseForMock(); + SaStorageForMock storage = new SaStorageForMock(); + SaManager.getSaTokenContext().setContext(request, response, storage); + } + + /** + * 写入 Mock 上下文,并执行一段代码,执行完毕后清除上下文 + * + * @param fun / + */ + public static void setMockContext(SaFunction fun) { + try { + setMockContext(); + fun.run(); + } finally { + clearContext(); + } + } + + /** + * 写入 Mock 上下文,并执行一段代码,执行完毕后清除上下文 + * + * @param fun / + */ + public static T setMockContext(SaRetGenericFunction fun) { + try { + setMockContext(); + return fun.run(); + } finally { + clearContext(); + } + } + + /** + * 清除上下文 + */ + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaCookie.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaCookie.java new file mode 100644 index 000000000..6d16e8980 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaCookie.java @@ -0,0 +1,374 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.model; + +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Cookie Model,代表一个 Cookie 应该具有的所有参数 + * + * @author click33 + * @since 1.16.0 + */ +public class SaCookie { + + /** + * 写入响应头时使用的key + */ + public static final String HEADER_NAME = "Set-Cookie"; + + /** + * 名称 + */ + private String name; + + /** + * 值 + */ + private String value; + + /** + * 有效时长 (单位:秒),-1 代表为临时Cookie 浏览器关闭后自动删除 + */ + private int maxAge = -1; + + /** + * 域 + */ + private String domain; + + /** + * 路径 + */ + private String path; + + /** + * 是否只在 https 协议下有效 + */ + private Boolean secure = false; + + /** + * 是否禁止 js 操作 Cookie + */ + private Boolean httpOnly = false; + + /** + * 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) + */ + private String sameSite; + + // Cookie 属性参考文章:https://blog.csdn.net/fengbin2005/article/details/136544226 + + /** + * 额外扩展属性 + */ + private Map extraAttrs = new LinkedHashMap<>(); + + + /** + * 构造一个 + */ + public SaCookie() { + } + + /** + * 构造一个 + * @param name 名字 + * @param value 值 + */ + public SaCookie(String name, String value) { + this.name = name; + this.value = value; + } + + + + /** + * @return 名称 + */ + public String getName() { + return name; + } + + /** + * @param name 名称 + * @return 对象自身 + */ + public SaCookie setName(String name) { + this.name = name; + return this; + } + + /** + * @return 值 + */ + public String getValue() { + return value; + } + + /** + * @param value 值 + * @return 对象自身 + */ + public SaCookie setValue(String value) { + this.value = value; + return this; + } + + /** + * @return 有效时长 (单位:秒),-1 代表为临时Cookie 浏览器关闭后自动删除 + */ + public int getMaxAge() { + return maxAge; + } + + /** + * @param maxAge 有效时长 (单位:秒),-1 代表为临时Cookie 浏览器关闭后自动删除 + * @return 对象自身 + */ + public SaCookie setMaxAge(int maxAge) { + this.maxAge = maxAge; + return this; + } + + /** + * @return 域 + */ + public String getDomain() { + return domain; + } + + /** + * @param domain 域 + * @return 对象自身 + */ + public SaCookie setDomain(String domain) { + this.domain = domain; + return this; + } + + /** + * @return 路径 + */ + public String getPath() { + return path; + } + + /** + * @param path 路径 + * @return 对象自身 + */ + public SaCookie setPath(String path) { + this.path = path; + return this; + } + + /** + * @return 是否只在 https 协议下有效 + */ + public Boolean getSecure() { + return secure; + } + + /** + * @param secure 是否只在 https 协议下有效 + * @return 对象自身 + */ + public SaCookie setSecure(Boolean secure) { + this.secure = secure; + return this; + } + + /** + * @return 是否禁止 js 操作 Cookie + */ + public Boolean getHttpOnly() { + return httpOnly; + } + + /** + * @param httpOnly 是否禁止 js 操作 Cookie + * @return 对象自身 + */ + public SaCookie setHttpOnly(Boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + /** + * @return 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) + */ + public String getSameSite() { + return sameSite; + } + + /** + * @param sameSite 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) + * @return 对象自身 + */ + public SaCookie setSameSite(String sameSite) { + this.sameSite = sameSite; + return this; + } + + /** + * @return 获取额外扩展属性 + */ + public Map getExtraAttrs() { + return extraAttrs; + } + + /** + * 写入额外扩展属性 + * @param extraAttrs / + * @return 对象自身 + */ + public SaCookie setExtraAttrs(Map extraAttrs) { + this.extraAttrs = extraAttrs; + return this; + } + + /** + * 追加扩展属性 + * @param name / + * @param value / + * @return 对象自身 + */ + public SaCookie addExtraAttr(String name, String value) { + if (extraAttrs == null) { + extraAttrs = new LinkedHashMap<>(); + } + this.extraAttrs.put(name, value); + return this; + } + + /** + * 追加扩展属性 + * @param name / + * @return 对象自身 + */ + public SaCookie addExtraAttr(String name) { + return this.addExtraAttr(name, null); + } + + /** + * 移除指定扩展属性 + * @param name / + * @return 对象自身 + */ + public SaCookie removeExtraAttr(String name) { + if(extraAttrs != null) { + this.extraAttrs.remove(name); + } + return this; + } + + + // toString + @Override + public String toString() { + return "SaCookie [" + + "name=" + name + + ", value=" + value + + ", maxAge=" + maxAge + + ", domain=" + domain + + ", path=" + path + + ", secure=" + secure + + ", httpOnly=" + httpOnly + + ", sameSite=" + sameSite + + ", extraAttrs=" + extraAttrs + + "]"; + } + + /** + * 构建一下 + */ + public void builder() { + if(path == null) { + path = "/"; + } + } + + /** + * 转换为响应头 Set-Cookie 参数需要的值 + * @return / + */ + public String toHeaderValue() { + this.builder(); + + if(SaFoxUtil.isEmpty(name)) { + throw new SaTokenException("name不能为空").setCode(SaErrorCode.CODE_12002); + } + if(value != null && value.contains(";")) { + throw new SaTokenException("无效Value:" + value).setCode(SaErrorCode.CODE_12003); + } + + // example: + // Set-Cookie: name=value; Max-Age=100000; Expires=Tue, 05-Oct-2021 20:28:17 GMT; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=Lax + + StringBuilder sb = new StringBuilder(); + sb.append(name).append("=").append(value); + + if(maxAge >= 0) { + sb.append("; Max-Age=").append(maxAge); + String expires; + if(maxAge == 0) { + expires = Instant.EPOCH.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME); + } else { + expires = OffsetDateTime.now().plusSeconds(maxAge).format(DateTimeFormatter.RFC_1123_DATE_TIME); + } + sb.append("; Expires=").append(expires); + } + if(!SaFoxUtil.isEmpty(domain)) { + sb.append("; Domain=").append(domain); + } + if(!SaFoxUtil.isEmpty(path)) { + sb.append("; Path=").append(path); + } + if(secure) { + sb.append("; Secure"); + } + if(httpOnly) { + sb.append("; HttpOnly"); + } + if(!SaFoxUtil.isEmpty(sameSite)) { + sb.append("; SameSite=").append(sameSite); + } + + // 扩展属性 + if(extraAttrs != null) { + extraAttrs.forEach((k, v) -> { + if(SaFoxUtil.isEmpty(v)) { + sb.append("; ").append(k); + } else { + sb.append("; ").append(k).append("=").append(v); + } + }); + } + + return sb.toString(); + } + +} 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 9d3a8b7eb..d8f2264f2 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 @@ -1,35 +1,56 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.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.Collection; +import java.util.Map; + /** - * Request 包装类 - * @author kong + * Request 请求对象 包装类 * + * @author click33 + * @since 1.16.0 */ public interface SaRequest { /** - * 获取底层源对象 - * @return see note + * 获取底层被包装的源对象 + * @return / */ - public Object getSource(); + Object getSource(); /** - * 在 [请求体] 里获取一个值 + * 在 [ 请求体 ] 里获取一个参数值 * @param name 键 * @return 值 */ - public String getParam(String name); + String getParam(String name); /** - * 在 [请求体] 里获取一个值,值为空时返回默认值 + * 在 [ 请求体 ] 里获取一个参数值,值为空时返回默认值 * @param name 键 * @param defaultValue 值为空时的默认值 * @return 值 */ - public default String getParam(String name, String defaultValue) { + default String getParam(String name, String defaultValue) { String value = getParam(name); if(SaFoxUtil.isEmpty(value)) { return defaultValue; @@ -38,53 +59,64 @@ public default String getParam(String name, String defaultValue) { } /** - * 检测提供的参数是否为指定值 + * 在 [ 请求体 ] 里检测提供的参数是否为指定值 * @param name 键 * @param value 值 * @return 是否相等 */ - public default boolean isParam(String name, String value) { + default boolean isParam(String name, String value) { String paramValue = getParam(name); return SaFoxUtil.isNotEmpty(paramValue) && paramValue.equals(value); } /** - * 检测请求是否提供了指定参数 + * 在 [ 请求体 ] 里检测请求是否提供了指定参数 * @param name 参数名称 * @return 是否提供 */ - public default boolean hasParam(String name) { + default boolean hasParam(String name) { return SaFoxUtil.isNotEmpty(getParam(name)); } /** - * 在 [请求体] 里获取一个值 (此值必须存在,否则抛出异常 ) + * 在 [ 请求体 ] 里获取一个值 (此值必须存在,否则抛出异常 ) * @param name 键 * @return 参数值 */ - public default String getParamNotNull(String name) { + default String getParamNotNull(String name) { String paramValue = getParam(name); if(SaFoxUtil.isEmpty(paramValue)) { - throw new SaTokenException("缺少参数:" + name); + throw new SaTokenException("缺少参数:" + name).setCode(SaErrorCode.CODE_12001); } return paramValue; } - - + + /** + * 获取 [ 请求体 ] 里提交的所有参数名称 + * @return 参数名称列表 + */ + Collection getParamNames(); + + /** + * 获取 [ 请求体 ] 里提交的所有参数 + * @return 参数列表 + */ + Map getParamMap(); + /** - * 在 [请求头] 里获取一个值 + * 在 [ 请求头 ] 里获取一个值 * @param name 键 * @return 值 */ - public String getHeader(String name); + String getHeader(String name); /** - * 在 [请求头] 里获取一个值 + * 在 [ 请求头 ] 里获取一个值 * @param name 键 * @param defaultValue 值为空时的默认值 * @return 值 */ - public default String getHeader(String name, String defaultValue) { + default String getHeader(String name, String defaultValue) { String value = getHeader(name); if(SaFoxUtil.isEmpty(value)) { return defaultValue; @@ -93,54 +125,90 @@ public default String getHeader(String name, String defaultValue) { } /** - * 在 [Cookie作用域] 里获取一个值 + * 在 [ Cookie作用域 ] 里获取一个值 * @param name 键 * @return 值 */ - public String getCookieValue(String name); + String getCookieValue(String name); + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + * @param name 键 + * @return 值 + */ + String getCookieFirstValue(String name); + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + String getCookieLastValue(String name); /** * 返回当前请求path (不包括上下文名称) - * @return see note + * @return / */ - public String getRequestPath(); + String getRequestPath(); /** - * 返回当前请求path是否为指定值 + * 返回当前请求 path 是否为指定值 * @param path path - * @return see note + * @return / */ - public default boolean isPath(String path) { + default boolean isPath(String path) { return getRequestPath().equals(path); } /** - * 返回当前请求的url,不带query参数,例:http://xxx.com/ - * @return see note + * 返回当前请求的url,不带query参数,例:http://xxx.com/test + * @return / */ - public String getUrl(); + String getUrl(); /** * 返回当前请求的类型 - * @return see note + * @return / */ - public String getMethod(); - + String getMethod(); + + /** + * 返回当前请求 Method 是否为指定值 + * @param method method + * @return / + */ + default boolean isMethod(String method) { + return getMethod().equals(method); + } + /** - * 此请求是否为Ajax请求 - * @return see note + * 返回当前请求 Method 是否为指定值 + * @param method method + * @return / */ - public default boolean isAjax() { - return getHeader("X-Requested-With") != null; + default boolean isMethod(SaHttpMethod method) { + return getMethod().equals(method.name()); + } + + /** + * 查询请求 host + * @return / + */ + String getHost(); + + /** + * 判断此请求是否为 Ajax 异步请求 + * @return / + */ + default boolean isAjax() { + return "XMLHttpRequest".equalsIgnoreCase(getHeader("X-Requested-With")) || isParam("_ajax", "true"); } /** * 转发请求 - * @param url 转发地址 + * @param path 转发地址 * @return 任意值 */ - public default Object forward(String path) { - throw new SaTokenException("No implementation"); - } + Object forward(String path); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java index 66b882212..a9748b8a9 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java @@ -1,33 +1,83 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.model; /** - * Response 包装类 - * @author kong + * Response 响应对象 包装类 * + * @author click33 + * @since 1.16.0 */ public interface SaResponse { /** - * 获取底层源对象 - * @return see note + * 指定前端可以获取到哪些响应头时使用的参数名 */ - public Object getSource(); + String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + + /** + * 获取底层被包装的源对象 + * @return / + */ + Object getSource(); /** * 删除指定Cookie * @param name Cookie名称 */ - public void deleteCookie(String name); - + default void deleteCookie(String name) { + addCookie(name, null, null, null, 0); + } + + /** + * 删除指定Cookie + * @param name Cookie名称 + * @param path Cookie 路径 + * @param domain Cookie 作用域 + */ + default void deleteCookie(String name, String path, String domain) { + addCookie(name, null, path, domain, 0); + } + /** - * 写入指定Cookie + * 写入指定Cookie * @param name Cookie名称 * @param value Cookie值 * @param path Cookie路径 * @param domain Cookie的作用域 * @param timeout 过期时间 (秒) */ - public void addCookie(String name, String value, String path, String domain, int timeout); + default void addCookie(String name, String value, String path, String domain, int timeout) { + this.addCookie(new SaCookie(name, value).setPath(path).setDomain(domain).setMaxAge(timeout)); + } + + /** + * 写入指定Cookie + * @param cookie Cookie-Model + */ + default void addCookie(SaCookie cookie) { + this.addHeader(SaCookie.HEADER_NAME, cookie.toHeaderValue()); + } + + /** + * 设置响应状态码 + * @param sc 响应状态码 + * @return 对象自身 + */ + SaResponse setStatus(int sc); /** * 在响应头里写入一个值 @@ -35,14 +85,22 @@ public interface SaResponse { * @param value 值 * @return 对象自身 */ - public SaResponse setHeader(String name, String value); + SaResponse setHeader(String name, String value); + + /** + * 在响应头里添加一个值 + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + SaResponse addHeader(String name, String value); /** * 在响应头写入 [Server] 服务器名称 * @param value 服务器名称 * @return 对象自身 */ - public default SaResponse setServer(String value) { + default SaResponse setServer(String value) { return this.setHeader("Server", value); } @@ -51,6 +109,6 @@ public default SaResponse setServer(String value) { * @param url 重定向地址 * @return 任意值 */ - public Object redirect(String url); + Object redirect(String url); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java index 630d18f3c..70f1a6b33 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java @@ -1,37 +1,50 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.model; +import cn.dev33.satoken.application.SaSetValueInterface; + /** - * [存储器] 包装类 - *

在 Request作用域里: 存值、取值 - * @author kong + * Storage Model,请求作用域的读取值对象。 * + *

在一次请求范围内: 存值、取值。数据在请求结束后失效。 + * + * @author click33 + * @since 1.16.0 */ -public interface SaStorage { +public interface SaStorage extends SaSetValueInterface { /** - * 获取底层源对象 - * @return see note - */ - public Object getSource(); - - /** - * 在 [Request作用域] 里写入一个值 - * @param key 键 - * @param value 值 + * 获取底层被包装的源对象 + * @return / */ - public void set(String key, Object value); - - /** - * 在 [Request作用域] 里获取一个值 - * @param key 键 - * @return 值 - */ - public Object get(String key); + Object getSource(); - /** - * 在 [Request作用域] 里删除一个值 - * @param key 键 - */ - public void delete(String key); + // ---- 实现接口存取值方法 + + /** 取值 */ + @Override + Object get(String key); + + /** 写值 */ + @Override + SaStorage set(String key, Object value); + + /** 删值 */ + @Override + SaStorage delete(String key); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaTokenContextModelBox.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaTokenContextModelBox.java new file mode 100644 index 000000000..1011344a4 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaTokenContextModelBox.java @@ -0,0 +1,52 @@ +package cn.dev33.satoken.context.model; + +/** + * Box 盒子类,用于存储 [ SaRequest、SaResponse、SaStorage ] 三个包装对象 + * + * @author click33 + * @since 1.16.0 + */ +public class SaTokenContextModelBox { + + public SaRequest request; + + public SaResponse response; + + public SaStorage storage; + + public SaTokenContextModelBox(SaRequest request, SaResponse response, SaStorage storage) { + this.request = request; + this.response = response; + this.storage = storage; + } + + public SaRequest getRequest() { + return request; + } + + public void setRequest(SaRequest request) { + this.request = request; + } + + public SaResponse getResponse() { + return response; + } + + public void setResponse(SaResponse response) { + this.response = response; + } + + public SaStorage getStorage() { + return storage; + } + + public void setStorage(SaStorage storage) { + this.storage = storage; + } + + @Override + public String toString() { + return "Box [request=" + request + ", response=" + response + ", storage=" + storage + "]"; + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/package-info.java b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/package-info.java index 435ca3a70..7a5a950af 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/context/model/package-info.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/context/model/package-info.java @@ -1,4 +1,20 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ /** - * 因为不能确定最终运行的容器属于标准Servlet模型还是非Servlet模型,特封装此包下的包装类进行对接 + * 因为不能确定最终运行的 web 容器属于标准 Servlet 模型还是非 Servlet 模型,特封装此包下的包装类进行对接。 + * 调用路径为:Sa-Token 功能函数 -> SaRequest 封装接口 -> SaRequest 具体实现类。 */ package cn.dev33.satoken.context.model; \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java b/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java index 733d26bfb..165fa6e51 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java @@ -1,165 +1,187 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; -import java.util.List; - import cn.dev33.satoken.session.SaSession; +import java.util.List; + /** - * Sa-Token持久层接口 - * @author kong + * Sa-Token 持久层接口 + * + *

+ * 此接口的不同实现类可将数据存储至不同位置,如:内存Map、Redis 等等。 + * 如果你要自定义数据存储策略,也需通过实现此接口来完成。 + *

+ * + * @author click33 + * @since 1.10.0 */ public interface SaTokenDao { - /** 常量,表示一个key永不过期 (在一个key被标注为永远不过期时返回此值) */ - public static final long NEVER_EXPIRE = -1; + /** 常量,表示一个 key 永不过期 (在一个 key 被标注为永远不过期时返回此值) */ + long NEVER_EXPIRE = -1; - /** 常量,表示系统中不存在这个缓存 (在对不存在的key获取剩余存活时间时返回此值) */ - public static final long NOT_VALUE_EXPIRE = -2; + /** 常量,表示系统中不存在这个缓存(在对不存在的 key 获取剩余存活时间时返回此值) */ + long NOT_VALUE_EXPIRE = -2; // --------------------- 字符串读写 --------------------- /** - * 获取Value,如无返空 + * 获取 value,如无返空 + * * @param key 键名称 * @return value */ - public String get(String key); + String get(String key); /** - * 写入Value,并设定存活时间 (单位: 秒) + * 写入 value,并设定存活时间(单位: 秒) + * * @param key 键名称 * @param value 值 - * @param timeout 过期时间(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储) + * @param timeout 数据有效期(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) */ - public void set(String key, String value, long timeout); + void set(String key, String value, long timeout); /** - * 更新Value (过期时间不变) + * 更新 value (过期时间不变) * @param key 键名称 * @param value 值 */ - public void update(String key, String value); + void update(String key, String value); /** - * 删除Value + * 删除 value * @param key 键名称 */ - public void delete(String key); + void delete(String key); /** - * 获取Value的剩余存活时间 (单位: 秒) - * @param key 指定key - * @return 这个key的剩余存活时间 + * 获取 value 的剩余存活时间(单位: 秒) + * @param key 指定 key + * @return 这个 key 的剩余存活时间 */ - public long getTimeout(String key); + long getTimeout(String key); /** - * 修改Value的剩余存活时间 (单位: 秒) - * @param key 指定key - * @param timeout 过期时间 + * 修改 value 的剩余存活时间(单位: 秒) + * @param key 指定 key + * @param timeout 过期时间(单位: 秒) */ - public void updateTimeout(String key, long timeout); + void updateTimeout(String key, long timeout); // --------------------- 对象读写 --------------------- /** - * 获取Object,如无返空 - * @param key 键名称 + * 获取 Object,如无返空 + * + * @param key 键名称 * @return object */ - public Object getObject(String key); + Object getObject(String key); /** - * 写入Object,并设定存活时间 (单位: 秒) - * @param key 键名称 - * @param object 值 - * @param timeout 存活时间 (值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储) + * 获取 Object (指定反序列化类型),如无返空 + * + * @param key 键名称 + * @return object + */ + T getObject(String key, Class classType); + + /** + * 写入 Object,并设定存活时间 (单位: 秒) + * + * @param key 键名称 + * @param object 值 + * @param timeout 存活时间(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) */ - public void setObject(String key, Object object, long timeout); + void setObject(String key, Object object, long timeout); /** - * 更新Object (过期时间不变) + * 更新 Object (过期时间不变) * @param key 键名称 * @param object 值 */ - public void updateObject(String key, Object object); + void updateObject(String key, Object object); /** - * 删除Object + * 删除 Object * @param key 键名称 */ - public void deleteObject(String key); + void deleteObject(String key); /** - * 获取Object的剩余存活时间 (单位: 秒) - * @param key 指定key - * @return 这个key的剩余存活时间 + * 获取 Object 的剩余存活时间 (单位: 秒) + * @param key 指定 key + * @return 这个 key 的剩余存活时间 */ - public long getObjectTimeout(String key); + long getObjectTimeout(String key); /** - * 修改Object的剩余存活时间 (单位: 秒) - * @param key 指定key - * @param timeout 过期时间 + * 修改 Object 的剩余存活时间(单位: 秒) + * @param key 指定 key + * @param timeout 剩余存活时间 */ - public void updateObjectTimeout(String key, long timeout); + void updateObjectTimeout(String key, long timeout); - // --------------------- Session读写 --------------------- + // --------------------- SaSession 读写 (默认复用 Object 读写方法) --------------------- /** - * 获取Session,如无返空 + * 获取 SaSession,如无返空 * @param sessionId sessionId * @return SaSession */ - public default SaSession getSession(String sessionId) { - return (SaSession)getObject(sessionId); - } + SaSession getSession(String sessionId); /** - * 写入Session,并设定存活时间 (单位: 秒) - * @param session 要保存的Session对象 - * @param timeout 过期时间 (单位: 秒) + * 写入 SaSession,并设定存活时间(单位: 秒) + * @param session 要保存的 SaSession 对象 + * @param timeout 过期时间(单位: 秒) */ - public default void setSession(SaSession session, long timeout) { - setObject(session.getId(), session, timeout); - } + void setSession(SaSession session, long timeout); /** - * 更新Session - * @param session 要更新的session对象 + * 更新 SaSession + * @param session 要更新的 SaSession 对象 */ - public default void updateSession(SaSession session) { - updateObject(session.getId(), session); - } + void updateSession(SaSession session); /** - * 删除Session + * 删除 SaSession * @param sessionId sessionId */ - public default void deleteSession(String sessionId) { - deleteObject(sessionId); - } + void deleteSession(String sessionId); /** - * 获取Session剩余存活时间 (单位: 秒) - * @param sessionId 指定Session - * @return 这个Session的剩余存活时间 + * 获取 SaSession 剩余存活时间(单位: 秒) + * @param sessionId 指定 SaSession + * @return 这个 SaSession 的剩余存活时间 */ - public default long getSessionTimeout(String sessionId) { - return getObjectTimeout(sessionId); - } + long getSessionTimeout(String sessionId); /** - * 修改Session剩余存活时间 (单位: 秒) - * @param sessionId 指定Session - * @param timeout 过期时间 + * 修改 SaSession 剩余存活时间(单位: 秒) + * @param sessionId 指定 SaSession + * @param timeout 剩余存活时间 */ - public default void updateSessionTimeout(String sessionId, long timeout) { - updateObjectTimeout(sessionId, timeout); - } + void updateSessionTimeout(String sessionId, long timeout); // --------------------- 会话管理 --------------------- @@ -168,11 +190,27 @@ public default void updateSessionTimeout(String sessionId, long timeout) { * 搜索数据 * @param prefix 前缀 * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 + * @param start 开始处索引 + * @param size 获取数量 (-1代表从 start 处一直取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * * @return 查询到的数据集合 */ - public List searchData(String prefix, String keyword, int start, int size); - - + List searchData(String prefix, String keyword, int start, int size, boolean sortType); + + + // --------------------- 生命周期 --------------------- + + /** + * 当此 SaTokenDao 实例被装载时触发 + */ + default void init() { + } + + /** + * 当此 SaTokenDao 实例被卸载时触发 + */ + default void destroy() { + } + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDaoDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDaoDefaultImpl.java index 81839c61a..2c457a8eb 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDaoDefaultImpl.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDaoDefaultImpl.java @@ -1,254 +1,103 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject; +import cn.dev33.satoken.dao.timedcache.SaMapPackageForConcurrentHashMap; +import cn.dev33.satoken.dao.timedcache.SaTimedCache; import cn.dev33.satoken.util.SaFoxUtil; +import java.util.List; + /** - * Sa-Token持久层接口 [默认实现类, 基于内存Map] - * @author kong + * Sa-Token 持久层接口,默认实现类,基于 SaTimedCache - ConcurrentHashMap (内存缓存,系统重启后数据丢失) * + * @author click33 + * @since 1.10.0 */ -public class SaTokenDaoDefaultImpl implements SaTokenDao { - +public class SaTokenDaoDefaultImpl implements SaTokenDaoByStringFollowObject { - /** - * 数据集合 - */ - public Map dataMap = new ConcurrentHashMap(); - - /** - * 过期时间集合 (单位: 毫秒) , 记录所有key的到期时间 [注意不是剩余存活时间] - */ - public Map expireMap = new ConcurrentHashMap(); - - /** - * 构造函数 - */ - public SaTokenDaoDefaultImpl() { - initRefreshThread(); - } - + public SaTimedCache timedCache = new SaTimedCache( + new SaMapPackageForConcurrentHashMap<>(), + new SaMapPackageForConcurrentHashMap<>() + ); - // ------------------------ String 读写操作 - - @Override - public String get(String key) { - clearKeyByTimeout(key); - return (String)dataMap.get(key); - } - - @Override - public void set(String key, String value, long timeout) { - if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - dataMap.put(key, value); - expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000)); - } - - @Override - public void update(String key, String value) { - if(getKeyTimeout(key) == SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - dataMap.put(key, value); - } - - @Override - public void delete(String key) { - dataMap.remove(key); - expireMap.remove(key); - } + // ------------------------ Object 读写操作 @Override - public long getTimeout(String key) { - return getKeyTimeout(key); - } - - @Override - public void updateTimeout(String key, long timeout) { - expireMap.put(key, System.currentTimeMillis() + timeout * 1000); + public Object getObject(String key) { + return timedCache.getObject(key); } - - // ------------------------ Object 读写操作 - @Override - public Object getObject(String key) { - clearKeyByTimeout(key); - return dataMap.get(key); + @SuppressWarnings("unchecked") + public T getObject(String key, Class classType){ + return (T) getObject(key); } @Override public void setObject(String key, Object object, long timeout) { - if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - dataMap.put(key, object); - expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000)); + timedCache.setObject(key, object, timeout); } @Override public void updateObject(String key, Object object) { - if(getKeyTimeout(key) == SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - // 无动作 + timedCache.updateObject(key, object); } @Override public void deleteObject(String key) { - dataMap.remove(key); - expireMap.remove(key); + timedCache.deleteObject(key); } @Override public long getObjectTimeout(String key) { - return getKeyTimeout(key); + return timedCache.getObjectTimeout(key); } @Override public void updateObjectTimeout(String key, long timeout) { - expireMap.put(key, System.currentTimeMillis() + timeout * 1000); + timedCache.updateObjectTimeout(key, timeout); } - - - // ------------------------ Session 读写操作 - // 使用接口默认实现 - - // ------------------------ 过期时间相关操作 - /** - * 如果指定key已经过期,则立即清除它 - * @param key 指定key - */ - void clearKeyByTimeout(String key) { - Long expirationTime = expireMap.get(key); - // 清除条件:如果不为空 && 不是[永不过期] && 已经超过过期时间 - if(expirationTime != null && expirationTime != SaTokenDao.NEVER_EXPIRE && expirationTime < System.currentTimeMillis()) { - dataMap.remove(key); - expireMap.remove(key); - } - } + // --------- 会话管理 - /** - * 获取指定key的剩余存活时间 (单位:秒) - */ - long getKeyTimeout(String key) { - // 先检查是否已经过期 - clearKeyByTimeout(key); - // 获取过期时间 - Long expire = expireMap.get(key); - // 如果根本没有这个值 - if(expire == null) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - // 如果被标注为永不过期 - if(expire == SaTokenDao.NEVER_EXPIRE) { - return SaTokenDao.NEVER_EXPIRE; - } - // ---- 计算剩余时间并返回 - long timeout = (expire - System.currentTimeMillis()) / 1000; - // 小于零时,视为不存在 - if(timeout < 0) { - dataMap.remove(key); - expireMap.remove(key); - return SaTokenDao.NOT_VALUE_EXPIRE; - } - return timeout; + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + return SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType); } - - - // --------------------- 定时清理过期数据 - - /** - * 执行数据清理的线程 - */ - public Thread refreshThread; - - /** - * 是否继续执行数据清理的线程标记 - */ - public boolean refreshFlag; - + + + // --------- 组件生命周期 /** - * 清理所有已经过期的key - */ - public void refreshDataMap() { - Iterator keys = expireMap.keySet().iterator(); - while (keys.hasNext()) { - clearKeyByTimeout(keys.next()); - } - } - - /** - * 初始化定时任务 + * 组件被安装时,开始刷新数据线程 */ - public void initRefreshThread() { - - // 如果配置了<=0的值,则不启动定时清理 - if(SaManager.getConfig().getDataRefreshPeriod() <= 0) { - return; - } - // 启动定时刷新 - this.refreshFlag = true; - this.refreshThread = new Thread(() -> { - for (;;) { - try { - try { - // 如果已经被标记为结束 - if(refreshFlag == false) { - return; - } - // 执行清理 - refreshDataMap(); - } catch (Exception e) { - e.printStackTrace(); - } - // 休眠N秒 - int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod(); - if(dataRefreshPeriod <= 0) { - dataRefreshPeriod = 1; - } - Thread.sleep(dataRefreshPeriod * 1000); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - refreshThread.start(); + @Override + public void init() { + timedCache.initRefreshThread(); } - + /** - * 结束定时任务 + * 组件被卸载时,结束定时任务,不再定时清理过期数据 */ - public void endRefreshThread() { - this.refreshFlag = false; - } - - - - - // --------------------- 会话管理 - @Override - public List searchData(String prefix, String keyword, int start, int size) { - return SaFoxUtil.searchList(expireMap.keySet(), prefix, keyword, start, size); + public void destroy() { + timedCache.endRefreshThread(); } - - - - - - - - } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByObjectFollowString.java b/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByObjectFollowString.java new file mode 100644 index 000000000..8b863b27e --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByObjectFollowString.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.auto; + +import cn.dev33.satoken.SaManager; + +/** + * SaTokenDao 次级实现,Object 读写跟随 String 读写 (推荐中间件型缓存实现 implements 此接口) + * + * @author click33 + * @since 1.41.0 + */ +public interface SaTokenDaoByObjectFollowString extends SaTokenDaoBySessionFollowObject { + + // --------------------- Object 读写 --------------------- + + /** + * 获取 Object,如无返空 + * + * @param key 键名称 + * @return object + */ + @Override + default Object getObject(String key) { + String jsonString = get(key); + return SaManager.getSaSerializerTemplate().stringToObject(jsonString); + } + + /** + * 获取 Object (指定反序列化类型),如无返空 + * + * @param key 键名称 + * @return object + */ + default T getObject(String key, Class classType) { + String jsonString = get(key); + return SaManager.getSaSerializerTemplate().stringToObject(jsonString, classType); + } + + /** + * 写入 Object,并设定存活时间 (单位: 秒) + * + * @param key 键名称 + * @param object 值 + * @param timeout 存活时间(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) + */ + @Override + default void setObject(String key, Object object, long timeout) { + String jsonString = SaManager.getSaSerializerTemplate().objectToString(object); + set(key, jsonString, timeout); + } + + /** + * 更新 Object (过期时间不变) + * @param key 键名称 + * @param object 值 + */ + @Override + default void updateObject(String key, Object object) { + String jsonString = SaManager.getSaSerializerTemplate().objectToString(object); + update(key, jsonString); + } + + /** + * 删除 Object + * @param key 键名称 + */ + @Override + default void deleteObject(String key) { + delete(key); + } + + /** + * 获取 Object 的剩余存活时间 (单位: 秒) + * @param key 指定 key + * @return 这个 key 的剩余存活时间 + */ + @Override + default long getObjectTimeout(String key) { + return getTimeout(key); + } + + /** + * 修改 Object 的剩余存活时间(单位: 秒) + * @param key 指定 key + * @param timeout 剩余存活时间 + */ + @Override + default void updateObjectTimeout(String key, long timeout) { + updateTimeout(key, timeout); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoBySessionFollowObject.java b/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoBySessionFollowObject.java new file mode 100644 index 000000000..34b18eca0 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoBySessionFollowObject.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.auto; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.strategy.SaStrategy; + +/** + * SaTokenDao 次级实现:SaSession 读写跟随 Object 读写 + * + * @author click33 + * @since 1.41.0 + */ +public interface SaTokenDaoBySessionFollowObject extends SaTokenDao { + + // --------------------- SaSession 读写 (默认复用 Object 读写方法) --------------------- + + /** + * 获取 SaSession,如无返空 + * @param sessionId sessionId + * @return SaSession + */ + default SaSession getSession(String sessionId) { + return getObject(sessionId, SaStrategy.instance.sessionClassType); + } + + /** + * 写入 SaSession,并设定存活时间(单位: 秒) + * @param session 要保存的 SaSession 对象 + * @param timeout 过期时间(单位: 秒) + */ + default void setSession(SaSession session, long timeout) { + setObject(session.getId(), session, timeout); + } + + /** + * 更新 SaSession + * @param session 要更新的 SaSession 对象 + */ + default void updateSession(SaSession session) { + updateObject(session.getId(), session); + } + + /** + * 删除 SaSession + * @param sessionId sessionId + */ + default void deleteSession(String sessionId) { + deleteObject(sessionId); + } + + /** + * 获取 SaSession 剩余存活时间(单位: 秒) + * @param sessionId 指定 SaSession + * @return 这个 SaSession 的剩余存活时间 + */ + default long getSessionTimeout(String sessionId) { + return getObjectTimeout(sessionId); + } + + /** + * 修改 SaSession 剩余存活时间(单位: 秒) + * @param sessionId 指定 SaSession + * @param timeout 剩余存活时间 + */ + default void updateSessionTimeout(String sessionId, long timeout) { + updateObjectTimeout(sessionId, timeout); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByStringFollowObject.java b/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByStringFollowObject.java new file mode 100644 index 000000000..3872ae34f --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByStringFollowObject.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.auto; + +/** + * SaTokenDao 次级实现:String 读写跟随 Object 读写 (推荐内存型缓存实现 implements 此接口) + * + * @author click33 + * @since 1.41.0 + */ +public interface SaTokenDaoByStringFollowObject extends SaTokenDaoBySessionFollowObject { + + // --------------------- String 读写 --------------------- + + @Override + default String get(String key) { + return (String) getObject(key); + } + + @Override + default void set(String key, String value, long timeout) { + setObject(key, value, timeout); + } + + @Override + default void update(String key, String value) { + updateObject(key, value); + } + + @Override + default void delete(String key) { + deleteObject(key); + } + + @Override + default long getTimeout(String key) { + return getObjectTimeout(key); + } + + @Override + default void updateTimeout(String key, long timeout) { + updateObjectTimeout(key, timeout); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackage.java b/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackage.java new file mode 100644 index 000000000..a18eaf5ba --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackage.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.timedcache; + +import java.util.Set; + +/** + * Map 包装类 + * + * @author click33 + * @since 1.41.0 + */ +public interface SaMapPackage { + + /** + * 获取底层被包装的源对象 + * + * @return / + */ + Object getSource(); + + + /** + * 读 + * + * @param key / + * @return / + */ + V get(String key); + + /** + * 写 + * + * @param key / + * @param value / + */ + void put(String key, V value); + + /** + * 删 + * @param key / + */ + void remove(String key); + + /** + * 所有 key + */ + Set keySet(); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackageForConcurrentHashMap.java b/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackageForConcurrentHashMap.java new file mode 100644 index 000000000..5c8840327 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackageForConcurrentHashMap.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.timedcache; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Map 包装类 (ConcurrentHashMap 版) + * + * @author click33 + * @since 1.41.0 + */ +public class SaMapPackageForConcurrentHashMap implements SaMapPackage { + + private final ConcurrentHashMap map = new ConcurrentHashMap(); + + @Override + public Object getSource() { + return map; + } + + @Override + public V get(String key) { + return map.get(key); + } + + @Override + public void put(String key, V value) { + map.put(key, value); + } + + @Override + public void remove(String key) { + map.remove(key); + } + + @Override + public Set keySet() { + return map.keySet(); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaTimedCache.java b/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaTimedCache.java new file mode 100644 index 000000000..713d71c35 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaTimedCache.java @@ -0,0 +1,209 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.timedcache; + + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; + +import java.util.Set; + +/** + * 一个定时缓存的简单实现,采用:惰性检查 + 异步循环扫描 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTimedCache { + + /** + * 存储数据的集合 + */ + public SaMapPackage dataMap; + + /** + * 存储数据过期时间的集合(单位: 毫秒), 记录所有 key 的到期时间 (注意存储的是到期时间,不是剩余存活时间) + */ + public SaMapPackage expireMap; + + public SaTimedCache(SaMapPackage dataMap, SaMapPackage expireMap) { + this.dataMap = dataMap; + this.expireMap = expireMap; + } + + + // ------------------------ 基础 API 读写操作 + + public Object getObject(String key) { + clearKeyByTimeout(key); + return dataMap.get(key); + } + + public void setObject(String key, Object object, long timeout) { + if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + dataMap.put(key, object); + expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000)); + } + + public void updateObject(String key, Object object) { + if(getKeyTimeout(key) == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + dataMap.put(key, object); + } + + public void deleteObject(String key) { + dataMap.remove(key); + expireMap.remove(key); + } + + public long getObjectTimeout(String key) { + return getKeyTimeout(key); + } + + public void updateObjectTimeout(String key, long timeout) { + expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000)); + } + + public Set keySet() { + return dataMap.keySet(); + } + + + // --------- 过期时间相关操作 + + /** + * 如果指定的 key 已经过期,则立即清除它 + * @param key 指定 key + */ + void clearKeyByTimeout(String key) { + Long expirationTime = expireMap.get(key); + // 清除条件: + // 1、数据存在。 + // 2、不是 [ 永不过期 ]。 + // 3、已经超过过期时间。 + if(expirationTime != null && expirationTime != SaTokenDao.NEVER_EXPIRE && expirationTime < System.currentTimeMillis()) { + dataMap.remove(key); + expireMap.remove(key); + } + } + + /** + * 获取指定 key 的剩余存活时间 (单位:秒) + * @param key 指定 key + * @return 这个 key 的剩余存活时间 + */ + long getKeyTimeout(String key) { + // 由于数据过期检测属于惰性扫描,很可能此时这个 key 已经是过期状态了,所以这里需要先检查一下 + clearKeyByTimeout(key); + + // 获取这个 key 的过期时间 + Long expire = expireMap.get(key); + + // 如果 expire 数据不存在,说明框架没有存储这个 key,此时返回 NOT_VALUE_EXPIRE + if(expire == null) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + + // 如果 expire 被标注为永不过期,则返回 NEVER_EXPIRE + if(expire == SaTokenDao.NEVER_EXPIRE) { + return SaTokenDao.NEVER_EXPIRE; + } + + // ---- 代码至此,说明这个 key 是有过期时间的,且未过期,那么: + + // 计算剩余时间并返回 (过期时间戳 - 当前时间戳) / 1000 转秒 + long timeout = (expire - System.currentTimeMillis()) / 1000; + + // 小于零时,视为不存在 + if(timeout < 0) { + dataMap.remove(key); + expireMap.remove(key); + return SaTokenDao.NOT_VALUE_EXPIRE; + } + return timeout; + } + + // --------- 定时清理过期数据 + + /** + * 执行数据清理的线程引用 + */ + public Thread refreshThread; + + /** + * 是否继续执行数据清理的线程标记 + */ + public volatile boolean refreshFlag; + + /** + * 清理所有已经过期的 key + */ + public void refreshDataMap() { + for (String s : expireMap.keySet()) { + clearKeyByTimeout(s); + } + } + + /** + * 初始化定时任务,定时清理过期数据 + */ + public void initRefreshThread() { + + // 如果开发者配置了 <=0 的值,则不启动定时清理 + if(SaManager.getConfig().getDataRefreshPeriod() <= 0) { + return; + } + + // 启动定时刷新 + this.refreshFlag = true; + this.refreshThread = new Thread(() -> { + for (;;) { + try { + try { + // 如果已经被标记为结束 + if( ! refreshFlag) { + return; + } + // 执行清理 + refreshDataMap(); + } catch (Exception e) { + e.printStackTrace(); + } + // 休眠N秒 + int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod(); + if(dataRefreshPeriod <= 0) { + dataRefreshPeriod = 1; + } + Thread.sleep(dataRefreshPeriod * 1000L); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + this.refreshThread.start(); + } + + /** + * 结束定时任务 + */ + public void endRefreshThread() { + this.refreshFlag = false; + } + +} 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 new file mode 100644 index 000000000..01f6833eb --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java @@ -0,0 +1,205 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.error; + +/** + * 定义所有异常细分状态码 + * + * @author click33 + * @since 1.33.0 + */ +public interface SaErrorCode { + + /** 代表这个异常在抛出时未指定异常细分状态码 */ + int CODE_UNDEFINED = -1; + + // ------------ + + /** 未能获取有效的上下文处理器 */ + int CODE_10001 = 10001; + + /** 未能获取有效的上下文 */ + int CODE_10002 = 10002; + + /** JSON 转换器未实现 */ + int CODE_10003 = 10003; + + /** HTTP 请求处理器未实现 */ + int CODE_10004 = 10004; + + /** 未能从全局 StpLogic 集合中找到对应 type 的 StpLogic */ + int CODE_10011 = 10011; + + /** 指定的配置文件加载失败 */ + int CODE_10021 = 10021; + + /** 配置文件属性无法正常读取 */ + int CODE_10022 = 10022; + + /** 重置的侦听器集合不可以为空 */ + int CODE_10031 = 10031; + + /** 注册的侦听器不可以为空 */ + int CODE_10032 = 10032; + + // 1030x core模块 + + /** 提供的 Same-Token 是无效的 */ + int CODE_10301 = 10301; + + /** 表示未能通过 Http Basic 认证校验 */ + int CODE_10311 = 10311; + + /** 表示未能通过 Http Digest 认证校验 */ + int CODE_10312 = 10312; + + /** 提供的 HttpMethod 是无效的 */ + int CODE_10321 = 10321; + + // 1100x StpLogic + + /** 未能读取到有效Token */ + int CODE_11001 = 11001; + + /** 登录时的账号id值为空 */ + int CODE_11002 = 11002; + + /** 更改 Token 指向的 账号Id 时,账号Id值为空 */ + int CODE_11003 = 11003; + + /** 登录失败:当前账号已在其它客户端登录 */ + int CODE_11004 = 11004; + + /** 未能读取到有效Token */ + int CODE_11011 = 11011; + + /** Token无效 */ + int CODE_11012 = 11012; + + /** Token已过期 */ + int CODE_11013 = 11013; + + /** Token已被顶下线 */ + int CODE_11014 = 11014; + + /** Token已被踢下线 */ + int CODE_11015 = 11015; + + /** Token已被冻结 */ + int CODE_11016 = 11016; + + /** 前端未按照指定的前缀提交 token */ + int CODE_11017 = 11017; + + /** 在未集成 sa-token-jwt 插件时调用 getExtra() 抛出异常 */ + int CODE_11031 = 11031; + + /** 缺少指定的角色 */ + int CODE_11041 = 11041; + + /** 缺少指定的权限 */ + int CODE_11051 = 11051; + + /** 当前账号未通过服务封禁校验 */ + int CODE_11061 = 11061; + + /** 提供要解禁的账号无效 */ + int CODE_11062 = 11062; + + /** 提供要解禁的服务无效 */ + int CODE_11063 = 11063; + + /** 提供要解禁的等级无效 */ + int CODE_11064 = 11064; + + /** 二级认证校验未通过 */ + int CODE_11071 = 11071; + + /** 获取 SaSession 时提供的 SessionId 为空 */ + int CODE_11072 = 11072; + + /** 获取 Token-Session 时提供的 token 为空 */ + int CODE_11073 = 11073; + + /** 获取 Token-Session 时提供的 token 为无效 token */ + int CODE_11074 = 11074; + + + // ------------ + + /** 请求中缺少指定的参数 */ + int CODE_12001 = 12001; + + /** 构建 Cookie 时缺少 name 参数 */ + int CODE_12002 = 12002; + + /** 构建 Cookie 时缺少 value 参数 */ + int CODE_12003 = 12003; + + // ------------ + + /** Base64 编码异常 */ + int CODE_12101 = 12101; + + /** Base64 解码异常 */ + int CODE_12102 = 12102; + + /** URL 编码异常 */ + int CODE_12103 = 12103; + + /** URL 解码异常 */ + int CODE_12104 = 12104; + + /** md5 加密异常 */ + int CODE_12111 = 12111; + + /** sha1 加密异常 */ + int CODE_12112 = 12112; + + /** sha256 加密异常 */ + int CODE_12113 = 12113; + + /** sha384 加密异常 */ + int CODE_121131 = 121131; + + /** sha512 加密异常 */ + int CODE_121132 = 121132; + + /** AES 加密异常 */ + int CODE_12114 = 12114; + + /** AES 解密异常 */ + int CODE_12115 = 12115; + + /** RSA 公钥加密异常 */ + int CODE_12116 = 12116; + + /** RSA 私钥加密异常 */ + int CODE_12117 = 12117; + + /** RSA 公钥解密异常 */ + int CODE_12118 = 12118; + + /** RSA 私钥解密异常 */ + int CODE_12119 = 12119; + + + // ------------ + + /** 未实现具体的路由匹配策略 */ + int CODE_12401 = 12401; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/ApiDisabledException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/ApiDisabledException.java new file mode 100644 index 000000000..c4de430ad --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/ApiDisabledException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表 API 已被禁用 + * + *

一般在 API 不合适调用的时候抛出,例如在集成 jwt 模块后调用数据持久化相关方法

+ * + * @author click33 + * @since 1.28.0 + */ +public class ApiDisabledException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130133L; + + /** 异常提示语 */ + public static final String BE_MESSAGE = "this api is disabled"; + + /** + * 一个异常:代表 API 已被禁用 + */ + public ApiDisabledException() { + super(BE_MESSAGE); + } + + /** + * 一个异常:代表 API 已被禁用 + * @param message 异常描述 + */ + public ApiDisabledException(String message) { + super(message); + } +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/BackResultException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/BackResultException.java index b978bea0c..2d28ed2a3 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/BackResultException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/BackResultException.java @@ -1,9 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; /** - * 一个异常:代表停止匹配,直接退出,向前端输出结果 + * 一个异常:代表停止匹配,直接退出,向前端输出结果 (框架内部专属异常,一般情况下开发者无需关注) * - * @author kong + * @author click33 + * @since 1.21.0 */ public class BackResultException extends SaTokenException { diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/DisableLoginException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/DisableLoginException.java deleted file mode 100644 index 852b8308f..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/DisableLoginException.java +++ /dev/null @@ -1,76 +0,0 @@ -package cn.dev33.satoken.exception; - -/** - * 一个异常:代表账号已被封禁 - * - * @author kong - */ -public class DisableLoginException extends SaTokenException { - - /** - * 序列化版本号 - */ - private static final long serialVersionUID = 6806129545290130143L; - - /** 异常标记值 */ - public static final String BE_VALUE = "disable"; - - /** 异常提示语 */ - public static final String BE_MESSAGE = "此账号已被封禁"; - - /** - * 账号类型 - */ - private String loginType; - - /** - * 被封禁的账号id - */ - private Object loginId; - - /** - * 封禁剩余时间,单位:秒 - */ - private long disableTime; - - /** - * 获取账号类型 - * - * @return See Note - */ - public String getLoginType() { - return loginType; - } - - /** - * 获取: 被封禁的账号id - * - * @return See above - */ - public Object getLoginId() { - return loginId; - } - - /** - * 获取: 封禁剩余时间,单位:秒 - * @return See above - */ - public long getDisableTime() { - return disableTime; - } - - /** - * 一个异常:代表账号已被封禁 - * - * @param loginType 账号类型 - * @param loginId 被封禁的账号id - * @param disableTime 封禁剩余时间,单位:秒 - */ - public DisableLoginException(String loginType, Object loginId, long disableTime) { - super(BE_MESSAGE); - this.loginId = loginId; - this.loginType = loginType; - this.disableTime = disableTime; - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/DisableServiceException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/DisableServiceException.java new file mode 100644 index 000000000..d8dde25b2 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/DisableServiceException.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表指定账号的指定服务已被封禁 + * + * @author click33 + * @since 1.31.0 + */ +public class DisableServiceException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130143L; + + /** 异常标记值(已更改为 SaTokenConsts.DEFAULT_DISABLE_LEVEL) */ + @Deprecated + public static final String BE_VALUE = "disable"; + + /** 异常提示语 */ + public static final String BE_MESSAGE = "此账号已被禁止访问服务:"; + + /** + * 账号类型 + */ + private final String loginType; + + /** + * 被封禁的账号id + */ + private final Object loginId; + + /** + * 具体被封禁的服务 + */ + private final String service; + + /** + * 具体被封禁的等级 + */ + private final int level; + + /** + * 校验时要求低于的等级 + */ + private final int limitLevel; + + /** + * 封禁剩余时间,单位:秒 + */ + private final long disableTime; + + /** + * 获取:账号类型 + * + * @return / + */ + public String getLoginType() { + return loginType; + } + + /** + * 获取: 被封禁的账号id + * + * @return / + */ + public Object getLoginId() { + return loginId; + } + + /** + * 获取: 被封禁的服务 + * + * @return / + */ + public Object getService() { + return service; + } + + /** + * 获取: 被封禁的等级 + * + * @return / + */ + public int getLevel() { + return level; + } + + /** + * 获取: 校验时要求低于的等级 + * + * @return / + */ + public int getLimitLevel() { + return limitLevel; + } + + /** + * 获取: 封禁剩余时间,单位:秒 + * @return / + */ + public long getDisableTime() { + return disableTime; + } + + /** + * 一个异常:代表指定账号指定服务已被封禁 + * + * @param loginType 账号类型 + * @param loginId 被封禁的账号id + * @param service 具体封禁的服务 + * @param level 被封禁的等级 + * @param limitLevel 校验时要求低于的等级 + * @param disableTime 封禁剩余时间,单位:秒 + */ + public DisableServiceException(String loginType, Object loginId, String service, int level, int limitLevel, long disableTime) { + super(BE_MESSAGE + service); + this.loginId = loginId; + this.loginType = loginType; + this.service = service; + this.level = level; + this.limitLevel = limitLevel; + this.disableTime = disableTime; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/FirewallCheckException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/FirewallCheckException.java new file mode 100644 index 000000000..4c7bb6f37 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/FirewallCheckException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表防火墙检验未通过 + * + * @author click33 + * @since 1.41.0 + */ +public class FirewallCheckException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 8243974276159004739L; + + public FirewallCheckException(String message) { + super(message); + } + + public FirewallCheckException(Throwable e) { + super(e); + } + + public FirewallCheckException(String message, Throwable e) { + super(message, e); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/IdTokenInvalidException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/IdTokenInvalidException.java deleted file mode 100644 index 4a6e94f37..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/IdTokenInvalidException.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.dev33.satoken.exception; - -/** - * 一个异常:代表提供的 Id-Token 无效 - * - * @author kong - */ -public class IdTokenInvalidException extends SaTokenException { - - /** - * 序列化版本号 - */ - private static final long serialVersionUID = 6806129545290130144L; - - /** - * 一个异常:代表提供的 Id-Token 无效 - */ - public IdTokenInvalidException(String message) { - super(message); - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/InvalidContextException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/InvalidContextException.java new file mode 100644 index 000000000..8bd49f385 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/InvalidContextException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表框架未能获取有效的上下文 + *

已过期:请更名为 SaTokenContextException 用法不变,未来版本将彻底删除此类

+ * + * @author click33 + * @since 1.33.0 + */ +@Deprecated +public class InvalidContextException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** + * 一个异常:代表框架未能获取有效的上下文 + * @param message 异常描述 + */ + public InvalidContextException(String message) { + super(message); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpBasicAuthException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpBasicAuthException.java new file mode 100644 index 000000000..bfa02b3d7 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpBasicAuthException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表会话未能通过 Http Basic 认证校验 + * + * @author click33 + * @since 1.26.0 + */ +public class NotHttpBasicAuthException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** 异常提示语 */ + public static final String BE_MESSAGE = "no basic auth"; + + /** + * 一个异常:代表会话未通过 Http Basic 认证 + */ + public NotHttpBasicAuthException() { + super(BE_MESSAGE); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java new file mode 100644 index 000000000..3944387a4 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表会话未能通过 Http Digest 认证校验 + * + * @author click33 + * @since 1.38.0 + */ +public class NotHttpDigestAuthException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** 异常提示语 */ + public static final String BE_MESSAGE = "no http digest auth"; + + /** + * 一个异常:代表会话未通过 Http Digest 认证 + */ + public NotHttpDigestAuthException() { + super(BE_MESSAGE); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotImplException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotImplException.java new file mode 100644 index 000000000..0e68f471b --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotImplException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表组件或方法未被提供有效的实现 + * + * @author click33 + * @since 1.33.0 + */ +public class NotImplException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** + * 一个异常:代表组件或方法未被提供有效的实现 + * @param message 异常描述 + */ + public NotImplException(String message) { + super(message); + } + +} + diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotLoginException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotLoginException.java index 37c1e8f69..029db8ca7 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotLoginException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotLoginException.java @@ -1,11 +1,30 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; +import cn.dev33.satoken.util.SaFoxUtil; + import java.util.Arrays; import java.util.List; /** - * 一个异常:代表会话未能通过登录认证 - * @author kong + * 一个异常:代表会话未能通过登录认证校验 + * + * @author click33 + * @since 1.10.0 */ public class NotLoginException extends SaTokenException { @@ -22,40 +41,49 @@ public class NotLoginException extends SaTokenException { * 因为loginId刚取出的时候类型为String,为了避免两者相比较时不必要的类型转换带来的性能消耗,故在此直接将常量类型设计为String */ - /** 表示未提供token */ + /** 表示未能读取到有效 token */ public static final String NOT_TOKEN = "-1"; - public static final String NOT_TOKEN_MESSAGE = "未提供token"; + public static final String NOT_TOKEN_MESSAGE = "未能读取到有效 token"; - /** 表示token无效 */ + /** 表示 token 无效 */ public static final String INVALID_TOKEN = "-2"; - public static final String INVALID_TOKEN_MESSAGE = "token无效"; + public static final String INVALID_TOKEN_MESSAGE = "token 无效"; - /** 表示token已过期 */ + /** 表示 token 已过期 */ public static final String TOKEN_TIMEOUT = "-3"; - public static final String TOKEN_TIMEOUT_MESSAGE = "token已过期"; + public static final String TOKEN_TIMEOUT_MESSAGE = "token 已过期"; - /** 表示token已被顶下线 */ + /** 表示 token 已被顶下线 */ public static final String BE_REPLACED = "-4"; - public static final String BE_REPLACED_MESSAGE = "token已被顶下线"; + public static final String BE_REPLACED_MESSAGE = "token 已被顶下线"; - /** 表示token已被踢下线 */ + /** 表示 token 已被踢下线 */ public static final String KICK_OUT = "-5"; - public static final String KICK_OUT_MESSAGE = "token已被踢下线"; + public static final String KICK_OUT_MESSAGE = "token 已被踢下线"; + + /** 表示 token 已被冻结 */ + public static final String TOKEN_FREEZE = "-6"; + public static final String TOKEN_FREEZE_MESSAGE = "token 已被冻结"; + + /** 表示 未按照指定前缀提交 token */ + public static final String NO_PREFIX = "-7"; + public static final String NO_PREFIX_MESSAGE = "未按照指定前缀提交 token"; /** 默认的提示语 */ public static final String DEFAULT_MESSAGE = "当前会话未登录"; /** - * 代表异常token的标志集合 + * 代表异常 token 的标志集合 */ - public static final List ABNORMAL_LIST = Arrays.asList(NOT_TOKEN, INVALID_TOKEN, TOKEN_TIMEOUT, BE_REPLACED, KICK_OUT); + public static final List ABNORMAL_LIST = + Arrays.asList(NOT_TOKEN, INVALID_TOKEN, TOKEN_TIMEOUT, BE_REPLACED, KICK_OUT, TOKEN_FREEZE, NO_PREFIX); /** * 异常类型 */ - private String type; + private final String type; /** * 获取异常类型 @@ -69,7 +97,7 @@ public String getType() { /** * 账号类型 */ - private String loginType; + private final String loginType; /** * 获得账号类型 @@ -78,45 +106,30 @@ public String getType() { public String getLoginType() { return loginType; } - - + /** - * 构造方法创建一个 - * @param message 异常消息 + * 构造方法创建一个 + * @param message 异常消息 * @param loginType 账号类型 - * @param type 类型 + * @param type 类型 */ public NotLoginException(String message, String loginType, String type) { - super(message); + super(message); this.loginType = loginType; this.type = type; } - + /** - * 静态方法构建一个NotLoginException + * 静态方法构建一个 NotLoginException * @param loginType 账号类型 - * @param type 账号类型 - * @return 构建完毕的异常对象 + * @param type 未登录场景值 + * @param message 异常描述信息 + * @param token 引起异常的 token 值,可不填,如果填了会拼接到异常描述信息后面 + * @return 构建完毕的异常对象 */ - public static NotLoginException newInstance(String loginType, String type) { - String message = null; - if(NOT_TOKEN.equals(type)) { - message = NOT_TOKEN_MESSAGE; - } - else if(INVALID_TOKEN.equals(type)) { - message = INVALID_TOKEN_MESSAGE; - } - else if(TOKEN_TIMEOUT.equals(type)) { - message = TOKEN_TIMEOUT_MESSAGE; - } - else if(BE_REPLACED.equals(type)) { - message = BE_REPLACED_MESSAGE; - } - else if(KICK_OUT.equals(type)) { - message = KICK_OUT_MESSAGE; - } - else { - message = DEFAULT_MESSAGE; + public static NotLoginException newInstance(String loginType, String type, String message, String token) { + if(SaFoxUtil.isNotEmpty(token)) { + message = message + ":" + token; } return new NotLoginException(message, loginType, type); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotPermissionException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotPermissionException.java index 6ab6e0b80..672b1097a 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotPermissionException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotPermissionException.java @@ -1,12 +1,27 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; import cn.dev33.satoken.stp.StpUtil; /** - * 一个异常:代表会话未能通过权限认证 + * 一个异常:代表会话未能通过权限认证校验 * - * @author kong - * + * @author click33 + * @since 1.10.0 */ public class NotPermissionException extends SaTokenException { @@ -16,19 +31,19 @@ public class NotPermissionException extends SaTokenException { private static final long serialVersionUID = 6806129545290130141L; /** 权限码 */ - private String code; + private final String permission; /** - * @return 获得权限码 + * @return 获得具体缺少的权限码 */ - public String getCode() { - return code; + public String getPermission() { + return permission; } /** * 账号类型 */ - private String loginType; + private final String loginType; /** * 获得账号类型 @@ -39,13 +54,13 @@ public String getLoginType() { return loginType; } - public NotPermissionException(String code) { - this(code, StpUtil.stpLogic.loginType); + public NotPermissionException(String permission) { + this(permission, StpUtil.stpLogic.loginType); } - public NotPermissionException(String code, String loginType) { - super("无此权限:" + code); - this.code = code; + public NotPermissionException(String permission, String loginType) { + super("无此权限:" + permission); + this.permission = permission; this.loginType = loginType; } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotRoleException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotRoleException.java index 59d773d11..15758c8c1 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotRoleException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotRoleException.java @@ -1,12 +1,27 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; import cn.dev33.satoken.stp.StpUtil; /** - * 一个异常:代表会话未能通过角色认证 + * 一个异常:代表会话未能通过角色认证校验 * - * @author kong - * + * @author click33 + * @since 1.10.0 */ public class NotRoleException extends SaTokenException { @@ -16,7 +31,7 @@ public class NotRoleException extends SaTokenException { private static final long serialVersionUID = 8243974276159004739L; /** 角色标识 */ - private String role; + private final String role; /** * @return 获得角色标识 @@ -28,7 +43,7 @@ public String getRole() { /** * 账号类型 */ - private String loginType; + private final String loginType; /** * 获得账号类型 diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotSafeException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotSafeException.java index 5a467fc5a..b65b9e512 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotSafeException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotSafeException.java @@ -1,9 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; /** - * 一个异常:代表会话未能通过二级认证 + * 一个异常:代表会话未能通过二级认证校验 * - * @author kong + * @author click33 + * @since 1.21.0 */ public class NotSafeException extends SaTokenException { @@ -13,13 +29,62 @@ public class NotSafeException extends SaTokenException { private static final long serialVersionUID = 6806129545290130144L; /** 异常提示语 */ - public static final String BE_MESSAGE = "二级认证失败"; + public static final String BE_MESSAGE = "二级认证校验失败"; + + /** + * 账号类型 + */ + private final String loginType; + + /** + * 未通过校验的 Token 值 + */ + private final Object tokenValue; + + /** + * 未通过校验的服务 + */ + private final String service; + + /** + * 获取:账号类型 + * + * @return / + */ + public String getLoginType() { + return loginType; + } + + /** + * 获取: 未通过校验的 Token 值 + * + * @return / + */ + public Object getTokenValue() { + return tokenValue; + } + + /** + * 获取: 未通过校验的服务 + * + * @return / + */ + public Object getService() { + return service; + } /** - * 一个异常:代表会话未通过二级认证 + * 一个异常:代表会话未能通过二级认证校验 + * + * @param loginType 账号类型 + * @param tokenValue 未通过校验的 Token 值 + * @param service 未通过校验的服务 */ - public NotSafeException() { - super(BE_MESSAGE); + public NotSafeException(String loginType, String tokenValue, String service) { + super(BE_MESSAGE + ":" + service); + this.tokenValue = tokenValue; + this.loginType = loginType; + this.service = service; } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotWebContextException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotWebContextException.java new file mode 100644 index 000000000..e3e805092 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotWebContextException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表当前不是 Web 上下文,无法调用某个 API + * + * @author click33 + * @since 1.33.0 + */ +public class NotWebContextException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** + * 一个异常:代表当前不是 Web 上下文,无法调用某个 API + * @param message 异常描述 + */ + public NotWebContextException(String message) { + super(message); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/RequestPathInvalidException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/RequestPathInvalidException.java new file mode 100644 index 000000000..d5f780439 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/RequestPathInvalidException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表请求 path 无效或非法 + * + * @author click33 + * @since 1.37.0 + */ +public class RequestPathInvalidException extends FirewallCheckException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 8243974276159004739L; + + /** 具体无效的 path */ + private final String path; + + /** + * @return 具体无效的 path + */ + public String getPath() { + return path; + } + + public RequestPathInvalidException(String message, String path) { + super(message); + this.path = path; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaJsonConvertException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaJsonConvertException.java new file mode 100644 index 000000000..39e4b61d2 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaJsonConvertException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表 JSON 转换失败 + * + * @author click33 + * @since 1.30.0 + */ +public class SaJsonConvertException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290134144L; + + /** + * 一个异常:代表 JSON 转换失败 + * @param cause 异常对象 + */ + public SaJsonConvertException(Throwable cause) { + super(cause); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenContextException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenContextException.java new file mode 100644 index 000000000..8b822f3b2 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenContextException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +import java.io.Serializable; + +/** + * 一个异常:代表框架未能获取有效的上下文 + * + * @author click33 + * @since 1.33.0 + */ +public class SaTokenContextException extends InvalidContextException implements Serializable { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** + * 一个异常:代表框架未能获取有效的上下文 + * @param message 异常描述 + */ + public SaTokenContextException(String message) { + super(message); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java index fea45b3c0..a008fa5fc 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java @@ -1,11 +1,30 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.util.SaFoxUtil; + /** - * Sa-Token框架内部逻辑发生错误抛出的异常 - * (自定义此异常方便开发者在做全局异常处理时分辨异常类型) - * - * @author kong + * Sa-Token 框架内部逻辑发生错误抛出的异常 * + *

框架其它异常均继承自此类,开发者可通过捕获此异常来捕获框架内部抛出的所有异常

+ * + * @author click33 + * @since 1.10.0 */ public class SaTokenException extends RuntimeException { @@ -14,6 +33,22 @@ public class SaTokenException extends RuntimeException { */ private static final long serialVersionUID = 6806129545290130132L; + /** + * 异常细分状态码 + */ + private int code = SaErrorCode.CODE_UNDEFINED; + + /** + * 构建一个异常 + * + * @param code 异常细分状态码 + */ + public SaTokenException(int code) { + super(); + this.code = code; + } + + /** * 构建一个异常 * @@ -23,6 +58,17 @@ public SaTokenException(String message) { super(message); } + /** + * 构建一个异常 + * + * @param code 异常细分状态码 + * @param message 异常信息 + */ + public SaTokenException(int code, String message) { + super(message); + this.code = code; + } + /** * 构建一个异常 * @@ -42,4 +88,64 @@ public SaTokenException(String message, Throwable cause) { super(message, cause); } + /** + * 获取异常细分状态码 + * @return 异常细分状态码 + */ + public int getCode() { + return code; + } + + /** + * 写入异常细分状态码 + * @param code 异常细分状态码 + * @return 对象自身 + */ + public SaTokenException setCode(int code) { + this.code = code; + return this; + } + + /** + * 断言 flag 不为 true,否则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + */ + public static void notTrue(boolean flag, String message) { + notTrue(flag, message, SaErrorCode.CODE_UNDEFINED); + } + + /** + * 断言 flag 不为 true,否则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分状态码 + */ + public static void notTrue(boolean flag, String message, int code) { + if(flag) { + throw new SaTokenException(message).setCode(code); + } + } + + /** + * 断言 value 不为空,否则抛出 message 异常 + * @param value 值 + * @param message 异常信息 + */ + public static void notEmpty(Object value, String message) { + notEmpty(value, message, SaErrorCode.CODE_UNDEFINED); + } + + /** + * 断言 value 不为空,否则抛出 message 异常 + * @param value 值 + * @param message 异常信息 + * @param code 异常细分状态码 + */ + public static void notEmpty(Object value, String message, int code) { + if(SaFoxUtil.isEmpty(value)) { + throw new SaTokenException(message).setCode(code); + } + } + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenPluginException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenPluginException.java new file mode 100644 index 000000000..7180bc7cb --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenPluginException.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表插件安装过程中出现异常 + * + * @author click33 + * @since 1.28.0 + */ +public class SaTokenPluginException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130131L; + + /** + * 一个异常:代表插件安装过程中出现异常 + * @param message 异常描述 + */ + public SaTokenPluginException(String message) { + super(message); + } + + /** + * 一个异常:代表插件安装过程中出现异常 + * + * @param cause 异常对象 + */ + public SaTokenPluginException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表插件安装过程中出现异常 + * + * @param message 异常描述 + * @param cause 异常对象 + */ + public SaTokenPluginException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/SameTokenInvalidException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SameTokenInvalidException.java new file mode 100644 index 000000000..acab8605c --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SameTokenInvalidException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表 Same-Token 校验未通过 + * + * @author click33 + * @since 1.32.0 + */ +public class SameTokenInvalidException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** + * 一个异常:代表 Same-Token 校验未通过 + * @param message 异常描述 + */ + public SameTokenInvalidException(String message) { + super(message); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/StopMatchException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/StopMatchException.java index 8f2793310..1f37620a0 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/StopMatchException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/StopMatchException.java @@ -1,9 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; /** - * 一个异常:代表停止路由匹配 + * 一个异常:代表停止路由匹配,进入 Controller (框架内部专属异常,一般情况下开发者无需关注) * - * @author kong + * @author click33 + * @since 1.20.0 */ public class StopMatchException extends SaTokenException { diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/TotpAuthException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/TotpAuthException.java new file mode 100644 index 000000000..1d0fc4987 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/TotpAuthException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.exception; + +/** + * 一个异常:代表 TOTP 校验未通过 + * + * @author click33 + * @since 1.41.0 + */ +public class TotpAuthException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** 异常提示语 */ + public static final String BE_MESSAGE = "totp check fail"; + + /** + * 一个异常:代表会话未通过 Totp 校验 + */ + public TotpAuthException() { + super(BE_MESSAGE); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilter.java b/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilter.java new file mode 100644 index 000000000..1c7c11523 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; + +import java.util.List; + +/** + * Sa-Token 过滤器接口,为不同版本的过滤器: + * 1、封装共同代码。 + * 2、定义统一的行为接口。 + * + * @author click33 + * @since 1.34.0 + */ +public interface SaFilter { + + // ------------------------ 设置此过滤器 拦截 & 放行 的路由 + + /** + * 添加 [ 拦截路由 ] + * @param paths 路由 + * @return 对象自身 + */ + SaFilter addInclude(String... paths); + + /** + * 添加 [ 放行路由 ] + * @param paths 路由 + * @return 对象自身 + */ + SaFilter addExclude(String... paths); + + /** + * 写入 [ 拦截路由 ] 集合 + * @param pathList 路由集合 + * @return 对象自身 + */ + SaFilter setIncludeList(List pathList); + + /** + * 写入 [ 放行路由 ] 集合 + * @param pathList 路由集合 + * @return 对象自身 + */ + SaFilter setExcludeList(List pathList); + + + // ------------------------ 钩子函数 + + /** + * 写入[ 认证函数 ]: 每次请求执行 + * @param auth see note + * @return 对象自身 + */ + SaFilter setAuth(SaFilterAuthStrategy auth); + + /** + * 写入[ 异常处理函数 ]:每次[ 认证函数 ]发生异常时执行此函数 + * @param error see note + * @return 对象自身 + */ + SaFilter setError(SaFilterErrorStrategy error); + + /** + * 写入[ 前置函数 ]:在每次[ 认证函数 ]之前执行。 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth + * @param beforeAuth / + * @return 对象自身 + */ + SaFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth); + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterAuthStrategy.java b/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterAuthStrategy.java index 1873d5f06..20962ada6 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterAuthStrategy.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterAuthStrategy.java @@ -1,16 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; /** - * Sa-Token全局过滤器-认证策略 - * @author kong + * Sa-Token 全局过滤器 - 认证策略封装,方便 lambda 表达式风格调用 * + * @author click33 + * @since 1.17.0 */ +@FunctionalInterface public interface SaFilterAuthStrategy { /** * 执行方法 - * @param r 无含义参数,留作扩展 + * @param obj 无含义参数,留作扩展 */ - public void run(Object r); + void run(Object obj); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterErrorStrategy.java b/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterErrorStrategy.java index b1fff4130..afaec9f5e 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterErrorStrategy.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterErrorStrategy.java @@ -1,17 +1,36 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; /** - * Sa-Token全局过滤器-异常处理策略 - * @author kong + * Sa-Token 全局过滤器 - 异常处理策略封装,方便 lambda 表达式风格调用 + * + *

此方法的返回值将在 toString() 后返回给前端,如果你要返回 JSON 数据,需要在返回前自行序列化为 JSON 字符串

* + * @author click33 + * @since 1.16.0 */ +@FunctionalInterface public interface SaFilterErrorStrategy { /** * 执行方法 * @param e 异常对象 - * @return 输出对象(请提前序列化) + * @return 输出对象,此返回值将在 toString() 后返回给前端,如果你要返回 JSON 数据,需要在返回前自行序列化为 JSON 字符串 */ - public Object run(Throwable e); + Object run(Throwable e); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/IsRunFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/IsRunFunction.java index 7d611a648..d528dec40 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/fun/IsRunFunction.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/IsRunFunction.java @@ -1,10 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun; /** - * 根据Boolean变量,决定是否执行一个函数 + * lambda 表达式辅助封装:根据 Boolean 变量,决定是否执行一个函数 * - * @author kong - * + * @author click33 + * @since 1.13.0 */ public class IsRunFunction { diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaFunction.java index e8ad5e5d8..019d8dfbb 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaFunction.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaFunction.java @@ -1,10 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun; /** - * 设定一个函数,方便在Lambda表达式下的函数式编程 + * 无形参、无返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 * - * @author kong - * + * @author click33 + * @since 1.13.0 */ @FunctionalInterface public interface SaFunction { @@ -12,6 +27,6 @@ public interface SaFunction { /** * 执行的方法 */ - public void run(); + void run(); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamFunction.java new file mode 100644 index 000000000..d0e33f1de --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun; + +/** + * 单形参、无返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 + * + * @author click33 + * @since 1.27.0 + */ +@FunctionalInterface +public interface SaParamFunction { + + /** + * 执行的方法 + * @param r 传入的参数 + */ + void run(T r); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamRetFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamRetFunction.java new file mode 100644 index 000000000..ed924055e --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamRetFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun; + +/** + * 单形参、有返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 + * + * @author click33 + * @since 1.27.0 + */ +@FunctionalInterface +public interface SaParamRetFunction { + + /** + * 执行的方法 + * @param param 传入的参数 + * @return 返回值 + */ + R run(T param); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetFunction.java index 58cc7ea31..3a067b39a 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetFunction.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetFunction.java @@ -1,9 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun; /** - * 设定一个函数,并返回一个值,方便在Lambda表达式下的函数式编程 - * @author kong + * 无形参、有返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 * + * @author click33 + * @since 1.20.0 */ @FunctionalInterface public interface SaRetFunction { @@ -12,6 +28,6 @@ public interface SaRetFunction { * 执行的方法 * @return 返回值 */ - public Object run(); + Object run(); } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetGenericFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetGenericFunction.java new file mode 100644 index 000000000..a1a5f0688 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetGenericFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun; + +/** + * 无形参、有返回值(泛型)的函数式接口,方便开发者进行 lambda 表达式风格调用 + * + * @author click33 + * @since 1.42.0 + */ +@FunctionalInterface +public interface SaRetGenericFunction { + + /** + * 执行的方法 + * @return 返回值 + */ + T run(); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRouteFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRouteFunction.java new file mode 100644 index 000000000..54bdbe716 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRouteFunction.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; + +/** + * 路由拦截器验证方法的函数式接口,方便开发者进行 lambda 表达式风格调用 + * + * @author click33 + * @since 1.34.0 + */ +@FunctionalInterface +public interface SaRouteFunction { + + /** + * 执行验证的方法 + * + * @param request Request 包装对象 + * @param response Response 包装对象 + * @param handler 处理对象 + */ + void run(SaRequest request, SaResponse response, Object handler); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaTwoParamFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaTwoParamFunction.java new file mode 100644 index 000000000..c7373c8d5 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/SaTwoParamFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun; + +/** + * 双形参、无返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 + * + * @author click33 + * @since 1.41.0 + */ +@FunctionalInterface +public interface SaTwoParamFunction { + + /** + * 执行的方法 + * @param r 传入的参数 + * @param r2 传入的参数 2 + */ + void run(T r, T2 r2); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/hooks/SaTokenPluginHookFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/hooks/SaTokenPluginHookFunction.java new file mode 100644 index 000000000..3c4aab082 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/hooks/SaTokenPluginHookFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.hooks; + +import cn.dev33.satoken.plugin.SaTokenPlugin; + +/** + * SaTokenPlugin 钩子函数 + * + * @author click33 + * @since 1.41.0 + */ +@FunctionalInterface +public interface SaTokenPluginHookFunction { + + /** + * 执行的方法 + * @param plugin 插件实例 + */ + void execute(SaTokenPlugin plugin); +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaAutoRenewFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaAutoRenewFunction.java new file mode 100644 index 000000000..ed778e7c0 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaAutoRenewFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import cn.dev33.satoken.stp.StpLogic; + +import java.util.function.Function; + +/** + * 函数式接口:自定义自动续期条件 + * + *

参数:StpLogic 实例

+ *

返回:Boolean 是否续期

+ * + * @author fangzhengjin + * @since 1.41.0 + */ +@FunctionalInterface +public interface SaAutoRenewFunction extends Function { +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckELRootMapExtendFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckELRootMapExtendFunction.java new file mode 100644 index 000000000..7ea12b26c --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckELRootMapExtendFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.util.Map; +import java.util.function.Consumer; + +/** + * 函数式接口:SaCheckELRootMap 扩展函数 + * + *

参数:SaCheckELRootMap 对象

+ * + * @author click33 + * @since 1.40.0 + */ +@FunctionalInterface +public interface SaCheckELRootMapExtendFunction extends Consumer> { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckElementAnnotationFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckElementAnnotationFunction.java new file mode 100644 index 000000000..97f82efbf --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckElementAnnotationFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.lang.reflect.AnnotatedElement; +import java.util.function.Consumer; + +/** + * 函数式接口:对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) + * + *

参数:element元素

+ *

返回:无

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaCheckElementAnnotationFunction extends Consumer { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckMethodAnnotationFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckMethodAnnotationFunction.java new file mode 100644 index 000000000..e2ac1dff9 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckMethodAnnotationFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.lang.reflect.Method; +import java.util.function.Consumer; + +/** + * 函数式接口:对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) + * + *

参数:Method句柄

+ *

返回:无

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaCheckMethodAnnotationFunction extends Consumer { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckOrAnnotationFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckOrAnnotationFunction.java new file mode 100644 index 000000000..7ca89f13e --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckOrAnnotationFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import cn.dev33.satoken.annotation.SaCheckOr; + +import java.util.function.Consumer; + +/** + * 函数式接口:对一个 @SaCheckOr 进行注解校验 + * + *

参数:SaCheckOr 注解的实例

+ *

返回:无

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaCheckOrAnnotationFunction extends Consumer { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCorsHandleFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCorsHandleFunction.java new file mode 100644 index 000000000..c37224df8 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCorsHandleFunction.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; + +/** + * CORS 跨域策略处理函数 + * + * @author click33 + * @since 1.42.0 + */ +@FunctionalInterface +public interface SaCorsHandleFunction { + + /** + * CORS 策略处理函数 + * + * @param req 请求包装对象 + * @param res 响应包装对象 + * @param sto 数据读写对象 + */ + void execute( + SaRequest req, + SaResponse res, + SaStorage sto + ); + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateSessionFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateSessionFunction.java new file mode 100644 index 000000000..31e4413bd --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateSessionFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import cn.dev33.satoken.session.SaSession; + +import java.util.function.Function; + +/** + * 函数式接口:创建 SaSession 的策略 + * + *

参数:SessionId

+ *

返回:SaSession对象

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaCreateSessionFunction extends Function { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateStpLogicFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateStpLogicFunction.java new file mode 100644 index 000000000..5f6b6c173 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateStpLogicFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import cn.dev33.satoken.stp.StpLogic; + +import java.util.function.Function; + +/** + * 函数式接口:创建 StpLogic 的算法 + * + *

参数:账号体系标识

+ *

返回:创建好的 StpLogic 对象

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaCreateStpLogicFunction extends Function { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateTokenFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateTokenFunction.java new file mode 100644 index 000000000..5598ee0bd --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateTokenFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.util.function.BiFunction; + +/** + * 函数式接口:创建 token 的策略 + * + *

参数:账号 id、账号类型

+ *

返回:token 值

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaCreateTokenFunction extends BiFunction { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFailHandleFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFailHandleFunction.java new file mode 100644 index 000000000..37457a72a --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFailHandleFunction.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.FirewallCheckException; + +/** + * 函数式接口:当防火墙校验不通过时执行的函数 + * + * @author click33 + * @since 1.37.0 + */ +@FunctionalInterface +public interface SaFirewallCheckFailHandleFunction { + + /** + * 执行函数 + * @param e 防火墙校验异常 + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + void run(FirewallCheckException e, SaRequest req, SaResponse res, Object extArg); + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFunction.java new file mode 100644 index 000000000..63921bf48 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFunction.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; + +/** + * 函数式接口:防火墙校验函数 + * + * @author click33 + * @since 1.37.0 + */ +@FunctionalInterface +public interface SaFirewallCheckFunction { + + /** + * 执行函数 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + void execute(SaRequest req, SaResponse res, Object extArg); + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGenerateUniqueTokenFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGenerateUniqueTokenFunction.java new file mode 100644 index 000000000..4f3b7df19 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGenerateUniqueTokenFunction.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 生成唯一式 token 的函数式接口,方便开发者进行 lambda 表达式风格调用 + * + *

参数:元素名称, 最大尝试次数, 创建 token 函数, 检查 token 函数

+ *

返回:生成的token

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaGenerateUniqueTokenFunction { + + /** + * 封装 token 生成、校验的代码,生成唯一式 token + * + * @param elementName 要生成的元素名称,方便抛出异常时组织提示信息 + * @param maxTryTimes 最大尝试次数 + * @param createTokenFunction 创建 token 的函数 + * @param checkTokenFunction 校验 token 是否唯一的函数(返回 true 表示唯一,可用) + * @return 最终生成的唯一式 token + */ + String execute( + String elementName, + int maxTryTimes, + Supplier createTokenFunction, + Function checkTokenFunction + ); + +} \ No newline at end of file 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 new file mode 100644 index 000000000..c0c8ccf80 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGetAnnotationFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.function.BiFunction; + +/** + * 函数式接口:从元素上获取注解 + * + *

参数:element元素,要获取的注解类型

+ *

返回:注解对象

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaGetAnnotationFunction extends BiFunction, Annotation> { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaHasElementFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaHasElementFunction.java new file mode 100644 index 000000000..c878f7d02 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaHasElementFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.util.List; +import java.util.function.BiFunction; + +/** + * 函数式接口:判断集合中是否包含指定元素(模糊匹配) + * + *

参数:集合、元素

+ *

返回:是否包含

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaHasElementFunction extends BiFunction, String, Boolean> { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaIsAnnotationPresentFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaIsAnnotationPresentFunction.java new file mode 100644 index 000000000..9d5f1f6c8 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaIsAnnotationPresentFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.function.BiFunction; + +/** + * 函数式接口:判断一个 Method 或其所属 Class 是否包含指定注解 + * + *

参数:Method、注解

+ *

返回:是否包含

+ * + * @author click33 + * @since 1.35.0 + */ +@FunctionalInterface +public interface SaIsAnnotationPresentFunction extends BiFunction, Boolean> { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaRouteMatchFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaRouteMatchFunction.java new file mode 100644 index 000000000..dc9fa1278 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaRouteMatchFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.fun.strategy; + +import java.util.function.BiFunction; + +/** + * 函数式接口:路由匹配策略 + * + *

参数:pattern, path

+ *

返回:是否匹配

+ * + * @author click33 + * @since 1.42.0 + */ +@FunctionalInterface +public interface SaRouteMatchFunction extends BiFunction { + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplate.java new file mode 100644 index 000000000..64cde0449 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplate.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.http; + +import java.util.Map; + +/** + * Http 请求处理器 + * + * @author click33 + * @since 1.43.0 + */ +public interface SaHttpTemplate { + + /** + * get 请求 + * + * @param url / + * @return / + */ + String get(String url); + + /** + * post 请求,form-data 格式参数 + * + * @param url / + * @param params / + * @return / + */ + String postByFormData(String url, Map params); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplateDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplateDefaultImpl.java new file mode 100644 index 000000000..400ae1ee0 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplateDefaultImpl.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.http; + +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.NotImplException; + +import java.util.Map; + +/** + * Http 请求处理器,默认实现类 + * + * @author click33 + * @since 1.43.0 + */ +public class SaHttpTemplateDefaultImpl implements SaHttpTemplate { + + public static final String ERROR_MESSAGE = "HTTP 请求处理器未实现"; + + /** + * get 请求 + * + * @param url / + * @return / + */ + @Override + public String get(String url) { + throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10004); + } + + /** + * post 请求,form-data 格式参数 + */ + @Override + public String postByFormData(String url, Map params) { + throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10004); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpUtil.java new file mode 100644 index 000000000..5a46a76ae --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.http; + +import cn.dev33.satoken.SaManager; + +import java.util.Map; + +/** + * Http 请求处理器 工具类 + * + * @author click33 + * @since 1.43.0 + */ +public class SaHttpUtil { + + /** + * get 请求 + * + * @param url / + * @return / + */ + public static String get(String url) { + return SaManager.getSaHttpTemplate().get(url); + } + + /** + * post 请求,form-data 格式参数 + * + * @param url / + * @param params / + * @return / + */ + public static String postByFormData(String url, Map params) { + return SaManager.getSaHttpTemplate().postByFormData(url, params); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicAccount.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicAccount.java new file mode 100644 index 000000000..70c48a5e6 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicAccount.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.httpauth.basic; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * Sa-Token Http Basic 账号 + * + * @author click33 + * @since 1.41.0 + */ +public class SaHttpBasicAccount { + + /** + * 账号 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 构造函数 + * @param username 账号 + * @param password 密码 + */ + public SaHttpBasicAccount(String username, String password) { + this.username = username; + this.password = password; + } + + /** + * 构造函数 + * @param usernameAndPassword 账号和密码,冒号隔开 + */ + public SaHttpBasicAccount(String usernameAndPassword) { + if(SaFoxUtil.isEmpty(usernameAndPassword)) { + throw new SaTokenException("UsernameAndPassword 不能为空"); + } + String[] arr = usernameAndPassword.split(":"); + if(arr.length != 2) { + throw new SaTokenException("UsernameAndPassword 格式错误,正确格式为:username:password"); + } + this.username = arr[0]; + this.password = arr[1]; + } + + /** + * 获取 账号 + * + * @return username 账号 + */ + public String getUsername() { + return this.username; + } + + /** + * 设置 账号 + * + * @param username 账号 + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * 获取 密码 + * + * @return password 密码 + */ + public String getPassword() { + return this.password; + } + + /** + * 设置 密码 + * + * @param password 密码 + */ + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "SaHttpBasicAccount{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + '}'; + } + +} 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 new file mode 100644 index 000000000..ae4ffa9fc --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicTemplate.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.httpauth.basic; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.NotHttpBasicAuthException; +import cn.dev33.satoken.secure.SaBase64Util; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * Sa-Token Http Basic 认证模块 + * + * @author click33 + * @since 1.26.0 + */ +public class SaHttpBasicTemplate { + + /** + * 默认的 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 NotHttpBasicAuthException().setCode(SaErrorCode.CODE_10311); + } + + /** + * 获取浏览器提交的 Http 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)); + } + + /** + * 获取 Http Basic 账号密码对象 + * @return / + */ + public SaHttpBasicAccount getHttpBasicAccount() { + String authorizationValue = getAuthorizationValue(); + if(authorizationValue == null) { + return null; + } + return new SaHttpBasicAccount(authorizationValue); + } + + /** + * 对当前会话进行 Basic 校验(使用全局配置的账号密码),校验不通过则抛出异常 + */ + public void check() { + check(DEFAULT_REALM, SaManager.getConfig().getHttpBasic()); + } + + /** + * 对当前会话进行 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().getHttpBasic(); + } + String authorization = getAuthorizationValue(); + if(SaFoxUtil.isEmpty(authorization) || ! authorization.equals(account)) { + throwNotBasicAuthException(realm); + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicUtil.java new file mode 100644 index 000000000..ad1eaeb73 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicUtil.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.httpauth.basic; + +/** + * Sa-Token Http Basic 认证模块,Util 工具类 + * + * @author click33 + * @since 1.26.0 + */ +public class SaHttpBasicUtil { + + private SaHttpBasicUtil() { + } + + /** + * 底层使用的 SaBasicTemplate 对象 + */ + public static SaHttpBasicTemplate saHttpBasicTemplate = new SaHttpBasicTemplate(); + + /** + * 获取浏览器提交的 Http Basic 参数 (裁剪掉前缀并解码) + * @return 值 + */ + public static String getAuthorizationValue() { + return saHttpBasicTemplate.getAuthorizationValue(); + } + + /** + * 获取 Http Basic 账号密码对象 + * @return / + */ + public static SaHttpBasicAccount getHttpBasicAccount() { + return saHttpBasicTemplate.getHttpBasicAccount(); + } + + /** + * 对当前会话进行 Basic 校验(使用全局配置的账号密码),校验不通过则抛出异常 + */ + public static void check() { + saHttpBasicTemplate.check(); + } + + /** + * 对当前会话进行 Basic 校验(手动设置账号密码),校验不通过则抛出异常 + * @param account 账号(格式为 user:password) + */ + public static void check(String account) { + saHttpBasicTemplate.check(account); + } + + /** + * 对当前会话进行 Basic 校验(手动设置 Realm 和 账号密码),校验不通过则抛出异常 + * @param realm 领域 + * @param account 账号(格式为 user:password) + */ + public static void check(String realm, String account) { + saHttpBasicTemplate.check(realm, account); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java new file mode 100644 index 000000000..5e956057b --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java @@ -0,0 +1,346 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.httpauth.digest; + +/** + * Sa-Token Http Digest 认证 - 参数实体类 + * + * @author click33 + * @since 1.38.0 + */ +public class SaHttpDigestModel { + + /** + * 默认的 Realm 领域名称 + */ + public static final String DEFAULT_REALM = "Sa-Token"; + + /** + * 默认的 qop 值 + */ + public static final String DEFAULT_QOP = "auth"; + + + /** + * 用户名 + */ + public String username; + + /** + * 密码 + */ + public String password; + + /** + * 领域 + */ + public String realm = DEFAULT_REALM; + + /** + * 随机数 + */ + public String nonce; + + /** + * 请求 uri + */ + public String uri; + + /** + * 请求方法 + */ + public String method; + + /** + * 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + */ + public String qop; + + /** + * nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + */ + public String nc; + + /** + * 客户端随机数,由客户端提供 + */ + public String cnonce; + + /** + * opaque + */ + public String opaque; + + /** + * 请求摘要,最终计算的摘要结果 + */ + public String response; + + // ------------------- 构造函数 ------------------- + + public SaHttpDigestModel() { + } + public SaHttpDigestModel(String username, String password) { + this.username = username; + this.password = password; + } + public SaHttpDigestModel(String username, String password, String realm) { + this.username = username; + this.password = password; + this.realm = realm; + } + + + // ------------------- get/set ------------------- + + /** + * 获取 用户名 + * + * @return username 用户名 + */ + public String getUsername() { + return this.username; + } + + /** + * 设置 用户名 + * + * @param username 用户名 + * @return / + */ + public SaHttpDigestModel setUsername(String username) { + this.username = username; + return this; + } + + /** + * 获取 领域 + * + * @return realm 领域 + */ + public String getRealm() { + return this.realm; + } + + /** + * 设置 领域 + * + * @param realm 领域 + * @return / + */ + public SaHttpDigestModel setRealm(String realm) { + this.realm = realm; + return this; + } + + /** + * 获取 密码 + * + * @return password 密码 + */ + public String getPassword() { + return this.password; + } + + /** + * 设置 密码 + * + * @param password 密码 + * @return / + */ + public SaHttpDigestModel setPassword(String password) { + this.password = password; + return this; + } + + /** + * 获取 随机数 + * + * @return nonce 随机数 + */ + public String getNonce() { + return this.nonce; + } + + /** + * 设置 随机数 + * + * @param nonce 随机数 + * @return / + */ + public SaHttpDigestModel setNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * 获取 请求 uri + * + * @return uri 请求 uri + */ + public String getUri() { + return this.uri; + } + + /** + * 设置 请求 uri + * + * @param uri 请求 uri + * @return / + */ + public SaHttpDigestModel setUri(String uri) { + this.uri = uri; + return this; + } + + /** + * 获取 请求方法 + * + * @return method 请求方法 + */ + public String getMethod() { + return this.method; + } + + /** + * 设置 请求方法 + * + * @param method 请求方法 + * @return / + */ + public SaHttpDigestModel setMethod(String method) { + this.method = method; + return this; + } + + /** + * 获取 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + * + * @return qop 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + */ + public String getQop() { + return this.qop; + } + + /** + * 设置 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + * + * @param qop 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + * @return / + */ + public SaHttpDigestModel setQop(String qop) { + this.qop = qop; + return this; + } + + /** + * 获取 nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + * + * @return nc nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + */ + public String getNc() { + return this.nc; + } + + /** + * 设置 nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + * + * @param nc nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + * @return / + */ + public SaHttpDigestModel setNc(String nc) { + this.nc = nc; + return this; + } + + /** + * 获取 客户端随机数,由客户端提供 + * + * @return cnonce 客户端随机数,由客户端提供 + */ + public String getCnonce() { + return this.cnonce; + } + + /** + * 设置 客户端随机数,由客户端提供 + * + * @param cnonce 客户端随机数,由客户端提供 + * @return / + */ + public SaHttpDigestModel setCnonce(String cnonce) { + this.cnonce = cnonce; + return this; + } + + /** + * 获取 opaque + * + * @return opaque opaque + */ + public String getOpaque() { + return this.opaque; + } + + /** + * 设置 opaque + * + * @param opaque opaque + * @return / + */ + public SaHttpDigestModel setOpaque(String opaque) { + this.opaque = opaque; + return this; + } + + /** + * 获取 请求摘要,最终计算的摘要结果 + * + * @return response 请求摘要,最终计算的摘要结果 + */ + public String getResponse() { + return this.response; + } + + /** + * 设置 请求摘要,最终计算的摘要结果 + * + * @param response 请求摘要,最终计算的摘要结果 + * @return / + */ + public SaHttpDigestModel setResponse(String response) { + this.response = response; + return this; + } + + @Override + public String toString() { + return "SaHttpDigestModel[" + + "username=" + username + + ", password=" + password + + ", realm=" + realm + + ", nonce=" + nonce + + ", uri=" + uri + + ", method=" + method + + ", qop=" + qop + + ", nc=" + nc + + ", cnonce=" + cnonce + + ", opaque=" + opaque + + ", response=" + response + + "]"; + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..97ecae467 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java @@ -0,0 +1,305 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.httpauth.digest; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.SaCheckHttpDigest; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.NotHttpDigestAuthException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.secure.SaSecureUtil; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sa-Token Http Digest 认证模块 - 模板方法类 + * + * @author click33 + * @since 1.38.0 + */ +public class SaHttpDigestTemplate { + + /* + 这里只是 Http Digest 认证的一个简单实现,待实现功能还有: + 1、nonce 防重放攻击 + 2、nc 计数器 + 3、qop 保护质量=auth-int + 4、opaque 透明值 + 5、algorithm 更多摘要算法 + 等等 + */ + + /** + * 构建认证失败的响应头参数 + * @param model 参数对象 + * @return 响应头值 + */ + public String buildResponseHeaderValue(SaHttpDigestModel model) { + // 抛异常 + String headerValue = "Digest " + + "realm=\"" + model.realm + "\", " + + "qop=\"" + model.qop + "\", " + + "nonce=\"" + model.nonce + "\", " + + "nc=" + model.nc + ", " + + "opaque=\"" + model.opaque + "\""; + return headerValue; + } + + /** + * 在校验失败时,设置响应头,并抛出异常 + * @param model Digest 参数对象 + */ + public void throwNotHttpDigestAuthException(SaHttpDigestModel model) { + // 补全一些必须的参数 + model.realm = (model.realm != null) ? model.realm : SaHttpDigestModel.DEFAULT_REALM; + model.qop = (model.qop != null) ? model.qop : SaHttpDigestModel.DEFAULT_QOP; + model.nonce = (model.nonce != null) ? model.nonce : SaFoxUtil.getRandomString(32); + model.opaque = (model.opaque != null) ? model.opaque : SaFoxUtil.getRandomString(32); + model.nc = (model.nc != null) ? model.nc : "00000001"; + + // 设置响应头 + SaHolder.getResponse() + .setStatus(401) + .setHeader("WWW-Authenticate", buildResponseHeaderValue(model)); + + // 抛异常 + throw new NotHttpDigestAuthException().setCode(SaErrorCode.CODE_10312); + } + + /** + * 获取浏览器提交的 Digest 参数 (裁剪掉前缀) + * @return 值 + */ + public String getAuthorizationValue() { + + // 获取前端提交的请求头 Authorization 参数 + String authorization = SaHolder.getRequest().getHeader("Authorization"); + + // 如果不是以 Digest 作为前缀,则视为无效 + if(authorization == null || ! authorization.startsWith("Digest ")) { + return null; + } + + // 裁剪前缀并解码 + return authorization.substring(7); + } + + /** + * 获取浏览器提交的 Digest 参数,并转化为 Map + * @return / + */ + public SaHttpDigestModel getAuthorizationValueToModel() { + + // 先获取字符串值 + String authorization = getAuthorizationValue(); + if(authorization == null) { +// throw new SaTokenException("请求头中未携带 Digest 认证参数"); + return null; + } + + // 根据逗号分割,解析为 Map + Map map = new LinkedHashMap<>(); + String[] arr = authorization.split(","); + for (String s : arr) { + String[] kv = s.split("="); + if (kv.length == 2) { + map.put(kv[0].trim(), kv[1].trim().replace("\"", "")); + } + // 兼容字符串包含多个=的情况,如:uri 带参数的问题 + // username="sa", realm="Sa-Token", nonce="IWlEwO23oCAbIAbHX1BYnX5ddKHUdsjW", uri="/test/testDigest?name=zhangsan&age=18", response="c4359210ccb23c985234ee6e02def88d", opaque="H6jPyjwfioc0oUbDE0OSmpX7wznfxxMo", qop=auth, nc=00000002, cnonce="46dd0073c981a9c7" + else if (s.contains("=")) { + map.put(kv[0].trim(), s.substring(kv[0].length() + 1).trim().replace("\"", "")); + } + } + + /* + 参考样例: + username=sa, + realm=Sa-Token, + nonce=dcd98b7102dd2f0e8b11d0f600bfb0c093, + uri=/test/testDigest, + response=a32023c128e142163dd4856a2f511c70, + opaque=5ccc069c403ebaf9f0171e9517f40e41, + qop=auth, + nc=00000002, + cnonce=f3ca6bfc0b2f59c4 + */ + + // 转化为 Model + SaHttpDigestModel model = new SaHttpDigestModel(); + model.username = map.get("username"); + model.realm = map.get("realm"); + model.nonce = map.get("nonce"); + model.uri = map.get("uri"); + model.method = SaHolder.getRequest().getMethod(); + model.qop = map.get("qop"); + model.nc = map.get("nc"); + model.cnonce = map.get("cnonce"); + model.opaque = map.get("opaque"); + model.response = map.get("response"); + + // + return model; + } + + /** + * 计算:根据 Digest 参数计算 response + * + * @param model Digest 参数对象 + * @return 计算出的 response + */ + public String calcResponse(SaHttpDigestModel model) { + + // frag1 = md5(username:realm:password) + String frag1 = SaSecureUtil.md5(model.username + ":" + model.realm + ":" + model.password); + + // frag2 = nonce:nc:cnonce:qop + String frag2 = model.nonce + ":" + model.nc + ":" + model.cnonce + ":" + model.qop; + + // frag3 = md5(method:uri) + String frag3 = SaSecureUtil.md5(model.method + ":" + model.uri); + + // 最终结果 = md5(frag1:frag2:frag3) + String response = SaSecureUtil.md5(frag1 + ":" + frag2 + ":" + frag3); + + // + return response; + } + + /** + * 把 hopeModel 有的值都 copy 到 reqModel 中 + */ + public void copyHopeToReq(SaHttpDigestModel hopeModel, SaHttpDigestModel reqModel){ + reqModel.username = hopeModel.username; + reqModel.password = hopeModel.password; + reqModel.realm = hopeModel.realm != null ? hopeModel.realm : reqModel.realm; + reqModel.nonce = hopeModel.nonce != null ? hopeModel.nonce : reqModel.nonce; + reqModel.uri = hopeModel.uri != null ? hopeModel.uri : reqModel.uri; + reqModel.method = hopeModel.method != null ? hopeModel.method : reqModel.method; + reqModel.qop = hopeModel.qop != null ? hopeModel.qop : reqModel.qop; + reqModel.nc = hopeModel.nc != null ? hopeModel.nc : reqModel.nc; + reqModel.opaque = hopeModel.opaque != null ? hopeModel.opaque : reqModel.opaque; + // reqModel.cnonce = hopeModel.cnonce != null ? hopeModel.cnonce : reqModel.cnonce; + // reqModel.response = hopeModel.response != null ? hopeModel.response : reqModel.response; + } + + // ---------- 校验 ---------- + + /** + * 校验:根据提供 Digest 参数计算 res,与 request 请求中的 Digest 参数进行校验,校验不通过则抛出异常 + * @param hopeModel 提供的 Digest 参数对象 + */ + public void check(SaHttpDigestModel hopeModel) { + + // 先进行一些必须的希望参数校验 + SaTokenException.notEmpty(hopeModel, "Digest参数对象不能为空"); + SaTokenException.notEmpty(hopeModel.username, "必须提供希望的 username 参数"); + SaTokenException.notEmpty(hopeModel.password, "必须提供希望的 password 参数"); + + // 获取 web 请求中的 Digest 参数 + SaHttpDigestModel reqModel = getAuthorizationValueToModel(); + + // 为空代表前端根本没有提交 Digest 参数,直接抛异常 + if(reqModel == null) { + throwNotHttpDigestAuthException(hopeModel); + } + + // 把 hopeModel 有的值都 copy 到 reqModel 中 + copyHopeToReq(hopeModel, reqModel); + + // 计算 + String cResponse = calcResponse(reqModel); + + // 比对,不一致就抛异常 + if(! cResponse.equals(reqModel.response)) { + throwNotHttpDigestAuthException(hopeModel); + } + + // 认证通过 + } + + /** + * 校验:根据提供的参数,校验不通过抛出异常 + * @param username 用户名 + * @param password 密码 + */ + public void check(String username, String password) { + check(new SaHttpDigestModel(username, password)); + } + + /** + * 校验:根据提供的参数,校验不通过抛出异常 + * @param username 用户名 + * @param password 密码 + * @param realm 领域 + */ + public void check(String username, String password, String realm) { + check(new SaHttpDigestModel(username, password, realm)); + } + + /** + * 校验:根据全局配置参数,校验不通过抛出异常 + */ + public void check() { + String httpDigest = SaManager.getConfig().getHttpDigest(); + if(SaFoxUtil.isEmpty(httpDigest)){ + throw new SaTokenException("未配置全局 Http Digest 认证参数"); + } + String[] arr = httpDigest.split(":"); + if(arr.length != 2){ + throw new SaTokenException("全局 Http Digest 认证参数配置错误,格式应如:username:password"); + } + check(arr[0], arr[1]); + } + + + + // ----------------- 过期方法 ----------------- + + /** + * 根据注解 ( @SaCheckHttpDigest ) 鉴权 + * + * @param at 注解对象 + */ + @Deprecated + public void checkByAnnotation(SaCheckHttpDigest at) { + + // 如果配置了 value,则以 value 优先 + String value = at.value(); + if(SaFoxUtil.isNotEmpty(value)){ + String[] arr = value.split(":"); + if(arr.length != 2){ + throw new SaTokenException("注解参数配置错误,格式应如:username:password"); + } + check(arr[0], arr[1]); + return; + } + + // 如果配置了 username,则分别获取参数 + String username = at.username(); + if(SaFoxUtil.isNotEmpty(username)){ + check(username, at.password(), at.realm()); + return; + } + + // 都没有配置,则根据全局配置参数进行校验 + check(); + } + +} 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 new file mode 100644 index 000000000..6fbfedda0 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.httpauth.digest; + +import cn.dev33.satoken.annotation.SaCheckHttpDigest; + +import java.util.Map; + +/** + * Sa-Token Http Digest 认证模块,Util 工具类 + * + * @author click33 + * @since 1.38.0 + */ +public class SaHttpDigestUtil { + + private SaHttpDigestUtil() { + } + + /** + * 底层使用的 SaHttpDigestTemplate 对象 + */ + public static SaHttpDigestTemplate saHttpDigestTemplate = new SaHttpDigestTemplate(); + + + /** + * 获取浏览器提交的 Digest 参数 (裁剪掉前缀) + * @return 值 + */ + public static String getAuthorizationValue() { + return saHttpDigestTemplate.getAuthorizationValue(); + } + + /** + * 获取浏览器提交的 Digest 参数,并转化为 Map + * @return / + */ + public static SaHttpDigestModel getAuthorizationValueToModel() { + return saHttpDigestTemplate.getAuthorizationValueToModel(); + } + + // ---------- 校验 ---------- + + /** + * 校验:根据提供 Digest 参数计算 res,与 request 请求中的 Digest 参数进行校验,校验不通过则抛出异常 + * @param hopeModel 提供的 Digest 参数对象 + */ + public static void check(SaHttpDigestModel hopeModel) { + saHttpDigestTemplate.check(hopeModel); + } + + /** + * 校验:根据提供的参数,校验不通过抛出异常 + * @param username 用户名 + * @param password 密码 + */ + public static void check(String username, String password) { + saHttpDigestTemplate.check(username, password); + } + + /** + * 校验:根据提供的参数,校验不通过抛出异常 + * @param username 用户名 + * @param password 密码 + * @param realm 领域 + */ + public static void check(String username, String password, String realm) { + saHttpDigestTemplate.check(username, password, realm); + } + + /** + * 校验:根据全局配置参数,校验不通过抛出异常 + */ + public static void check() { + 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/id/SaIdTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/id/SaIdTemplate.java deleted file mode 100644 index 9381bdb84..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/id/SaIdTemplate.java +++ /dev/null @@ -1,172 +0,0 @@ -package cn.dev33.satoken.id; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.context.SaHolder; -import cn.dev33.satoken.exception.IdTokenInvalidException; -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * Sa-Token-Id 身份凭证模块 - *

身份凭证的获取与校验,可用于微服务内部调用鉴权 - * @author kong - * - */ -public class SaIdTemplate { - - /** - * 在 Request 上储存 Id-Token 时建议使用的key - */ - public static final String ID_TOKEN = "SA_ID_TOKEN"; - - // -------------------- 获取 & 校验 - - /** - * 获取当前Id-Token, 如果不存在,则立即创建并返回 - * @return 当前token - */ - public String getToken() { - String currentToken = getTokenNh(); - if(SaFoxUtil.isEmpty(currentToken)) { - // 注意这里的自刷新不能做到高并发可用 - currentToken = refreshToken(); - } - return currentToken; - } - - /** - * 判断一个Id-Token是否有效 - * @param token 要验证的token - * @return 这个token是否有效 - */ - public boolean isValid(String token) { - // 1、 如果传入的token未空,立即返回false - if(SaFoxUtil.isEmpty(token)) { - return false; - } - - // 2、 验证当前 Id-Token 及 Past-Id-Token - return token.equals(getToken()) || token.equals(getPastTokenNh()); - } - - /** - * 校验一个Id-Token是否有效 (如果无效则抛出异常) - * @param token 要验证的token - */ - public void checkToken(String token) { - if(isValid(token) == false) { - token = (token == null ? "" : token); - throw new IdTokenInvalidException("无效Id-Token:" + token); - } - } - - /** - * 校验当前Request提供的Id-Token是否有效 (如果无效则抛出异常) - */ - public void checkCurrentRequestToken() { - checkToken(SaHolder.getRequest().getHeader(ID_TOKEN)); - } - - /** - * 刷新一次Id-Token (注意集群环境中不要多个服务重复调用) - * @return 新Token - */ - public String refreshToken() { - - // 1. 先将当前 Id-Token 写入到 Past-Id-Token 中 - String idToken = getTokenNh(); - if(SaFoxUtil.isEmpty(idToken) == false) { - savePastToken(idToken, getTokenTimeout()); - } - - // 2. 再刷新当前Id-Token - String newIdToken = createToken(); - saveToken(newIdToken); - - // 3. 返回新的 Id-Token - return newIdToken; - } - - - // ------------------------------ 保存Token - - /** - * 保存Id-Token - * @param token - */ - public void saveToken(String token) { - if(SaFoxUtil.isEmpty(token)) { - return; - } - SaManager.getSaTokenDao().set(splicingTokenSaveKey(), token, SaManager.getConfig().getIdTokenTimeout()); - } - - /** - * 保存Past-Id-Token - * @param token token - * @param timeout 有效期(单位:秒) - */ - public void savePastToken(String token, long timeout){ - if(SaFoxUtil.isEmpty(token)) { - return; - } - SaManager.getSaTokenDao().set(splicingPastTokenSaveKey(), token, timeout); - } - - - // -------------------- 获取Token - - /** - * 获取Id-Token,不做任何处理 - * @return token - */ - public String getTokenNh() { - return SaManager.getSaTokenDao().get(splicingTokenSaveKey()); - } - - /** - * 获取Past-Id-Token,不做任何处理 - * @return token - */ - public String getPastTokenNh() { - return SaManager.getSaTokenDao().get(splicingPastTokenSaveKey()); - } - - /** - * 获取Id-Token的剩余有效期 (单位:秒) - * @return token - */ - public long getTokenTimeout() { - return SaManager.getSaTokenDao().getTimeout(splicingTokenSaveKey()); - } - - - // -------------------- 创建Token - - /** - * 创建一个Id-Token - * @return Token - */ - public String createToken() { - return SaFoxUtil.getRandomString(60); - } - - - // -------------------- 拼接key - - /** - * 拼接key:Id-Token的存储key - * @return key - */ - public String splicingTokenSaveKey() { - return SaManager.getConfig().getTokenName() + ":var:id-token"; - } - - /** - * 拼接key:Id-Token的存储key - * @return key - */ - public String splicingPastTokenSaveKey() { - return SaManager.getConfig().getTokenName() + ":var:past-id-token"; - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/id/SaIdUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/id/SaIdUtil.java deleted file mode 100644 index 79c6c325b..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/id/SaIdUtil.java +++ /dev/null @@ -1,81 +0,0 @@ -package cn.dev33.satoken.id; - -/** - * Sa-Token-Id 身份凭证模块-工具类 - * @author kong - * - */ -public class SaIdUtil { - - /** - * 在 Request 上储存 Id-Token 时建议使用的key - */ - public static final String ID_TOKEN = SaIdTemplate.ID_TOKEN; - - /** - * 底层 SaIdTemplate 对象 - */ - public static SaIdTemplate saIdTemplate = new SaIdTemplate(); - - // -------------------- 获取 & 校验 - - /** - * 获取当前Id-Token, 如果不存在,则立即创建并返回 - * @return 当前token - */ - public static String getToken() { - return saIdTemplate.getToken(); - } - - /** - * 判断一个Id-Token是否有效 - * @param token 要验证的token - * @return 这个token是否有效 - */ - public static boolean isValid(String token) { - return saIdTemplate.isValid(token); - } - - /** - * 校验一个Id-Token是否有效 (如果无效则抛出异常) - * @param token 要验证的token - */ - public static void checkToken(String token) { - saIdTemplate.checkToken(token); - } - - /** - * 校验当前Request提供的Id-Token是否有效 (如果无效则抛出异常) - */ - public static void checkCurrentRequestToken() { - saIdTemplate.checkCurrentRequestToken(); - } - - /** - * 刷新一次Id-Token (注意集群环境中不要多个服务重复调用) - * @return 新Token - */ - public static String refreshToken() { - return saIdTemplate.refreshToken(); - } - - - // -------------------- 获取Token - - /** - * 获取Id-Token,不做任何处理 - * @return token - */ - public static String getTokenNh() { - return saIdTemplate.getTokenNh(); - } - - /** - * 获取Past-Id-Token,不做任何处理 - * @return token - */ - public static String getPastTokenNh() { - return saIdTemplate.getPastTokenNh(); - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java new file mode 100644 index 000000000..93f4c7a09 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.json; + +import java.util.Map; + +/** + * JSON 转换器 + * + * @author click33 + * @since 1.30.0 + */ +public interface SaJsonTemplate { + + /** + * 序列化:对象 -> json 字符串 + * + * @param obj / + * @return / + */ + String objectToJson(Object obj); + + /** + * 反序列化:json 字符串 → 对象 + * + * @param jsonStr / + * @param type / + * @return / + * @param / + */ + T jsonToObject(String jsonStr, Class type); + + /** + * 反序列化:json 字符串 → 对象 (自动判断类型) + * + * @param jsonStr / + * @return / + */ + default Object jsonToObject(String jsonStr) { + return jsonToObject(jsonStr, Object.class); + }; + + /** + * 反序列化:json 字符串 → Map + * + * @param jsonStr / + * @return / + */ + default Map jsonToMap(String jsonStr) { + return jsonToObject(jsonStr, Map.class); + }; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplateDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplateDefaultImpl.java new file mode 100644 index 000000000..63cf6d112 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplateDefaultImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.json; + +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.NotImplException; + +import java.util.Map; + +/** + * JSON 转换器,默认实现类 + * + *

如果代码断点走到了此默认实现类,说明框架没有注入有效的 JSON 转换器,需要开发者自行实现并注入

+ * + * @author click33 + * @since 1.30.0 + */ +public class SaJsonTemplateDefaultImpl implements SaJsonTemplate { + + public static final String ERROR_MESSAGE = "未实现具体的 json 转换器"; + + @Override + public String objectToJson(Object obj) { + throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003); + } + + @Override + public Object jsonToObject(String jsonStr) { + throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003); + } + + @Override + public T jsonToObject(String jsonStr, Class type) { + throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003); + } + + @Override + public Map jsonToMap(String jsonStr) { + throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003); + } + +} 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 new file mode 100644 index 000000000..b98070f6b --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenEventCenter.java @@ -0,0 +1,322 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.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; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.StpLogic; + +/** + * Sa-Token 事件中心 事件发布器 + * + *

提供侦听器注册、事件发布能力

+ * + * @author click33 + * @since 1.31.0 + */ +public class SaTokenEventCenter { + + // --------- 注册侦听器 + + private static List listenerList = new ArrayList<>(); + + static { + // 默认添加控制台日志侦听器 + listenerList.add(new SaTokenListenerForLog()); + } + + /** + * 获取已注册的所有侦听器 + * @return / + */ + public static List getListenerList() { + return listenerList; + } + + /** + * 重置侦听器集合 + * @param listenerList / + */ + public static void setListenerList(List listenerList) { + if(listenerList == null) { + throw new SaTokenException("重置的侦听器集合不可以为空").setCode(SaErrorCode.CODE_10031); + } + SaTokenEventCenter.listenerList = listenerList; + } + + /** + * 注册一个侦听器 + * @param listener / + */ + public static void registerListener(SaTokenListener listener) { + if(listener == null) { + throw new SaTokenException("注册的侦听器不可以为空").setCode(SaErrorCode.CODE_10032); + } + listenerList.add(listener); + } + + /** + * 注册一组侦听器 + * @param listenerList / + */ + public static void registerListenerList(List listenerList) { + if(listenerList == null) { + throw new SaTokenException("注册的侦听器集合不可以为空").setCode(SaErrorCode.CODE_10031); + } + for (SaTokenListener listener : listenerList) { + if(listener == null) { + throw new SaTokenException("注册的侦听器不可以为空").setCode(SaErrorCode.CODE_10032); + } + } + SaTokenEventCenter.listenerList.addAll(listenerList); + } + + /** + * 移除一个侦听器 + * @param listener / + */ + public static void removeListener(SaTokenListener listener) { + listenerList.remove(listener); + } + + /** + * 移除指定类型的所有侦听器 + * @param cls / + */ + public static void removeListener(Class cls) { + ArrayList listenerListCopy = new ArrayList<>(listenerList); + for (SaTokenListener listener : listenerListCopy) { + if(cls.isAssignableFrom(listener.getClass())) { + listenerList.remove(listener); + } + } + } + + /** + * 清空所有已注册的侦听器 + */ + public static void clearListener() { + listenerList.clear(); + } + + /** + * 判断是否已经注册了指定侦听器 + * @param listener / + * @return / + */ + public static boolean hasListener(SaTokenListener listener) { + return listenerList.contains(listener); + } + + /** + * 判断是否已经注册了指定类型的侦听器 + * @param cls / + * @return / + */ + public static boolean hasListener(Class cls) { + for (SaTokenListener listener : listenerList) { + if(cls.isAssignableFrom(listener.getClass())) { + return true; + } + } + return false; + } + + + // --------- 事件发布 + + /** + * 事件发布:xx 账号登录 + * @param loginType 账号类别 + * @param loginId 账号id + * @param tokenValue 本次登录产生的 token 值 + * @param loginParameter 登录参数 + */ + public static void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { + for (SaTokenListener listener : listenerList) { + listener.doLogin(loginType, loginId, tokenValue, loginParameter); + } + } + + /** + * 事件发布:xx 账号注销 + * @param loginType 账号类别 + * @param loginId 账号id + * @param tokenValue token值 + */ + public static void doLogout(String loginType, Object loginId, String tokenValue) { + for (SaTokenListener listener : listenerList) { + listener.doLogout(loginType, loginId, tokenValue); + } + } + + /** + * 事件发布:xx 账号被踢下线 + * @param loginType 账号类别 + * @param loginId 账号id + * @param tokenValue token值 + */ + public static void doKickout(String loginType, Object loginId, String tokenValue) { + for (SaTokenListener listener : listenerList) { + listener.doKickout(loginType, loginId, tokenValue); + } + } + + /** + * 事件发布:xx 账号被顶下线 + * @param loginType 账号类别 + * @param loginId 账号id + * @param tokenValue token值 + */ + public static void doReplaced(String loginType, Object loginId, String tokenValue) { + for (SaTokenListener listener : listenerList) { + listener.doReplaced(loginType, loginId, tokenValue); + } + } + + /** + * 事件发布:xx 账号被封禁 + * @param loginType 账号类别 + * @param loginId 账号id + * @param service 指定服务 + * @param level 封禁等级 + * @param disableTime 封禁时长,单位: 秒 + */ + public static void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { + for (SaTokenListener listener : listenerList) { + listener.doDisable(loginType, loginId, service, level, disableTime); + } + } + + /** + * 事件发布:xx 账号被解封 + * @param loginType 账号类别 + * @param loginId 账号id + * @param service 指定服务 + */ + public static void doUntieDisable(String loginType, Object loginId, String service) { + for (SaTokenListener listener : listenerList) { + listener.doUntieDisable(loginType, loginId, service); + } + } + + /** + * 事件发布:xx 账号完成二级认证 + * @param loginType 账号类别 + * @param tokenValue token值 + * @param service 指定服务 + * @param safeTime 认证时间,单位:秒 + */ + public static void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { + for (SaTokenListener listener : listenerList) { + listener.doOpenSafe(loginType, tokenValue, service, safeTime); + } + } + + /** + * 事件发布:xx 账号关闭二级认证 + * @param loginType 账号类别 + * @param service 指定服务 + * @param tokenValue token值 + */ + public static void doCloseSafe(String loginType, String tokenValue, String service) { + for (SaTokenListener listener : listenerList) { + listener.doCloseSafe(loginType, tokenValue, service); + } + } + + /** + * 事件发布:创建了一个新的 SaSession + * @param id SessionId + */ + public static void doCreateSession(String id) { + for (SaTokenListener listener : listenerList) { + listener.doCreateSession(id); + } + } + + /** + * 事件发布:一个 SaSession 注销了 + * @param id SessionId + */ + public static void doLogoutSession(String id) { + for (SaTokenListener listener : listenerList) { + listener.doLogoutSession(id); + } + } + + /** + * 每次 Token 续期时触发(注意:是 timeout 续期,而不是 active-timeout 续期) + * + * @param loginType 账号类别 + * @param loginId 账号id + * @param tokenValue token 值 + * @param timeout 续期时间 + */ + public static void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { + for (SaTokenListener listener : listenerList) { + listener.doRenewTimeout(loginType, loginId, tokenValue, timeout); + } + } + + /** + * 事件发布:有新的全局组件载入到框架中 + * @param compName 组件名称 + * @param compObj 组件对象 + */ + public static void doRegisterComponent(String compName, Object compObj) { + for (SaTokenListener listener : listenerList) { + listener.doRegisterComponent(compName, compObj); + } + } + + /** + * 事件发布:有新的注解处理器载入到框架中 + * @param handler 注解处理器 + */ + public static void doRegisterAnnotationHandler(SaAnnotationHandlerInterface handler) { + for (SaTokenListener listener : listenerList) { + listener.doRegisterAnnotationHandler(handler); + } + } + + /** + * 事件发布:有新的 StpLogic 载入到框架中 + * @param stpLogic / + */ + public static void doSetStpLogic(StpLogic stpLogic) { + for (SaTokenListener listener : listenerList) { + listener.doSetStpLogic(stpLogic); + } + } + + /** + * 事件发布:有新的全局配置载入到框架中 + * @param config / + */ + public static void doSetConfig(SaTokenConfig config) { + for (SaTokenListener listener : listenerList) { + listener.doSetConfig(config); + } + } + +} 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 e3e4e96f2..cfe2eb221 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 @@ -1,12 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.listener; -import cn.dev33.satoken.stp.SaLoginModel; +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.StpLogic; /** * Sa-Token 侦听器 - *

你可以通过实现此接口在用户登陆、退出等关键性操作时进行一些AOP操作 - * @author kong * + *

你可以通过实现此接口在用户登录、退出等关键性操作时进行一些AOP切面操作

+ * + * @author click33 + * @since 1.17.0 */ public interface SaTokenListener { @@ -14,9 +34,10 @@ public interface SaTokenListener { * 每次登录时触发 * @param loginType 账号类别 * @param loginId 账号id - * @param loginModel 登录参数 + * @param tokenValue 本次登录产生的 token 值 + * @param loginParameter 登录参数 */ - public void doLogin(String loginType, Object loginId, SaLoginModel loginModel); + void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter); /** * 每次注销时触发 @@ -24,51 +45,104 @@ public interface SaTokenListener { * @param loginId 账号id * @param tokenValue token值 */ - public void doLogout(String loginType, Object loginId, String tokenValue); + void doLogout(String loginType, Object loginId, String tokenValue); /** - * 每次被踢下线时触发 - * @param loginType 账号类别 - * @param loginId 账号id - * @param tokenValue token值 - * @param device 设备标识 + * 每次被踢下线时触发 + * @param loginType 账号类别 + * @param loginId 账号id + * @param tokenValue token值 */ - public void doLogoutByLoginId(String loginType, Object loginId, String tokenValue, String device); + void doKickout(String loginType, Object loginId, String tokenValue); /** * 每次被顶下线时触发 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token值 - * @param device 设备标识 */ - public void doReplaced(String loginType, Object loginId, String tokenValue, String device); + void doReplaced(String loginType, Object loginId, String tokenValue); /** * 每次被封禁时触发 * @param loginType 账号类别 * @param loginId 账号id + * @param service 指定服务 + * @param level 封禁等级 * @param disableTime 封禁时长,单位: 秒 */ - public void doDisable(String loginType, Object loginId, long disableTime); + void doDisable(String loginType, Object loginId, String service, int level, long disableTime); /** * 每次被解封时触发 * @param loginType 账号类别 * @param loginId 账号id + * @param service 指定服务 */ - public void doUntieDisable(String loginType, Object loginId); - + void doUntieDisable(String loginType, Object loginId, String service); + + /** + * 每次打开二级认证时触发 + * @param loginType 账号类别 + * @param tokenValue token值 + * @param service 指定服务 + * @param safeTime 认证时间,单位:秒 + */ + void doOpenSafe(String loginType, String tokenValue, String service, long safeTime); + + /** + * 每次关闭二级认证时触发 + * @param loginType 账号类别 + * @param tokenValue token值 + * @param service 指定服务 + */ + void doCloseSafe(String loginType, String tokenValue, String service); + /** - * 每次创建Session时触发 + * 每次创建 SaSession 时触发 * @param id SessionId */ - public void doCreateSession(String id); + void doCreateSession(String id); /** - * 每次注销Session时触发 + * 每次注销 SaSession 时触发 * @param id SessionId */ - public void doLogoutSession(String id); - + void doLogoutSession(String id); + + /** + * 每次 Token 续期时触发(注意:是 timeout 续期,而不是 active-timeout 续期) + * + * @param loginType 账号类别 + * @param loginId 账号id + * @param tokenValue token 值 + * @param timeout 续期时间 + */ + void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout); + + /** + * 全局组件载入 + * @param compName 组件名称 + * @param compObj 组件对象 + */ + default void doRegisterComponent(String compName, Object compObj) {} + + /** + * 注册了自定义注解处理器 + * @param handler 注解处理器 + */ + default void doRegisterAnnotationHandler(SaAnnotationHandlerInterface handler) {} + + /** + * StpLogic 对象替换 + * @param stpLogic / + */ + default void doSetStpLogic(StpLogic stpLogic) {} + + /** + * 载入全局配置 + * @param config / + */ + default void doSetConfig(SaTokenConfig config) {} + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerDefaultImpl.java deleted file mode 100644 index c995b84c5..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerDefaultImpl.java +++ /dev/null @@ -1,96 +0,0 @@ -package cn.dev33.satoken.listener; - -import java.util.Date; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.stp.SaLoginModel; -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * Sa-Token 侦听器的默认实现:log打印 - * @author kong - * - */ -public class SaTokenListenerDefaultImpl implements SaTokenListener { - - /** - * 每次登录时触发 - */ - @Override - public void doLogin(String loginType, Object loginId, SaLoginModel loginModel) { - println("账号[" + loginId + "]登录成功"); - } - - /** - * 每次注销时触发 - */ - @Override - public void doLogout(String loginType, Object loginId, String tokenValue) { - println("账号[" + loginId + "]注销成功"); - } - - /** - * 每次被踢下线时触发 - */ - @Override - public void doLogoutByLoginId(String loginType, Object loginId, String tokenValue, String device) { - println("账号[" + loginId + "]被踢下线 (终端: " + device + ")"); - } - - /** - * 每次被顶下线时触发 - */ - @Override - public void doReplaced(String loginType, Object loginId, String tokenValue, String device) { - println("账号[" + loginId + "]被顶下线 (终端: " + device + ")"); - } - - /** - * 每次被封禁时触发 - */ - @Override - public void doDisable(String loginType, Object loginId, long disableTime) { - Date date = new Date(System.currentTimeMillis() + disableTime * 1000); - println("账号[" + loginId + "]被封禁 (解封时间: " + SaFoxUtil.formatDate(date) + ")"); - } - - /** - * 每次被解封时触发 - */ - @Override - public void doUntieDisable(String loginType, Object loginId) { - println("账号[" + loginId + "]被解除封禁"); - } - - /** - * 每次创建Session时触发 - */ - @Override - public void doCreateSession(String id) { - println("Session[" + id + "]创建成功"); - } - - /** - * 每次注销Session时触发 - */ - @Override - public void doLogoutSession(String id) { - println("Session[" + id + "]注销成功"); - } - - /** - * 日志输出的前缀 - */ - public static final String LOG_PREFIX = "SaLog -->: "; - - /** - * 打印指定字符串 - * @param str 字符串 - */ - public void println(String str) { - if(SaManager.getConfig().getIsLog()) { - System.out.println(LOG_PREFIX + str); - } - } - -} 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 new file mode 100644 index 000000000..a4dd029f6 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java @@ -0,0 +1,166 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.listener; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaFoxUtil; + +import static cn.dev33.satoken.SaManager.log; + +/** + * Sa-Token 侦听器的一个实现:Log 打印 + * + * @author click33 + * @since 1.33.0 + */ +public class SaTokenListenerForLog implements SaTokenListener { + + /** + * 每次登录时触发 + */ + @Override + public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { + log.info("账号 {} 登录成功 (loginType={}), 会话凭证 token={}", loginId, loginType, tokenValue); + } + + /** + * 每次注销时触发 + */ + @Override + public void doLogout(String loginType, Object loginId, String tokenValue) { + log.info("账号 {} 注销登录 (loginType={}), 会话凭证 token={}", loginId, loginType, tokenValue); + } + + /** + * 每次被踢下线时触发 + */ + @Override + public void doKickout(String loginType, Object loginId, String tokenValue) { + log.info("账号 {} 被踢下线 (loginType={}), 会话凭证 token={}", loginId, loginType, tokenValue); + } + + /** + * 每次被顶下线时触发 + */ + @Override + public void doReplaced(String loginType, Object loginId, String tokenValue) { + log.info("账号 {} 被顶下线 (loginType={}), 会话凭证 token={}", loginId, loginType, tokenValue); + } + + /** + * 每次被封禁时触发 + */ + @Override + public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { + log.info("账号 {} [{}服务] 被封禁 (loginType={}), 封禁等级={}, 解封时间为 {}", loginId, loginType, service, level, SaFoxUtil.formatAfterDate(disableTime * 1000)); + } + + /** + * 每次被解封时触发 + */ + @Override + public void doUntieDisable(String loginType, Object loginId, String service) { + log.info("账号 {} [{}服务] 解封成功 (loginType={})", loginId, service, loginType); + } + + /** + * 每次打开二级认证时触发 + */ + @Override + public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { + log.info("token 二级认证成功, 业务标识={}, 有效期={}秒, Token值={}", service, safeTime, tokenValue); + } + + /** + * 每次关闭二级认证时触发 + */ + @Override + public void doCloseSafe(String loginType, String tokenValue, String service) { + log.info("token 二级认证关闭, 业务标识={}, Token值={}", service, tokenValue); + } + + /** + * 每次创建Session时触发 + */ + @Override + public void doCreateSession(String id) { + log.info("SaSession [{}] 创建成功", id); + } + + /** + * 每次注销Session时触发 + */ + @Override + public void doLogoutSession(String id) { + log.info("SaSession [{}] 注销成功", id); + } + + /** + * 每次 Token 续期时触发 + */ + @Override + public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { + log.info("token 续期成功, {} 秒后到期, 账号={}, token值={} ", timeout, loginId, tokenValue); + } + + /** + * 全局组件载入 + * @param compName 组件名称 + * @param compObj 组件对象 + */ + @Override + public void doRegisterComponent(String compName, Object compObj) { + String canonicalName = compObj == null ? null : compObj.getClass().getCanonicalName(); + 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 / + */ + @Override + public void doSetStpLogic(StpLogic stpLogic) { + if(stpLogic != null) { + log.info("会话组件 StpLogic(type={}) 重置成功: {}", stpLogic.getLoginType(), stpLogic.getClass()); + } + } + + /** + * 载入全局配置 + * @param config / + */ + @Override + public void doSetConfig(SaTokenConfig config) { + if(config != null) { + log.info("全局配置 {} ", config); + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForSimple.java b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForSimple.java new file mode 100644 index 000000000..1021afc79 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForSimple.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.listener; + +import cn.dev33.satoken.stp.parameter.SaLoginParameter; + +/** + * Sa-Token 侦听器,默认空实现 + * + *

对所有事件方法提供空实现,方便开发者通过继承此类快速实现一个可用的侦听器

+ * + * @author click33 + * @since 1.31.0 + */ +public class SaTokenListenerForSimple implements SaTokenListener { + + @Override + public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { + + } + + @Override + public void doLogout(String loginType, Object loginId, String tokenValue) { + + } + + @Override + public void doKickout(String loginType, Object loginId, String tokenValue) { + + } + + @Override + public void doReplaced(String loginType, Object loginId, String tokenValue) { + + } + + @Override + public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { + + } + + @Override + public void doUntieDisable(String loginType, Object loginId, String service) { + + } + + @Override + public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { + + } + + @Override + public void doCloseSafe(String loginType, String tokenValue, String service) { + + } + + @Override + public void doCreateSession(String id) { + + } + + @Override + public void doLogoutSession(String id) { + + } + + @Override + public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { + + } + + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/log/SaLog.java b/sa-token-core/src/main/java/cn/dev33/satoken/log/SaLog.java new file mode 100644 index 000000000..c663ff979 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/log/SaLog.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.log; + +/** + * Sa-Token 日志输出接口 + * + * @author click33 + * @since 1.33.0 + */ +public interface SaLog { + + /** + * 输出 trace 日志 + * @param str 日志内容 + * @param args 参数列表 + */ + void trace(String str, Object ...args); + + /** + * 输出 debug 日志 + * @param str 日志内容 + * @param args 参数列表 + */ + void debug(String str, Object ...args); + + /** + * 输出 info 日志 + * @param str 日志内容 + * @param args 参数列表 + */ + void info(String str, Object ...args); + + /** + * 输出 warn 日志 + * @param str 日志内容 + * @param args 参数列表 + */ + void warn(String str, Object ...args); + + /** + * 输出 error 日志 + * @param str 日志内容 + * @param args 参数列表 + */ + void error(String str, Object ...args); + + /** + * 输出 fatal 日志 + * @param str 日志内容 + * @param args 参数列表 + */ + void fatal(String str, Object ...args); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/log/SaLogForConsole.java b/sa-token-core/src/main/java/cn/dev33/satoken/log/SaLogForConsole.java new file mode 100644 index 000000000..74e8ce919 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/log/SaLogForConsole.java @@ -0,0 +1,142 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.log; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.util.StrFormatter; + +/** + * Sa-Token 日志实现类 [ 控制台打印 ] + * + * @author click33 + * @since 1.33.0 + */ +public class SaLogForConsole implements SaLog { + + /** + * 日志等级 + */ + public static final int trace = 1; + public static final int debug = 2; + public static final int info = 3; + public static final int warn = 4; + public static final int error = 5; + public static final int fatal = 6; + + /** + * 日志输出的前缀 + */ + public static String LOG_PREFIX = "SaLog -->: "; + public static String TRACE_PREFIX = "SA [TRACE]-->: "; + public static String DEBUG_PREFIX = "SA [DEBUG]-->: "; + public static String INFO_PREFIX = "SA [INFO] -->: "; + public static String WARN_PREFIX = "SA [WARN] -->: "; + public static String ERROR_PREFIX = "SA [ERROR]-->: "; + public static String FATAL_PREFIX = "SA [FATAL]-->: "; + + /** + * 日志输出的颜色 + */ + public static String TRACE_COLOR = "\033[39m"; + public static String DEBUG_COLOR = "\033[34m"; + public static String INFO_COLOR = "\033[32m"; + public static String WARN_COLOR = "\033[33m"; + public static String ERROR_COLOR = "\033[31m"; + public static String FATAL_COLOR = "\033[35m"; + + public static String DEFAULT_COLOR = "\033[39m"; + + @Override + public void trace(String str, Object... args) { + println(trace, TRACE_COLOR, TRACE_PREFIX, str, args); + } + + @Override + public void debug(String str, Object... args) { + println(debug, DEBUG_COLOR, DEBUG_PREFIX, str, args); + } + + @Override + public void info(String str, Object... args) { + println(info, INFO_COLOR, INFO_PREFIX, str, args); + } + + @Override + public void warn(String str, Object... args) { + println(warn, WARN_COLOR, WARN_PREFIX, str, args); + } + + @Override + public void error(String str, Object... args) { + println(error, ERROR_COLOR, ERROR_PREFIX, str, args); + } + + @Override + public void fatal(String str, Object... args) { + println(fatal, FATAL_COLOR, FATAL_PREFIX, str, args); + } + + /** + * 打印日志到控制台 + * @param level 日志等级 + * @param color 颜色编码 + * @param prefix 前缀 + * @param str 字符串 + * @param args 参数列表 + */ + public void println(int level, String color, String prefix, String str, Object... args) { + SaTokenConfig config = SaManager.getConfig(); + if(config.getIsLog() && level >= config.getLogLevelInt()) { + if(config.getIsColorLog() == Boolean.TRUE) { + // 彩色日志 + System.out.println(color + prefix + StrFormatter.format(str, args) + DEFAULT_COLOR); + } else { + // 黑白日志 + System.out.println(prefix + StrFormatter.format(str, args)); + } + } + } + + /* + // 三种写法速度对比 + // if( config.getIsColorLog() != null && config.getIsColorLog() ) 10亿次,2058ms + // if( config.getIsColorLog() == Boolean.TRUE ) 10亿次,1050ms 最快 + // if( Objects.equals(config.getIsColorLog(), Boolean.TRUE) ) 10亿次,1543ms + */ + + /* + 颜色参考: + DEFAULT 39 + BLACK 30 + RED 31 + GREEN 32 + YELLOW 33 + BLUE 34 + MAGENTA 35 + CYAN 36 + WHITE 37 + BRIGHT_BLACK 90 + BRIGHT_RED 91 + BRIGHT_GREEN 92 + BRIGHT_YELLOW 93 + BRIGHT_BLUE 94 + BRIGHT_MAGENTA 95 + BRIGHT_CYAN 96 + BRIGHT_WHITE 97 + */ + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/model/wrapperInfo/SaDisableWrapperInfo.java b/sa-token-core/src/main/java/cn/dev33/satoken/model/wrapperInfo/SaDisableWrapperInfo.java new file mode 100644 index 000000000..57f85f3e8 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/model/wrapperInfo/SaDisableWrapperInfo.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.model.wrapperInfo; + +import cn.dev33.satoken.util.SaTokenConsts; + +/** + * 返回值包装类:描述一个账号是否已被封禁等信息 + * + * @author click33 + * @since 1.40.0 + */ +public class SaDisableWrapperInfo { + + /** + * 是否被封禁 + */ + public boolean isDisable; + + /** + * 封禁剩余时间,单位:秒(-1=永久封禁,0 or -2=未封禁) + */ + public long disableTime; + + /** + * 封禁等级(最小1级,0=未封禁) + */ + public int disableLevel; + + /** + * 构建对象 + * + * @param isDisable 是否被封禁 + * @param disableTime 封禁剩余时间,单位:秒(-1=永久封禁,0 or -2=未封禁) + * @param disableLevel 封禁等级(最小1级,0=未封禁) + */ + public SaDisableWrapperInfo(boolean isDisable, long disableTime, int disableLevel) { + this.isDisable = isDisable; + this.disableTime = disableTime; + this.disableLevel = disableLevel; + } + + /** + * 创建一个已封禁描述对象 + * @param disableTime 封禁时间 + * @param disableLevel 封禁等级 + * @return / + */ + public static SaDisableWrapperInfo createDisabled(long disableTime, int disableLevel) { + return new SaDisableWrapperInfo(true, disableTime, disableLevel); + } + + /** + * 创建一个未封禁描述对象 + * @return / + */ + public static SaDisableWrapperInfo createNotDisabled() { + return new SaDisableWrapperInfo(false, 0, SaTokenConsts.NOT_DISABLE_LEVEL); + } + + /** + * 创建一个未封禁描述对象,并指定缓存时间,指定时间内不再重复查询 + * @param cacheTime 缓存时间(单位:秒) + * @return / + */ + public static SaDisableWrapperInfo createNotDisabled(long cacheTime) { + return new SaDisableWrapperInfo(false, cacheTime, SaTokenConsts.NOT_DISABLE_LEVEL); + } + + @Override + public String toString() { + return "SaDisableWrapperInfo{" + + "isDisable=" + isDisable + + ", disableTime=" + disableTime + + ", disableLevel=" + disableLevel + + '}'; + } + + // setter / getter 仅为兼容部分框架序列化操作,不建议调用 + + public boolean getIsDisable() { + return isDisable; + } + + public SaDisableWrapperInfo setIsDisable(boolean isDisable) { + this.isDisable = isDisable; + return this; + } + + public long getDisableTime() { + return disableTime; + } + + public SaDisableWrapperInfo setDisableTime(long disableTime) { + this.disableTime = disableTime; + return this; + } + + public int getDisableLevel() { + return disableLevel; + } + + public SaDisableWrapperInfo setDisableLevel(int disableLevel) { + this.disableLevel = disableLevel; + return this; + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPlugin.java b/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPlugin.java new file mode 100644 index 000000000..b6910b1bc --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPlugin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +/** + * Sa-Token 插件总接口 + * + * @author click33 + * @since 1.41.0 + */ +public interface SaTokenPlugin { + + /** + * 安装插件 + */ + void install(); + + /** + * 卸载插件 + */ + default void destroy(){ + + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHolder.java b/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHolder.java new file mode 100644 index 000000000..921a996a0 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHolder.java @@ -0,0 +1,411 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.exception.SaTokenPluginException; +import cn.dev33.satoken.fun.hooks.SaTokenPluginHookFunction; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * Sa-Token 插件管理器,管理所有插件的加载与卸载 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginHolder { + + /** + * 默认实例,非单例模式,可替换 + */ + public static SaTokenPluginHolder instance = new SaTokenPluginHolder(); + + + // ------------------- 插件管理器初始化相关 ------------------- + + /** + * 是否已经加载过插件 + */ + public boolean isLoader = false; + + /** + * SPI 文件所在目录名称 + */ + public String spiDir = "satoken"; + + /** + * 初始化加载所有插件(多次调用只会执行一次) + */ + public synchronized void init() { + if(isLoader) { + return; + } + loaderPlugins(); + isLoader = true; + } + + /** + * 根据 SPI 机制加载所有插件 + *

+ * 加载所有 jar 下 /META-INF/satoken/ 目录下 cn.dev33.satoken.plugin.SaTokenPlugin 文件指定的实现类 + *

+ */ + public synchronized void loaderPlugins() { + SaManager.getLog().info("SPI plugin loading start ..."); + List plugins = _loaderPluginsBySpi(SaTokenPlugin.class, spiDir); + for (SaTokenPlugin plugin : plugins) { + installPlugin(plugin); + } + SaManager.getLog().info("SPI plugin loading end ..."); + } + + /** + * 自定义 SPI 读取策略 (无状态函数) + * @param serviceInterface SPI 接口 + * @param dirName 目录名称 + * @return / + * @param / + */ + protected List _loaderPluginsBySpi(Class serviceInterface, String dirName) { + String path = "META-INF/" + dirName + "/" + serviceInterface.getName(); + List providers = new ArrayList<>(); + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration resources = classLoader.getResources(path); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + try (InputStream is = url.openStream()) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + // 忽略空行和注释行 + if (!line.isEmpty() && !line.startsWith("#")) { + Class clazz = Class.forName(line, true, classLoader); + T instance = serviceInterface.cast(clazz.getDeclaredConstructor().newInstance()); + providers.add(instance); + } + } + } catch (Exception e) { + throw new SaTokenPluginException("SPI 插件加载失败: " + e.getMessage(), e); + } + } + } catch (Exception e) { + throw new SaTokenPluginException("SPI 插件加载失败: " + e.getMessage(), e); + } + return providers; + } + + + + // ------------------- 插件管理 ------------------- + + /** + * 所有插件的集合 + */ + private final List pluginList = new ArrayList<>(); + + /** + * 获取插件集合副本 (拷贝插件集合,而非每个插件实例) + * @return / + */ + public synchronized List getPluginListCopy() { + return new ArrayList<>(pluginList); + } + + /** + * 判断是否已经安装了指定插件 + * + * @param pluginClass 插件类型 + * @return / + */ + public synchronized boolean isInstalledPlugin(Class pluginClass) { + for (SaTokenPlugin plugin : pluginList) { + if (plugin.getClass().equals(pluginClass)) { + return true; + } + } + return false; + } + + /** + * 获取指定类型的插件 + * @param pluginClass / + * @return / + * @param / + */ + public synchronized T getPlugin(Class pluginClass) { + for (SaTokenPlugin plugin : pluginList) { + if (plugin.getClass().equals(pluginClass)) { + return (T) plugin; + } + } + return null; + } + + /** + * 消费指定集合的钩子函数,返回消费的数量 + * @param pluginClass / + * @param hooks / + * @param / + */ + protected synchronized int _consumeHooks(List> hooks, Class pluginClass) { + int consumeCount = 0; + for (int i = 0; i < hooks.size(); i++) { + SaTokenPluginHookModel model = hooks.get(i); + if(model.listenerClass.equals(pluginClass)) { + model.executeFunction.execute(getPlugin(pluginClass)); + hooks.remove(i); + i--; + consumeCount++; + } + } + return consumeCount; + } + + + // ------------------- 插件 Install 与 Destroy ------------------- + + /** + * 安装指定插件 + * @param plugin / + */ + public synchronized SaTokenPluginHolder installPlugin(SaTokenPlugin plugin) { + + // 插件为空,拒绝安装 + if (plugin == null) { + throw new SaTokenPluginException("插件不可为空"); + } + + // 插件已经被安装过了,拒绝再次安装 + if (isInstalledPlugin(plugin.getClass())) { + throw new SaTokenPluginException("插件 [ " + plugin.getClass().getCanonicalName() + " ] 已安装,不可重复安装"); + } + + // 执行该插件的 install 前置钩子 + _consumeHooks(beforeInstallHooks, plugin.getClass()); + + // 插件安装 + int consumeCount = _consumeHooks(installHooks, plugin.getClass()); + if (consumeCount == 0) { + plugin.install(); + } + + // 执行该插件的 install 后置钩子 + _consumeHooks(afterInstallHooks, plugin.getClass()); + + // 添加到插件集合 + pluginList.add(plugin); + + // 返回对象自身,支持连缀风格调用 + return this; + } + + /** + * 安装指定插件,根据插件类型 + * @param pluginClass / + */ + public synchronized SaTokenPluginHolder installPlugin(Class pluginClass) { + try { + T plugin = pluginClass.getDeclaredConstructor().newInstance(); + return installPlugin(plugin); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new SaTokenPluginException(e); + } + } + + /** + * 卸载指定插件 + * @param plugin / + */ + public synchronized SaTokenPluginHolder destroyPlugin(SaTokenPlugin plugin) { + + // 插件为空,拒绝卸载 + if (plugin == null) { + throw new SaTokenPluginException("插件不可为空"); + } + + // 插件未被安装,拒绝卸载 + if (!isInstalledPlugin(plugin.getClass())) { + throw new SaTokenPluginException("插件 [ " + plugin.getClass().getCanonicalName() + " ] 未安装,无法卸载"); + } + + // 执行该插件的 destroy 前置钩子 + _consumeHooks(beforeDestroyHooks, plugin.getClass()); + + // 插件卸载 + int consumeCount = _consumeHooks(destroyHooks, plugin.getClass()); + if (consumeCount == 0) { + plugin.destroy(); + } + + // 执行该插件的 destroy 后置钩子 + _consumeHooks(afterDestroyHooks, plugin.getClass()); + + // 返回对象自身,支持连缀风格调用 + return this; + } + + /** + * 卸载指定插件,根据插件类型 + * @param pluginClass / + */ + public synchronized SaTokenPluginHolder destroyPlugin(Class pluginClass) { + return destroyPlugin(getPlugin(pluginClass)); + } + + + // ------------------- 插件 Install 钩子 ------------------- + + /** + * 插件 [ Install 钩子 ] 集合 + */ + private final List> installHooks = new ArrayList<>(); + + /** + * 插件 [ Install 前置钩子 ] 集合 + */ + private final List> beforeInstallHooks = new ArrayList<>(); + + /** + * 插件 [ Install 后置钩子 ] 集合 + */ + private final List> afterInstallHooks = new ArrayList<>(); + + /** + * 注册指定插件的 [ Install 钩子 ],1、同插件支持多次注册。2、如果插件已经安装完毕,则抛出异常。3、注册 Install 钩子的插件默认安装行为将不再执行 + * @param listenerClass / + * @param executeFunction / + * @param / + */ + public synchronized SaTokenPluginHolder onInstall(Class listenerClass, SaTokenPluginHookFunction executeFunction) { + // 如果指定的插件已经安装完毕,则不再允许注册前置钩子函数 + if(isInstalledPlugin(listenerClass)) { + throw new SaTokenPluginException("插件 [ " + listenerClass.getCanonicalName() + " ] 已安装完毕,不允许再注册 Install 钩子函数"); + } + + // 堆积到钩子函数集合 + installHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); + + // 返回对象自身,支持连缀风格调用 + return this; + } + + /** + * 注册指定插件的 [ Install 前置钩子 ],1、同插件支持多次注册。2、如果插件已经安装完毕,则抛出异常 + * @param listenerClass / + * @param executeFunction / + * @param / + */ + public synchronized SaTokenPluginHolder onBeforeInstall(Class listenerClass, SaTokenPluginHookFunction executeFunction) { + // 如果指定的插件已经安装完毕,则不再允许注册前置钩子函数 + if(isInstalledPlugin(listenerClass)) { + throw new SaTokenPluginException("插件 [ " + listenerClass.getCanonicalName() + " ] 已安装完毕,不允许再注册 Install 前置钩子函数"); + } + + // 堆积到钩子函数集合 + beforeInstallHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); + + // 返回对象自身,支持连缀风格调用 + return this; + } + + /** + * 注册指定插件的 [ Install 后置钩子 ],1、同插件支持多次注册。2、如果插件已经安装完毕,则立即执行该钩子函数 + * @param listenerClass / + * @param executeFunction / + * @param / + */ + public synchronized SaTokenPluginHolder onAfterInstall(Class listenerClass, SaTokenPluginHookFunction executeFunction) { + // 如果指定的插件已经安装完毕,则立即执行该钩子函数 + if(isInstalledPlugin(listenerClass)) { + executeFunction.execute(getPlugin(listenerClass)); + return this; + } + + // 堆积到钩子函数集合 + afterInstallHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); + + // 返回对象自身,支持连缀风格调用 + return this; + } + + + // ------------------- 插件 Destroy 钩子 ------------------- + + /** + * 插件 [ Destroy 钩子 ] 集合 + */ + private final List> destroyHooks = new ArrayList<>(); + + /** + * 插件 [ Destroy 前置钩子 ] 集合 + */ + private final List> beforeDestroyHooks = new ArrayList<>(); + + /** + * 插件 [ Destroy 后置钩子 ] 集合 + */ + private final List> afterDestroyHooks = new ArrayList<>(); + + /** + * 注册指定插件的 [ Destroy 钩子 ],1、同插件支持多次注册。2、注册 Destroy 钩子的插件默认卸载行为将不再执行 + * @param listenerClass / + * @param executeFunction / + * @param / + */ + public synchronized SaTokenPluginHolder onDestroy(Class listenerClass, SaTokenPluginHookFunction executeFunction) { + destroyHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); + + // 返回对象自身,支持连缀风格调用 + return this; + } + + /** + * 注册指定插件的 [ Destroy 前置钩子 ],同插件支持多次注册 + * @param listenerClass / + * @param executeFunction / + * @param / + */ + public synchronized SaTokenPluginHolder onBeforeDestroy(Class listenerClass, SaTokenPluginHookFunction executeFunction) { + beforeDestroyHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); + + // 返回对象自身,支持连缀风格调用 + return this; + } + + /** + * 注册指定插件的 [ Destroy 后置钩子 ],同插件支持多次注册 + * @param listenerClass / + * @param executeFunction / + * @param / + */ + public synchronized SaTokenPluginHolder onAfterDestroy(Class listenerClass, SaTokenPluginHookFunction executeFunction) { + afterDestroyHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); + + // 返回对象自身,支持连缀风格调用 + return this; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHookModel.java b/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHookModel.java new file mode 100644 index 000000000..758e4c787 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHookModel.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.fun.hooks.SaTokenPluginHookFunction; + +/** + * Sa-Token 插件 Hook Model + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginHookModel { + + /** + * 监听插件类型 + */ + public Class listenerClass; + + /** + * 执行的方法 + */ + public SaTokenPluginHookFunction executeFunction; + + /** + * 构造函数 + * @param listenerClass / + * @param executeFunction / + */ + public SaTokenPluginHookModel(Class listenerClass, SaTokenPluginHookFunction executeFunction) { + this.listenerClass = listenerClass; + this.executeFunction = executeFunction; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/router/SaHttpMethod.java b/sa-token-core/src/main/java/cn/dev33/satoken/router/SaHttpMethod.java new file mode 100644 index 000000000..59ccd845c --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/router/SaHttpMethod.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.router; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaTokenException; + +/** + * Http 请求各种请求类型的枚举表示 + * + *

参考:Spring - HttpMethod + * + * @author click33 + * @since 1.27.0 + */ +public enum SaHttpMethod { + + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT, + + /** + * 代表全部请求方式 + */ + ALL; + + private static final Map map = new ConcurrentHashMap<>(); + + static { + for (SaHttpMethod reqMethod : values()) { + map.put(reqMethod.name(), reqMethod); + } + } + + /** + * String 转 enum + * @param method 请求类型 + * @return SaHttpMethod 对象 + */ + public static SaHttpMethod toEnum(String method) { + if(method == null) { + throw new SaTokenException("Method 不可以是 null").setCode(SaErrorCode.CODE_10321); + } + SaHttpMethod reqMethod = map.get(method.toUpperCase()); + if(reqMethod == null) { + throw new SaTokenException("无效Method:" + method).setCode(SaErrorCode.CODE_10321); + } + return reqMethod; + } + + /** + * String[] 转 enum[] + * @param methods 请求类型数组 + * @return SaHttpMethod 数组 + */ + public static SaHttpMethod[] toEnumArray(String... methods) { + SaHttpMethod [] arr = new SaHttpMethod[methods.length]; + for (int i = 0; i < methods.length; i++) { + arr[i] = SaHttpMethod.toEnum(methods[i]); + } + return arr; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouteFunction.java b/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouteFunction.java deleted file mode 100644 index 7ec2af191..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouteFunction.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.dev33.satoken.router; - -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.context.model.SaResponse; - -/** - * 路由拦截器验证方法Lambda - * - * @author kong - * - */ -@FunctionalInterface -public interface SaRouteFunction { - - /** - * 执行验证的方法 - * - * @param request Request包装对象 - * @param response Response包装对象 - * @param handler 处理对象 - */ - public void run(SaRequest request, SaResponse response, Object handler); - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouter.java b/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouter.java index 4419efa6f..b928ae181 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouter.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouter.java @@ -1,22 +1,51 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.router; -import java.util.Arrays; -import java.util.List; - -import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; -import cn.dev33.satoken.fun.IsRunFunction; import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.fun.SaParamRetFunction; +import cn.dev33.satoken.strategy.SaStrategy; + +import java.util.List; /** - * 路由匹配操作工具类 - * @author kong + * 路由匹配操作工具类 + * + *

提供了一系列的路由匹配操作方法,一般用在全局拦截器、过滤器做路由拦截鉴权。

+ *

简单示例:

+ *
+ *    	// 指定一条 match 规则
+ *    	SaRouter
+ *    	   	.match("/**")    // 拦截的 path 列表,可以写多个
+ *   	   	.notMatch("/user/doLogin")        // 排除掉的 path 列表,可以写多个
+ *   	   	.check(r->StpUtil.checkLogin());        // 要执行的校验动作,可以写完整的 lambda 表达式
+ * 
* + * @author click33 + * @since 1.27.0 */ public class SaRouter { + private SaRouter() { + } + // -------------------- 路由匹配相关 -------------------- /** @@ -26,7 +55,7 @@ public class SaRouter { * @return 是否匹配成功 */ public static boolean isMatch(String pattern, String path) { - return SaManager.getSaTokenContext().matchPath(pattern, path); + return SaStrategy.instance.routeMatcher.apply(pattern, path); } /** @@ -36,6 +65,9 @@ public static boolean isMatch(String pattern, String path) { * @return 是否匹配成功 */ public static boolean isMatch(List patterns, String path) { + if(patterns == null) { + return false; + } for (String pattern : patterns) { if(isMatch(pattern, path)) { return true; @@ -44,6 +76,44 @@ public static boolean isMatch(List patterns, String path) { return false; } + /** + * 路由匹配 + * @param patterns 路由匹配符数组 + * @param path 被匹配的路由 + * @return 是否匹配成功 + */ + public static boolean isMatch(String[] patterns, String path) { + if(patterns == null) { + return false; + } + for (String pattern : patterns) { + if(isMatch(pattern, path)) { + return true; + } + } + return false; + } + + /** + * Http请求方法匹配 + * @param methods Http请求方法断言数组 + * @param methodString Http请求方法 + * @return 是否匹配成功 + */ + public static boolean isMatch(SaHttpMethod[] methods, String methodString) { + if(methods == null) { + return false; + } + for (SaHttpMethod method : methods) { + if(method == SaHttpMethod.ALL || (method != null && method.toString().equalsIgnoreCase(methodString))) { + return true; + } + } + return false; + } + + // ------ 使用当前URI匹配 + /** * 路由匹配 (使用当前URI) * @param pattern 路由匹配符 @@ -61,94 +131,221 @@ public static boolean isMatchCurrURI(String pattern) { public static boolean isMatchCurrURI(List patterns) { return isMatch(patterns, SaHolder.getRequest().getRequestPath()); } + + /** + * 路由匹配 (使用当前URI) + * @param patterns 路由匹配符数组 + * @return 是否匹配成功 + */ + public static boolean isMatchCurrURI(String[] patterns) { + return isMatch(patterns, SaHolder.getRequest().getRequestPath()); + } + + /** + * Http请求方法匹配 (使用当前请求方式) + * @param methods Http请求方法断言数组 + * @return 是否匹配成功 + */ + public static boolean isMatchCurrMethod(SaHttpMethod[] methods) { + return isMatch(methods, SaHolder.getRequest().getMethod()); + } - // -------------------- 执行相关 -------------------- + // -------------------- 开始匹配 -------------------- /** - * 路由匹配,如果匹配成功则执行认证函数 - * @param pattern 路由匹配符 - * @param function 要执行的方法 + * 初始化一个SaRouterStaff,开始匹配 + * @return SaRouterStaff */ - public static void match(String pattern, SaFunction function) { - if(isMatchCurrURI(pattern)) { - function.run(); - } + public static SaRouterStaff newMatch() { + return new SaRouterStaff(); } + // ----------------- path匹配 + /** - * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 - * @param pattern 路由匹配符 - * @param excludePattern 要排除的路由匹配符 - * @param function 要执行的方法 + * 路由匹配 + * @param patterns 路由匹配符集合 + * @return SaRouterStaff */ - public static void match(String pattern, String excludePattern, SaFunction function) { - if(isMatchCurrURI(pattern)) { - if(isMatchCurrURI(excludePattern) == false) { - function.run(); - } - } + public static SaRouterStaff match(String... patterns) { + return new SaRouterStaff().match(patterns); } /** - * 路由匹配,如果匹配成功则执行认证函数 - * @param patterns 路由匹配符集合 - * @param function 要执行的方法 + * 路由匹配排除 + * @param patterns 路由匹配符排除数组 + * @return SaRouterStaff */ - public static void match(List patterns, SaFunction function) { - if(isMatchCurrURI(patterns)) { - function.run(); - } + public static SaRouterStaff notMatch(String... patterns) { + return new SaRouterStaff().notMatch(patterns); } /** - * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 - * @param patterns 路由匹配符集合 - * @param excludePatterns 要排除的路由匹配符集合 - * @param function 要执行的方法 + * 路由匹配 + * @param patterns 路由匹配符集合 + * @return 对象自身 */ - public static void match(List patterns, List excludePatterns, SaFunction function) { - if(isMatchCurrURI(patterns)) { - if(isMatchCurrURI(excludePatterns) == false) { - function.run(); - } - } + public static SaRouterStaff match(List patterns) { + return new SaRouterStaff().match(patterns); + } + + /** + * 路由匹配排除 + * @param patterns 路由匹配符排除集合 + * @return 对象自身 + */ + public static SaRouterStaff notMatch(List patterns) { + return new SaRouterStaff().notMatch(patterns); } + + // ----------------- Method匹配 + /** + * Http请求方式匹配 (Enum) + * @param methods Http请求方法断言数组 + * @return SaRouterStaff + */ + public static SaRouterStaff match(SaHttpMethod... methods) { + return new SaRouterStaff().match(methods); + } + + /** + * Http请求方法匹配排除 (Enum) + * @param methods Http请求方法断言排除数组 + * @return SaRouterStaff + */ + public static SaRouterStaff notMatch(SaHttpMethod... methods) { + return new SaRouterStaff().notMatch(methods); + } + + /** + * Http请求方法匹配 (String) + * @param methods Http请求方法断言数组 + * @return SaRouterStaff + */ + public static SaRouterStaff matchMethod(String... methods) { + return new SaRouterStaff().matchMethod(methods); + } + + /** + * Http请求方法匹配排除 (String) + * @param methods Http请求方法断言排除数组 + * @return SaRouterStaff + */ + public static SaRouterStaff notMatchMethod(String... methods) { + return new SaRouterStaff().notMatchMethod(methods); + } + + // ----------------- 条件匹配 + + /** + * 根据 boolean 值进行匹配 + * @param flag boolean值 + * @return SaRouterStaff + */ + public static SaRouterStaff match(boolean flag) { + return new SaRouterStaff().match(flag); + } + + /** + * 根据 boolean 值进行匹配排除 + * @param flag boolean值 + * @return SaRouterStaff + */ + public static SaRouterStaff notMatch(boolean flag) { + return new SaRouterStaff().notMatch(flag); + } + + /** + * 根据自定义方法进行匹配 (lazy) + * @param fun 自定义方法 + * @return SaRouterStaff + */ + public static SaRouterStaff match(SaParamRetFunction fun) { + return new SaRouterStaff().match(fun); + } + + /** + * 根据自定义方法进行匹配排除 (lazy) + * @param fun 自定义排除方法 + * @return SaRouterStaff + */ + public static SaRouterStaff notMatch(SaParamRetFunction fun) { + return new SaRouterStaff().notMatch(fun); + } + + + // -------------------- 直接指定check函数 -------------------- /** * 路由匹配,如果匹配成功则执行认证函数 - * @param patterns 路由匹配符集合 - * @return 匹配结果包装对象 + * @param pattern 路由匹配符 + * @param fun 要执行的校验方法 + * @return / + */ + public static SaRouterStaff match(String pattern, SaFunction fun) { + return new SaRouterStaff().match(pattern, fun); + } + + /** + * 路由匹配,如果匹配成功则执行认证函数 + * @param pattern 路由匹配符 + * @param fun 要执行的校验方法 + * @return / + */ + public static SaRouterStaff match(String pattern, SaParamFunction fun) { + return new SaRouterStaff().match(pattern, fun); + } + + /** + * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 + * @param pattern 路由匹配符 + * @param excludePattern 要排除的路由匹配符 + * @param fun 要执行的方法 + * @return / */ - public static IsRunFunction match(String... patterns) { - boolean matchResult = isMatch(Arrays.asList(patterns), SaHolder.getRequest().getRequestPath()); - return new IsRunFunction(matchResult); + public static SaRouterStaff match(String pattern, String excludePattern, SaFunction fun) { + return new SaRouterStaff().match(pattern, excludePattern, fun); + } + + /** + * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 + * @param pattern 路由匹配符 + * @param excludePattern 要排除的路由匹配符 + * @param fun 要执行的方法 + * @return / + */ + public static SaRouterStaff match(String pattern, String excludePattern, SaParamFunction fun) { + return new SaRouterStaff().match(pattern, excludePattern, fun); } - // -------------------- 其它操作 -------------------- + // -------------------- 提前退出 -------------------- /** * 停止匹配,跳出函数 (在多个匹配链中一次性跳出Auth函数) + * @return SaRouterStaff */ - public static void stop() { + public static SaRouterStaff stop() { throw new StopMatchException(); } /** * 停止匹配,结束执行,向前端返回结果 - * @param result 要输出的结果 + * @return SaRouterStaff */ - public static void back(Object result) { - throw new BackResultException(result); + public static SaRouterStaff back() { + throw new BackResultException(""); } - + /** * 停止匹配,结束执行,向前端返回结果 + * @param result 要输出的结果 + * @return SaRouterStaff */ - public static void back() { - throw new BackResultException(""); + public static SaRouterStaff back(Object result) { + throw new BackResultException(result); } - + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouterStaff.java b/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouterStaff.java new file mode 100644 index 000000000..2275e8b63 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouterStaff.java @@ -0,0 +1,344 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.router; + +import java.util.List; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.fun.SaParamRetFunction; + +/** + * 路由匹配操作对象 + * + * @author click33 + * @since 1.27.0 + */ +public class SaRouterStaff { + + /** + * 是否命中的标记变量 + */ + public boolean isHit = true; + + /** + * @return 是否命中 + */ + public boolean isHit() { + return isHit; + } + + /** + * @param isHit 命中标记 + * @return 对象自身 + */ + public SaRouterStaff setHit(boolean isHit) { + this.isHit = isHit; + return this; + } + + /** + * 重置命中标记为 true + * @return 对象自身 + */ + public SaRouterStaff reset() { + this.isHit = true; + return this; + } + + + // ----------------- path匹配 + + /** + * 路由匹配 + * @param patterns 路由匹配符数组 + * @return 对象自身 + */ + public SaRouterStaff match(String... patterns) { + if(isHit) { + isHit = SaRouter.isMatchCurrURI(patterns); + } + return this; + } + + /** + * 路由匹配排除 + * @param patterns 路由匹配符排除数组 + * @return 对象自身 + */ + public SaRouterStaff notMatch(String... patterns) { + if(isHit) { + isHit = !SaRouter.isMatchCurrURI(patterns); + } + return this; + } + + /** + * 路由匹配 + * @param patterns 路由匹配符集合 + * @return 对象自身 + */ + public SaRouterStaff match(List patterns) { + if(isHit) { + isHit = SaRouter.isMatchCurrURI(patterns); + } + return this; + } + + /** + * 路由匹配排除 + * @param patterns 路由匹配符排除集合 + * @return 对象自身 + */ + public SaRouterStaff notMatch(List patterns) { + if(isHit) { + isHit = !SaRouter.isMatchCurrURI(patterns); + } + return this; + } + + // ----------------- Method匹配 + + /** + * Http请求方法匹配 (Enum) + * @param methods Http请求方法断言数组 + * @return 对象自身 + */ + public SaRouterStaff match(SaHttpMethod... methods) { + if(isHit) { + isHit = SaRouter.isMatchCurrMethod(methods); + } + return this; + } + + /** + * Http请求方法匹配排除 (Enum) + * @param methods Http请求方法断言排除数组 + * @return 对象自身 + */ + public SaRouterStaff notMatch(SaHttpMethod... methods) { + if(isHit) { + isHit = !SaRouter.isMatchCurrMethod(methods); + } + return this; + } + + /** + * Http请求方法匹配 (String) + * @param methods Http请求方法断言数组 + * @return 对象自身 + */ + public SaRouterStaff matchMethod(String... methods) { + if(isHit) { + SaHttpMethod [] arr = SaHttpMethod.toEnumArray(methods); + isHit = SaRouter.isMatchCurrMethod(arr); + } + return this; + } + + /** + * Http请求方法匹配排除 (String) + * @param methods Http请求方法断言排除数组 + * @return 对象自身 + */ + public SaRouterStaff notMatchMethod(String... methods) { + if(isHit) { + SaHttpMethod [] arr = SaHttpMethod.toEnumArray(methods); + isHit = !SaRouter.isMatchCurrMethod(arr); + } + return this; + } + + + // ----------------- 条件匹配 + + /** + * 根据 boolean 值进行匹配 + * @param flag boolean值 + * @return 对象自身 + */ + public SaRouterStaff match(boolean flag) { + if(isHit) { + isHit = flag; + } + return this; + } + + /** + * 根据 boolean 值进行匹配排除 + * @param flag boolean值 + * @return 对象自身 + */ + public SaRouterStaff notMatch(boolean flag) { + if(isHit) { + isHit = !flag; + } + return this; + } + + /** + * 根据自定义方法进行匹配 (lazy) + * @param fun 自定义方法 + * @return 对象自身 + */ + public SaRouterStaff match(SaParamRetFunction fun) { + if(isHit) { + isHit = fun.run(this); + } + return this; + } + + /** + * 根据自定义方法进行匹配排除 (lazy) + * @param fun 自定义排除方法 + * @return 对象自身 + */ + public SaRouterStaff notMatch(SaParamRetFunction fun) { + if(isHit) { + isHit = !fun.run(this); + } + return this; + } + + + // ----------------- 函数校验执行 + + /** + * 执行校验函数 (无参) + * @param fun 要执行的函数 + * @return 对象自身 + */ + public SaRouterStaff check(SaFunction fun) { + if(isHit) { + fun.run(); + } + return this; + } + + /** + * 执行校验函数 (带参) + * @param fun 要执行的函数 + * @return 对象自身 + */ + public SaRouterStaff check(SaParamFunction fun) { + if(isHit) { + fun.run(this); + } + return this; + } + + /** + * 自由匹配 ( 在free作用域里执行stop()不会跳出Auth函数,而是仅仅跳出free代码块 ) + * @param fun 要执行的函数 + * @return 对象自身 + */ + public SaRouterStaff free(SaParamFunction fun) { + if(isHit) { + try { + fun.run(this); + } catch (StopMatchException e) { + // 跳出 free自由匹配代码块 + } + } + return this; + } + + + // ----------------- 直接指定check函数 + + /** + * 路由匹配,如果匹配成功则执行认证函数 + * @param pattern 路由匹配符 + * @param fun 要执行的校验方法 + * @return / + */ + public SaRouterStaff match(String pattern, SaFunction fun) { + return this.match(pattern).check(fun); + } + + /** + * 路由匹配,如果匹配成功则执行认证函数 + * @param pattern 路由匹配符 + * @param fun 要执行的校验方法 + * @return / + */ + public SaRouterStaff match(String pattern, SaParamFunction fun) { + return this.match(pattern).check(fun); + } + + /** + * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 + * @param pattern 路由匹配符 + * @param excludePattern 要排除的路由匹配符 + * @param fun 要执行的方法 + * @return / + */ + public SaRouterStaff match(String pattern, String excludePattern, SaFunction fun) { + return this.match(pattern).notMatch(excludePattern).check(fun); + } + + /** + * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 + * @param pattern 路由匹配符 + * @param excludePattern 要排除的路由匹配符 + * @param fun 要执行的方法 + * @return / + */ + public SaRouterStaff match(String pattern, String excludePattern, SaParamFunction fun) { + return this.match(pattern).notMatch(excludePattern).check(fun); + } + + + // ----------------- 提前退出 + + /** + * 停止匹配,跳出函数 (在多个匹配链中一次性跳出Auth函数) + * @return 对象自身 + */ + public SaRouterStaff stop() { + if(isHit) { + throw new StopMatchException(); + } + return this; + } + + /** + * 停止匹配,结束执行,向前端返回结果 + * @return 对象自身 + */ + public SaRouterStaff back() { + if(isHit) { + throw new BackResultException(""); + } + return this; + } + + /** + * 停止匹配,结束执行,向前端返回结果 + * @return 对象自身 + * @param result 要输出的结果 + */ + public SaRouterStaff back(Object result) { + if(isHit) { + throw new BackResultException(result); + } + return this; + } + + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouterUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouterUtil.java deleted file mode 100644 index dd1e4a1bc..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouterUtil.java +++ /dev/null @@ -1,143 +0,0 @@ -package cn.dev33.satoken.router; - -import java.util.Arrays; -import java.util.List; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.context.SaHolder; -import cn.dev33.satoken.exception.StopMatchException; -import cn.dev33.satoken.fun.IsRunFunction; -import cn.dev33.satoken.fun.SaFunction; - -/** - *

本类设计已过时,未来版本可能移除此类,请及时更换为 SaRouter ,使用方式保持不变

- * 对路由匹配符相关操作的封装工具类 - * @author kong - * - */ -@Deprecated -public class SaRouterUtil { - - // -------------------- 路由匹配相关 -------------------- - - /** - * 校验指定路由匹配符是否可以匹配成功指定路径 - * @param pattern 路由匹配符 - * @param path 需要匹配的路径 - * @return 是否匹配成功 - */ - public static boolean isMatch(String pattern, String path) { - return SaManager.getSaTokenContext().matchPath(pattern, path); - } - - /** - * 校验指定路由匹配符是否可以匹配成功指定路径 - * @param patterns 路由匹配符 - * @param path 需要匹配的路径集合 - * @return 是否匹配成功 - */ - public static boolean isMatch(List patterns, String path) { - for (String pattern : patterns) { - if(isMatch(pattern, path)) { - return true; - } - } - return false; - } - - /** - * 校验指定路由匹配符是否可以匹配成功当前URI - * @param pattern 路由匹配符 - * @return 是否匹配成功 - */ - public static boolean isMatchCurrURI(String pattern) { - return isMatch(pattern, SaHolder.getRequest().getRequestPath()); - } - - /** - * 校验指定路由匹配符是否可以匹配成功当前URI - * @param patterns 路由匹配符 - * @return 是否匹配成功 - */ - public static boolean isMatchCurrURI(List patterns) { - return isMatch(patterns, SaHolder.getRequest().getRequestPath()); - } - - - // -------------------- 执行相关 -------------------- - - /** - * 使用路由匹配符与当前URI执行匹配,如果匹配成功则执行验证函数 - * @param pattern 路由匹配符 - * @param function 要执行的方法 - */ - public static void match(String pattern, SaFunction function) { - if(isMatchCurrURI(pattern)) { - function.run(); - } - } - - /** - * 使用路由匹配符与当前URI执行匹配 (并指定排除匹配符),如果匹配成功则执行验证函数 - * @param pattern 路由匹配符 - * @param excludePattern 要排除的路由匹配符 - * @param function 要执行的方法 - */ - public static void match(String pattern, String excludePattern, SaFunction function) { - if(isMatchCurrURI(pattern)) { - if(isMatchCurrURI(excludePattern) == false) { - function.run(); - } - } - } - - /** - * 使用路由匹配符集合与当前URI执行匹配,如果匹配成功则执行验证函数 - * @param patterns 路由匹配符集合 - * @param function 要执行的方法 - */ - public static void match(List patterns, SaFunction function) { - if(isMatchCurrURI(patterns)) { - function.run(); - } - } - - /** - * 使用路由匹配符集合与当前URI执行匹配 (并指定排除匹配符),如果匹配成功则执行验证函数 - * @param patterns 路由匹配符集合 - * @param excludePatterns 要排除的路由匹配符集合 - * @param function 要执行的方法 - */ - public static void match(List patterns, List excludePatterns, SaFunction function) { - if(isMatchCurrURI(patterns)) { - if(isMatchCurrURI(excludePatterns) == false) { - function.run(); - } - } - } - - - /** - * 使用路由匹配符集合与当前URI执行匹配,如果匹配成功则执行验证函数 - * @param patterns 路由匹配符集合 - * @return 匹配结果包装对象 - */ - public static IsRunFunction match(String... patterns) { - boolean matchResult = isMatch(Arrays.asList(patterns), SaHolder.getRequest().getRequestPath()); - return new IsRunFunction(matchResult); - } - - - // -------------------- 其它操作 -------------------- - - /** - * 停止匹配,跳出函数 (在多个匹配链中一次性跳出Auth函数) - */ - public static void stop() { - throw new StopMatchException(); - } - - - - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameTemplate.java new file mode 100644 index 000000000..26cb92e35 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameTemplate.java @@ -0,0 +1,190 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.same; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SameTokenInvalidException; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * Sa Same-Token 同源系统身份认证模块 - 模板方法类 + * + *

解决同源系统互相调用时的身份认证校验, 例如:微服务网关请求转发鉴权、微服务RPC调用鉴权 + * + * @author click33 + * @since 1.32.0 + */ +public class SaSameTemplate { + + /** + * 提交 Same-Token 时,建议使用的参数名称 + */ + public static final String SAME_TOKEN = "SA-SAME-TOKEN"; + + // -------------------- 获取 & 校验 + + /** + * 获取当前 Same-Token, 如果不存在,则立即创建并返回 + * @return / + */ + public String getToken() { + String currentToken = getTokenNh(); + if(SaFoxUtil.isEmpty(currentToken)) { + // 注意这里的自刷新不能做到高并发可用 + currentToken = refreshToken(); + } + return currentToken; + } + + /** + * 判断一个 Same-Token 是否有效 + * @param token / + * @return / + */ + public boolean isValid(String token) { + // 1、 如果传入的 token 为空,则立即返回 false + if(SaFoxUtil.isEmpty(token)) { + return false; + } + + // 2、 验证当前 Same-Token 及 Past-Same-Token + return token.equals(getToken()) || token.equals(getPastTokenNh()); + } + + /** + * 校验一个 Same-Token 是否有效 (如果无效则抛出异常) + * @param token / + */ + public void checkToken(String token) { + if( ! isValid(token)) { + token = (token == null ? "" : token); + throw new SameTokenInvalidException("无效Same-Token:" + token).setCode(SaErrorCode.CODE_10301); + } + } + + /** + * 校验当前 Request 上下文提供的 Same-Token 是否有效 (如果无效则抛出异常) + */ + public void checkCurrentRequestToken() { + checkToken(SaHolder.getRequest().getHeader(SAME_TOKEN)); + } + + /** + * 刷新一次 Same-Token (注意集群环境中不要多个服务重复调用) + * @return 刷新后产生的新 Same-Token + */ + public String refreshToken() { + + // 1. 先将当前 Same-Token 写入到 Past-Same-Token 中 + String sameToken = getTokenNh(); + if( ! SaFoxUtil.isEmpty(sameToken)) { + savePastToken(sameToken, getTokenTimeout()); + } + + // 2. 再刷新当前 Same-Token + String newSameToken = createToken(); + saveToken(newSameToken); + + // 3. 返回新的 Same-Token + return newSameToken; + } + + + // ------------------------------ 保存Token + + /** + * 保存 Same-Token + * @param token / + */ + public void saveToken(String token) { + if(SaFoxUtil.isEmpty(token)) { + return; + } + SaManager.getSaTokenDao().set(splicingTokenSaveKey(), token, SaManager.getConfig().getSameTokenTimeout()); + } + + /** + * 保存 Past-Same-Token + * @param token token + * @param timeout 有效期(单位:秒) + */ + public void savePastToken(String token, long timeout){ + if(SaFoxUtil.isEmpty(token)) { + return; + } + SaManager.getSaTokenDao().set(splicingPastTokenSaveKey(), token, timeout); + } + + + // -------------------- 获取Token + + /** + * 获取 Same-Token,不做任何处理 + * @return / + */ + public String getTokenNh() { + return SaManager.getSaTokenDao().get(splicingTokenSaveKey()); + } + + /** + * 获取 Past-Same-Token,不做任何处理 + * @return / + */ + public String getPastTokenNh() { + return SaManager.getSaTokenDao().get(splicingPastTokenSaveKey()); + } + + /** + * 获取 Same-Token 的剩余有效期 (单位:秒) + * @return / + */ + public long getTokenTimeout() { + return SaManager.getSaTokenDao().getTimeout(splicingTokenSaveKey()); + } + + + // -------------------- 创建Token + + /** + * 创建一个 Same-Token + * @return Token + */ + public String createToken() { + return SaFoxUtil.getRandomString(64); + } + + + // -------------------- 拼接key + + /** + * 拼接key:Same-Token 的存储 key + * @return key + */ + public String splicingTokenSaveKey() { + return SaManager.getConfig().getTokenName() + ":var:same-token"; + } + + /** + * 拼接key:次级 Same-Token 的存储 key + * @return key + */ + public String splicingPastTokenSaveKey() { + return SaManager.getConfig().getTokenName() + ":var:past-same-token"; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameUtil.java new file mode 100644 index 000000000..1fa6d2076 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameUtil.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.same; + +import cn.dev33.satoken.SaManager; + +/** + * Sa Same-Token 同源系统身份认证模块 - 工具类 + * + *

解决同源系统互相调用时的身份认证校验, 例如:微服务网关请求转发鉴权、微服务RPC调用鉴权 + * + * @author click33 + * @since 1.32.0 + */ +public class SaSameUtil { + + private SaSameUtil(){} + + /** + * 提交 Same-Token 时,建议使用的参数名称 + */ + public static final String SAME_TOKEN = SaSameTemplate.SAME_TOKEN; + + // -------------------- 获取 & 校验 + + /** + * 获取当前 Same-Token, 如果不存在,则立即创建并返回 + * @return / + */ + public static String getToken() { + return SaManager.getSaSameTemplate().getToken(); + } + + /** + * 判断一个 Same-Token 是否有效 + * @param token / + * @return / + */ + public static boolean isValid(String token) { + return SaManager.getSaSameTemplate().isValid(token); + } + + /** + * 校验一个 Same-Token 是否有效 (如果无效则抛出异常) + * @param token / + */ + public static void checkToken(String token) { + SaManager.getSaSameTemplate().checkToken(token); + } + + /** + * 校验当前 Request 上下文提供的 Same-Token 是否有效 (如果无效则抛出异常) + */ + public static void checkCurrentRequestToken() { + SaManager.getSaSameTemplate().checkCurrentRequestToken(); + } + + /** + * 刷新一次 Same-Token (注意集群环境中不要多个服务重复调用) + * @return 刷新后产生的新 Same-Token + */ + public static String refreshToken() { + return SaManager.getSaSameTemplate().refreshToken(); + } + + + // -------------------- 获取Token + + /** + * 获取 Same-Token,不做任何处理 + * @return / + */ + public static String getTokenNh() { + return SaManager.getSaSameTemplate().getTokenNh(); + } + + /** + * 获取 Past-Same-Token,不做任何处理 + * @return / + */ + public static String getPastTokenNh() { + return SaManager.getSaSameTemplate().getPastTokenNh(); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/secure/BCrypt.java b/sa-token-core/src/main/java/cn/dev33/satoken/secure/BCrypt.java new file mode 100644 index 000000000..f4172471d --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/secure/BCrypt.java @@ -0,0 +1,550 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.secure; + + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +/** + * BCrypt加密算法实现。由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。 + *

+ * 此类来自于https://github.com/jeremyh/jBCrypt/ + *

+ * 使用方法如下: + *

+ * {@code + * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); + * } + *

+ * 使用checkpw方法检查被加密的字符串是否与原始字符串匹配: + *

+ * {@code + * BCrypt.checkpw(candidate_password, stored_hash); + * } + *

+ * gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少,也决定了加密的复杂度: + *

+ * {@code + * String strong_salt = BCrypt.gensalt(10); + * String stronger_salt = BCrypt.gensalt(12); + * } + * + * @author Damien Miller + * @since 1.29.0 + */ +@Deprecated +@SuppressWarnings("all") +public class BCrypt { + // BCrypt parameters + private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; + private static final int BCRYPT_SALT_LEN = 16; + + // Blowfish parameters + private static final int BLOWFISH_NUM_ROUNDS = 16; + + // Initial contents of key schedule + private static final int[] P_orig = {0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, + 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b}; + private static final int[] S_orig = {0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, + 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, + 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, + 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482, + 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, + 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, + 0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, + 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, + 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, + 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0, + 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, + 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, + 0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, + 0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6, + 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, + 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146, + 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, + 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, + 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, + 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775, + 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, + 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 0x0e358829, + 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, + 0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, + 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, + 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, + 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, + 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, + 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, + 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, + 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, + 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, + 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7, + 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, + 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, + 0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, + 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, + 0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, + 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, + 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb, + 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, + 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, + 0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, + 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, 0x586cdecf, + 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, + 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82, + 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, + 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, + 0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, + 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60, + 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, + 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6}; + + // bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls + // this "ciphertext", but it is really plaintext or an IV. We keep + // the name to make code comparison easier. + static private final int[] bf_crypt_ciphertext = {0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274}; + + // Table for Base64 encoding + static private final char[] base64_code = {'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', + 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + + // Table for Base64 decoding + static private final byte[] index_64 = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1}; + + // Expanded Blowfish key + private int[] P; + private int[] S; + + /** + * Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note that this is *not* compatible with the standard MIME-base64 encoding. + * + * @param d the byte array to encode + * @param len the number of bytes to encode + * @return base64-encoded string + * @throws IllegalArgumentException if the length is invalid + */ + private static String encode_base64(byte[] d, int len) throws IllegalArgumentException { + int off = 0; + StringBuilder rs = new StringBuilder(); + int c1, c2; + + if (len <= 0 || len > d.length) + throw new IllegalArgumentException("Invalid len"); + + while (off < len) { + c1 = d[off++] & 0xff; + rs.append(base64_code[(c1 >> 2) & 0x3f]); + c1 = (c1 & 0x03) << 4; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + rs.append(base64_code[c1 & 0x3f]); + c1 = (c2 & 0x0f) << 2; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + rs.append(base64_code[c1 & 0x3f]); + rs.append(base64_code[c2 & 0x3f]); + } + return rs.toString(); + } + + /** + * Look up the 3 bits base64-encoded by the specified character, range-checking againt conversion table + * + * @param x the base64-encoded value + * @return the decoded value of x + */ + private static byte char64(char x) { + if ((int) x > index_64.length) + return -1; + return index_64[x]; + } + + /** + * Decode a string encoded using bcrypt's base64 scheme to a byte array.
+ * Note that this is *not* compatible with the standard MIME-base64 encoding. + * + * @param s the string to decode + * @param maxolen the maximum number of bytes to decode + * @return an array containing the decoded bytes + * @throws IllegalArgumentException if maxolen is invalid + */ + private static byte[] decodeBase64(String s, int maxolen) throws IllegalArgumentException { + final StringBuilder rs = new StringBuilder(); + int off = 0, slen = s.length(), olen = 0; + byte[] ret; + byte c1, c2, c3, c4, o; + + if (maxolen <= 0) + throw new IllegalArgumentException("Invalid maxolen"); + + while (off < slen - 1 && olen < maxolen) { + c1 = char64(s.charAt(off++)); + c2 = char64(s.charAt(off++)); + if (c1 == -1 || c2 == -1) + break; + o = (byte) (c1 << 2); + o |= (c2 & 0x30) >> 4; + rs.append((char) o); + if (++olen >= maxolen || off >= slen) + break; + c3 = char64(s.charAt(off++)); + if (c3 == -1) + break; + o = (byte) ((c2 & 0x0f) << 4); + o |= (c3 & 0x3c) >> 2; + rs.append((char) o); + if (++olen >= maxolen || off >= slen) + break; + c4 = char64(s.charAt(off++)); + o = (byte) ((c3 & 0x03) << 6); + o |= c4; + rs.append((char) o); + ++olen; + } + + ret = new byte[olen]; + for (off = 0; off < olen; off++) + ret[off] = (byte) rs.charAt(off); + return ret; + } + + /** + * Blowfish encipher a single 64-bit block encoded as two 32-bit halves + * + * @param lr an array containing the two 32-bit half blocks + * @param off the position in the array of the blocks + */ + private void encipher(int[] lr, int off) { + int i, n, l = lr[off], r = lr[off + 1]; + + l ^= P[0]; + for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2; ) { + // Feistel substitution on left word + n = S[(l >> 24) & 0xff]; + n += S[0x100 | ((l >> 16) & 0xff)]; + n ^= S[0x200 | ((l >> 8) & 0xff)]; + n += S[0x300 | (l & 0xff)]; + r ^= n ^ P[++i]; + + // Feistel substitution on right word + n = S[(r >> 24) & 0xff]; + n += S[0x100 | ((r >> 16) & 0xff)]; + n ^= S[0x200 | ((r >> 8) & 0xff)]; + n += S[0x300 | (r & 0xff)]; + l ^= n ^ P[++i]; + } + lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; + lr[off + 1] = l; + } + + /** + * Cycically extract a word of key material + * + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the current offset into data + * @return the next word of material from data + */ + private static int streamToWord(byte[] data, int[] offp) { + int i; + int word = 0; + int off = offp[0]; + + for (i = 0; i < 4; i++) { + word = (word << 8) | (data[off] & 0xff); + off = (off + 1) % data.length; + } + + offp[0] = off; + return word; + } + + /** + * Initialise the Blowfish key schedule + */ + private void init_key() { + P = P_orig.clone(); + S = S_orig.clone(); + } + + /** + * Key the Blowfish cipher + * + * @param key an array containing the key + */ + private void key(byte[] key) { + int i; + int[] koffp = {0}; + int[] lr = {0, 0}; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamToWord(key, koffp); + + for (i = 0; i < plen; i += 2) { + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the "enhanced key schedule" step described by Provos and Mazieres in "A Future-Adaptable Password Scheme" http://www.openbsd.org/papers/bcrypt-paper.ps + * + * @param data salt information + * @param key password information + */ + private void ekskey(byte[] data, byte[] key) { + int i; + int[] koffp = {0}; + int[] doffp = {0}; + int[] lr = {0, 0}; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamToWord(key, koffp); + + for (i = 0; i < plen; i += 2) { + lr[0] ^= streamToWord(data, doffp); + lr[1] ^= streamToWord(data, doffp); + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + lr[0] ^= streamToWord(data, doffp); + lr[1] ^= streamToWord(data, doffp); + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * 加密密文 + * + * @param password 明文密码 + * @param salt 加盐 + * @param log_rounds hash中叠加的对数 + * @param cdata 加密数据 + * @return 加密后的密文 + */ + public byte[] crypt(byte[] password, byte[] salt, int log_rounds, int[] cdata) { + int rounds, i, j; + int clen = cdata.length; + byte[] ret; + + if (log_rounds < 4 || log_rounds > 30) + throw new IllegalArgumentException("Bad number of rounds"); + rounds = 1 << log_rounds; + if (salt.length != BCRYPT_SALT_LEN) + throw new IllegalArgumentException("Bad salt length"); + + init_key(); + ekskey(salt, password); + for (i = 0; i != rounds; i++) { + key(password); + key(salt); + } + + for (i = 0; i < 64; i++) { + for (j = 0; j < (clen >> 1); j++) + encipher(cdata, j << 1); + } + + ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) { + ret[j++] = (byte) ((cdata[i] >> 24) & 0xff); + ret[j++] = (byte) ((cdata[i] >> 16) & 0xff); + ret[j++] = (byte) ((cdata[i] >> 8) & 0xff); + ret[j++] = (byte) (cdata[i] & 0xff); + } + return ret; + } + + /** + * 生成密文,使用长度为10的加盐方式 + * + * @param password 需要加密的明文 + * @return 密文 + */ + public static String hashpw(String password) { + return hashpw(password, gensalt()); + } + + /** + * 生成密文 + * + * @param password 需要加密的明文 + * @param salt 盐,使用{@link #gensalt()} 生成 + * @return 密文 + */ + public static String hashpw(String password, String salt) { + BCrypt bcrypt; + String real_salt; + byte[] saltb; + byte[] hashed; + char minor = (char) 0; + int rounds, off; + StringBuilder rs = new StringBuilder(); + + if (salt.charAt(0) != '$' || salt.charAt(1) != '2') + throw new IllegalArgumentException("Invalid salt version"); + if (salt.charAt(2) == '$') + off = 3; + else { + minor = salt.charAt(2); + // pr#1560@Github + // 修正一个在Blowfish实现上的安全风险 + if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') + throw new IllegalArgumentException("Invalid salt revision"); + off = 4; + } + + // Extract number of rounds + if (salt.charAt(off + 2) > '$') + throw new IllegalArgumentException("Missing salt rounds"); + rounds = Integer.parseInt(salt.substring(off, off + 2)); + + real_salt = salt.substring(off + 3, off + 25); + byte[] passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes(StandardCharsets.UTF_8); + saltb = decodeBase64(real_salt, BCRYPT_SALT_LEN); + + bcrypt = new BCrypt(); + hashed = bcrypt.crypt(passwordb, saltb, rounds, bf_crypt_ciphertext.clone()); + + rs.append("$2"); + if (minor >= 'a') + rs.append(minor); + rs.append("$"); + if (rounds < 10) + rs.append("0"); + if (rounds > 30) { + throw new IllegalArgumentException("rounds exceeds maximum (30)"); + } + rs.append(rounds); + rs.append("$"); + rs.append(encode_base64(saltb, saltb.length)); + rs.append(encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1)); + return rs.toString(); + } + + /** + * 生成盐 + * + * @param log_rounds hash中叠加的2的对数 - the work factor therefore increases as 2**log_rounds. + * @param random {@link SecureRandom} + * @return an encoded salt value + */ + public static String gensalt(int log_rounds, SecureRandom random) { + final StringBuilder rs = new StringBuilder(); + byte[] rnd = new byte[BCRYPT_SALT_LEN]; + + random.nextBytes(rnd); + + rs.append("$2a$"); + if (log_rounds < 10) + rs.append("0"); + if (log_rounds > 30) { + throw new IllegalArgumentException("log_rounds exceeds maximum (30)"); + } + rs.append(log_rounds); + rs.append("$"); + rs.append(encode_base64(rnd, rnd.length)); + return rs.toString(); + } + + /** + * 生成盐 + * + * @param log_rounds the log2 of the number of rounds of hashing to apply - the work factor therefore increases as 2**log_rounds. + * @return 盐 + */ + public static String gensalt(int log_rounds) { + return gensalt(log_rounds, new SecureRandom()); + } + + /** + * 生成盐 + * + * @return 盐 + */ + public static String gensalt() { + return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); + } + + /** + * 检查明文密码文本是否匹配加密后的文本 + * + * @param plaintext 需要验证的明文密码 + * @param hashed 密文 + * @return 是否匹配 + */ + public static boolean checkpw(String plaintext, String hashed) { + byte[] hashed_bytes; + byte[] try_bytes; + + String try_pw; + try { + try_pw = hashpw(plaintext, hashed); + } catch (Exception ignore) { + // 生成密文时错误直接返回false issue#1377@Github + return false; + } + hashed_bytes = hashed.getBytes(StandardCharsets.UTF_8); + try_bytes = try_pw.getBytes(StandardCharsets.UTF_8); + if (hashed_bytes.length != try_bytes.length) { + return false; + } + byte ret = 0; + for (int i = 0; i < try_bytes.length; i++) + ret |= hashed_bytes[i] ^ try_bytes[i]; + return ret == 0; + } +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase32Util.java b/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase32Util.java new file mode 100644 index 000000000..5bf6df2eb --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase32Util.java @@ -0,0 +1,126 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.secure; + +import java.nio.charset.StandardCharsets; + +/** + * Sa-Token Base32 工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaBase32Util { + + private static final String BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + private static final int[] BASE32_LOOKUP = new int[256]; + + static { + // 初始化解码查找表 + for (int i = 0; i < BASE32_CHARS.length(); i++) { + char c = BASE32_CHARS.charAt(i); + BASE32_LOOKUP[c] = i; + // 支持小写字母解码 + if (c >= 'A' && c <= 'Z') { + BASE32_LOOKUP[Character.toLowerCase(c)] = i; + } + } + } + + /** + * Base32 编码(byte[] 转 String) + */ + public static String encodeBytesToString(byte[] bytes) { + if (bytes == null) return null; + + StringBuilder result = new StringBuilder(); + int buffer = 0; + int bufferSize = 0; + + for (byte b : bytes) { + buffer = (buffer << 8) | (b & 0xFF); + bufferSize += 8; + + while (bufferSize >= 5) { + bufferSize -= 5; + int index = (buffer >> bufferSize) & 0x1F; + result.append(BASE32_CHARS.charAt(index)); + } + } + + // 处理剩余位 + if (bufferSize > 0) { + int index = (buffer << (5 - bufferSize)) & 0x1F; + result.append(BASE32_CHARS.charAt(index)); + } + + return result.toString(); + } + + /** + * Base32 解码(String 转 byte[]) + */ + public static byte[] decodeStringToBytes(String text) { + if (text == null) return null; + + text = text.replaceAll("=", "").trim(); + if (text.isEmpty()) return new byte[0]; + + int buffer = 0; + int bufferSize = 0; + int byteCount = (text.length() * 5 + 7) / 8; + byte[] bytes = new byte[byteCount]; + int byteIndex = 0; + + for (char c : text.toCharArray()) { + int value = BASE32_LOOKUP[c]; + if (value == 0 && c != 'A') continue; // 跳过非法字符 + + buffer = (buffer << 5) | value; + bufferSize += 5; + + if (bufferSize >= 8) { + bufferSize -= 8; + bytes[byteIndex++] = (byte) ((buffer >> bufferSize) & 0xFF); + } + } + + // 处理最后一个字节 + if (bufferSize > 0) { + bytes[byteIndex] = (byte) ((buffer << (8 - bufferSize)) & 0xFF); + } + + return bytes; + } + + /** + * Base32 编码(String 转 String) + */ + public static String encode(String text) { + if (text == null) return null; + return encodeBytesToString(text.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Base32 解码(String 转 String) + */ + public static String decode(String base32Text) { + if (base32Text == null) return null; + byte[] bytes = decodeStringToBytes(base32Text); + return new String(bytes, StandardCharsets.UTF_8); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase64Util.java b/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase64Util.java index cd56df8c3..9bc7b08a3 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase64Util.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase64Util.java @@ -1,17 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.secure; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.Base64; /** - * Sa-Token Base64工具类 - * @author kong + * Sa-Token Base64 工具类 * + * @author click33 + * @since 1.14.0 */ public class SaBase64Util { - private static Base64.Encoder encoder = Base64.getEncoder(); - private static Base64.Decoder decoder = Base64.getDecoder(); + private static final Base64.Encoder encoder = Base64.getEncoder(); + private static final Base64.Decoder decoder = Base64.getDecoder(); /** * Base64编码,byte[] 转 String @@ -37,11 +53,7 @@ public static byte[] decodeStringToBytes(String text){ * @return Base64格式字符串 */ public static String encode(String text){ - try { - return encoder.encodeToString(text.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + return encoder.encodeToString(text.getBytes(StandardCharsets.UTF_8)); } /** @@ -50,11 +62,7 @@ public static String encode(String text){ * @return 字符串 */ public static String decode(String base64Text){ - try { - return new String(decoder.decode(base64Text), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + return new String(decoder.decode(base64Text), StandardCharsets.UTF_8); } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaSecureUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaSecureUtil.java index f1a27a662..541fd1099 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaSecureUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/secure/SaSecureUtil.java @@ -1,14 +1,29 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.secure; -import java.security.InvalidParameterException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaTokenException; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; @@ -18,35 +33,31 @@ import java.util.HashMap; import java.util.UUID; -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import cn.dev33.satoken.exception.SaTokenException; - /** * Sa-Token 常见加密算法工具类 - * - * @author kong * + * @author click33 + * @since 1.14.0 */ public class SaSecureUtil { + private SaSecureUtil() { + } + /** * Base64编码 */ - private static Base64.Encoder encoder = Base64.getEncoder(); + private static final Base64.Encoder encoder = Base64.getEncoder(); /** * Base64解码 */ - private static Base64.Decoder decoder = Base64.getDecoder(); - + private static final Base64.Decoder decoder = Base64.getDecoder(); + // ----------------------- 摘要加密 ----------------------- - + /** - * md5加密 + * md5加密 * @param str 指定字符串 * @return 加密后的字符串 */ @@ -67,41 +78,29 @@ public static String md5(String str) { } return new String(strA); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12111); } } /** - * sha1加密 - * + * sha1加密 + * * @param str 指定字符串 * @return 加密后的字符串 */ public static String sha1(String str) { try { str = (str == null ? "" : str); - MessageDigest md = MessageDigest.getInstance("SHA1"); - byte[] b = str.getBytes(); - md.update(b); - byte[] b2 = md.digest(); - int len = b2.length; - String strA = "0123456789abcdef"; - char[] ch = strA.toCharArray(); - char[] chs = new char[len * 2]; - for (int i = 0, k = 0; i < len; i++) { - byte b3 = b2[i]; - chs[k++] = ch[b3 >>> 4 & 0xf]; - chs[k++] = ch[b3 & 0xf]; - } - return new String(chs); + MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); + return getShaHexString(str, messageDigest); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12112); } } /** - * sha256加密 - * + * sha256加密 + * * @param str 指定字符串 * @return 加密后的字符串 */ @@ -109,40 +108,93 @@ public static String sha256(String str) { try { str = (str == null ? "" : str); MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - messageDigest.update(str.getBytes("UTF-8")); - - byte[] bytes = messageDigest.digest(); - StringBuilder builder = new StringBuilder(); - String temp; - for (int i = 0; i < bytes.length; i++) { - temp = Integer.toHexString(bytes[i] & 0xFF); - if (temp.length() == 1) { - builder.append("0"); - } - builder.append(temp); - } - - return builder.toString(); + return getShaHexString(str, messageDigest); + } catch (Exception e) { + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12113); + } + } + + /** + * sha384加密 + * + * @param str 指定字符串 + * @return 加密后的字符串 + */ + public static String sha384(String str) { + try { + str = (str == null ? "" : str); + MessageDigest messageDigest = MessageDigest.getInstance("SHA-384"); + return getShaHexString(str, messageDigest); + } catch (Exception e) { + throw new SaTokenException(e).setCode(SaErrorCode.CODE_121131); + } + } + + /** + * sha512加密 + * + * @param str 指定字符串 + * @return 加密后的字符串 + */ + public static String sha512(String str) { + try { + str = (str == null ? "" : str); + MessageDigest messageDigest = MessageDigest.getInstance("SHA-512"); + return getShaHexString(str, messageDigest); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_121132); + } + } + + /** + * sha (Secure Hash Algorithm)加密 公共方法 + * + * @param str 指定字符串 + * @param messageDigest 消息摘要 + * @return 加密后的字符串 + */ + private static String getShaHexString(String str, MessageDigest messageDigest) { + messageDigest.update(str.getBytes(StandardCharsets.UTF_8)); + byte[] bytes = messageDigest.digest(); + StringBuilder builder = new StringBuilder(); + String temp; + for (byte aByte : bytes) { + temp = Integer.toHexString(aByte & 0xFF); // 获取无符号整数十六进制字符串 + if (temp.length() == 1) { + builder.append("0"); // 确保每个字节都用两个字符表示 + } + builder.append(temp); } + + return builder.toString(); } /** - * md5加盐加密: md5(md5(str) + md5(salt)) + * md5加盐加密: md5(md5(str) + md5(salt)) * @param str 字符串 - * @param salt 盐 + * @param salt 盐 * @return 加密后的字符串 */ + @Deprecated public static String md5BySalt(String str, String salt) { return md5(md5(str) + md5(salt)); } - - + + /** + * sha256加盐加密: sha256(sha256(str) + sha256(salt)) + * @param str 字符串 + * @param salt 盐 + * @return 加密后的字符串 + */ + @Deprecated + public static String sha256BySalt(String str, String salt) { + return sha256(sha256(str) + sha256(salt)); + } + // ----------------------- 对称加密 AES ----------------------- - + /** - * 默认密码算法 + * 默认密码算法 */ private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; @@ -156,17 +208,17 @@ public static String md5BySalt(String str, String salt) { public static String aesEncrypt(String key, String text) { try { Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM); - byte[] byteContent = text.getBytes("utf-8"); + byte[] byteContent = text.getBytes(StandardCharsets.UTF_8); cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key)); byte[] result = cipher.doFinal(byteContent); return encoder.encodeToString(result); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12114); } } /** - * AES解密 + * AES解密 * @param key 加密的密钥 * @param text 已加密的密文 * @return 返回解密后的数据 @@ -176,18 +228,17 @@ public static String aesDecrypt(String key, String text) { Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key)); byte[] result = cipher.doFinal(decoder.decode(text)); - return new String(result, "utf-8"); + return new String(result, StandardCharsets.UTF_8); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12115); } } /** - * 生成加密秘钥 + * 生成加密秘钥 * @param password 秘钥 - * @return SecretKeySpec - * @throws NoSuchAlgorithmException - */ + * @return SecretKeySpec + */ private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException { KeyGenerator kg = KeyGenerator.getInstance("AES"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); @@ -201,36 +252,31 @@ private static SecretKeySpec getSecretKey(final String password) throws NoSuchAl // ----------------------- 非对称加密 RSA ----------------------- private static final String ALGORITHM = "RSA"; - + private static final int KEY_SIZE = 1024; - // ---------- 5个常用方法 + // ---------- 5个常用方法 /** - * 生成密钥对 + * 生成密钥对 * @return Map对象 (private=私钥, public=公钥) * @throws Exception 异常 */ + @Deprecated public static HashMap rsaGenerateKeyPair() throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM); KeyPair keyPair; - try { - keyPairGenerator.initialize(KEY_SIZE, - new SecureRandom(UUID.randomUUID().toString().replaceAll("-", "").getBytes())); - keyPair = keyPairGenerator.generateKeyPair(); - } catch (InvalidParameterException e) { - throw e; - } catch (NullPointerException e) { - throw e; - } + keyPairGenerator.initialize(KEY_SIZE, + new SecureRandom(UUID.randomUUID().toString().replaceAll("-", "").getBytes())); + keyPair = keyPairGenerator.generateKeyPair(); RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); - HashMap map = new HashMap(16); + HashMap map = new HashMap<>(16); map.put("private", encoder.encodeToString(rsaPrivateKey.getEncoded())); map.put("public", encoder.encodeToString(rsaPublicKey.getEncoded())); return map; @@ -242,32 +288,34 @@ public static HashMap rsaGenerateKeyPair() throws Exception { * @param content 内容 * @return 加密后内容 */ + @Deprecated public static String rsaEncryptByPublic(String publicKeyString, String content) { try { // 获得公钥对象 - PublicKey publicKey = getPublicKeyFromString(publicKeyString); + PublicKey publicKey = getPublicKeyFromString(publicKeyString); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); // 该密钥能够加密的最大字节长度 int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8 - 11; byte[][] arrays = splitBytes(content.getBytes(), splitLength); - StringBuffer stringBuffer = new StringBuffer(); + StringBuilder stringBuilder = new StringBuilder(); for (byte[] array : arrays) { - stringBuffer.append(bytesToHexString(cipher.doFinal(array))); + stringBuilder.append(bytesToHexString(cipher.doFinal(array))); } - return stringBuffer.toString(); + return stringBuilder.toString(); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12116); } } - /** + /** * RSA私钥加密 * @param privateKeyString 私钥 * @param content 内容 * @return 加密后内容 */ + @Deprecated public static String rsaEncryptByPrivate(String privateKeyString, String content) { try { PrivateKey privateKey = getPrivateKeyFromString(privateKeyString); @@ -277,22 +325,23 @@ public static String rsaEncryptByPrivate(String privateKeyString, String content // 该密钥能够加密的最大字节长度 int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8 - 11; byte[][] arrays = splitBytes(content.getBytes(), splitLength); - StringBuffer stringBuffer = new StringBuffer(); + StringBuilder stringBuilder = new StringBuilder(); for (byte[] array : arrays) { - stringBuffer.append(bytesToHexString(cipher.doFinal(array))); + stringBuilder.append(bytesToHexString(cipher.doFinal(array))); } - return stringBuffer.toString(); + return stringBuilder.toString(); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12117); } } - /** + /** * RSA公钥解密 * @param publicKeyString 公钥 * @param content 已加密内容 * @return 解密后内容 */ + @Deprecated public static String rsaDecryptByPublic(String publicKeyString, String content) { try { @@ -304,13 +353,13 @@ public static String rsaDecryptByPublic(String publicKeyString, String content) int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8; byte[] contentBytes = hexStringToBytes(content); byte[][] arrays = splitBytes(contentBytes, splitLength); - StringBuffer stringBuffer = new StringBuffer(); + StringBuilder stringBuilder = new StringBuilder(); for (byte[] array : arrays) { - stringBuffer.append(new String(cipher.doFinal(array))); + stringBuilder.append(new String(cipher.doFinal(array))); } - return stringBuffer.toString(); + return stringBuilder.toString(); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12118); } } @@ -320,6 +369,7 @@ public static String rsaDecryptByPublic(String publicKeyString, String content) * @param content 已加密内容 * @return 解密后内容 */ + @Deprecated public static String rsaDecryptByPrivate(String privateKeyString, String content) { try { PrivateKey privateKey = getPrivateKeyFromString(privateKeyString); @@ -330,18 +380,18 @@ public static String rsaDecryptByPrivate(String privateKeyString, String content int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8; byte[] contentBytes = hexStringToBytes(content); byte[][] arrays = splitBytes(contentBytes, splitLength); - StringBuffer stringBuffer = new StringBuffer(); + StringBuilder stringBuilder = new StringBuilder(); for (byte[] array : arrays) { - stringBuffer.append(new String(cipher.doFinal(array))); + stringBuilder.append(new String(cipher.doFinal(array))); } - return stringBuffer.toString(); + return stringBuilder.toString(); } catch (Exception e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12119); } } - - // ---------- 获取*钥 + + // ---------- 获取*钥 /** 根据公钥字符串获取 公钥对象 */ private static PublicKey getPublicKeyFromString(String key) @@ -355,9 +405,7 @@ private static PublicKey getPublicKeyFromString(String key) KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); - PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); - - return publicKey; + return keyFactory.generatePublic(x509KeySpec); } /** 根据私钥字符串获取 私钥对象 */ @@ -372,9 +420,7 @@ private static PrivateKey getPrivateKeyFromString(String key) KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); - PrivateKey privateKey = keyFactory.generatePrivate(x509KeySpec); - - return privateKey; + return keyFactory.generatePrivate(x509KeySpec); } @@ -388,7 +434,7 @@ private static byte[][] splitBytes(byte[] bytes, int splitLength) { // 数据拆分后的组数,余数不为0时加1 int quotient = remainder != 0 ? bytes.length / splitLength + 1 : bytes.length / splitLength; byte[][] arrays = new byte[quotient][]; - byte[] array = null; + byte[] array; for (int i = 0; i < quotient; i++) { // 如果是最后一组(quotient-1),同时余数不等于0,就将最后一组设置为remainder的长度 if (i == quotient - 1 && remainder != 0) { @@ -406,10 +452,10 @@ private static byte[][] splitBytes(byte[] bytes, int splitLength) { /** 将字节数组转换成16进制字符串 */ private static String bytesToHexString(byte[] bytes) { - StringBuffer sb = new StringBuffer(bytes.length); - String temp = null; - for (int i = 0; i < bytes.length; i++) { - temp = Integer.toHexString(0xFF & bytes[i]); + StringBuilder sb = new StringBuilder(bytes.length); + String temp; + for (byte aByte : bytes) { + temp = Integer.toHexString(0xFF & aByte); if (temp.length() < 2) { sb.append(0); } @@ -438,5 +484,5 @@ private static byte toByte(char c) { return (byte) "0123456789ABCDEF".indexOf(c); } - + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpTemplate.java new file mode 100644 index 000000000..97064b1cc --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpTemplate.java @@ -0,0 +1,193 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.secure.totp; + +import cn.dev33.satoken.exception.TotpAuthException; +import cn.dev33.satoken.secure.SaBase32Util; +import cn.dev33.satoken.util.StrFormatter; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.time.Instant; + +/** + * TOTP 算法类,支持 生成/验证 动态一次性密码 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTotpTemplate { + + /** + * 时间窗口步长(秒) + */ + public int timeStep = 30; + + /** + * 生成的验证码位数 + */ + public int codeDigits = 6; + + /** + * 哈希算法(HmacSHA1、HmacSHA256等) + */ + public String hmacAlgorithm = "HmacSHA1"; + + /** + * 密钥长度(字节,推荐16或32) + */ + public int secretKeyLength = 16; + + /** + * 构造函数 (使用默认参数) + */ + public SaTotpTemplate() { + } + + /** + * 构造函数 (使用自定义参数) + * + * @param timeStep 时间窗口步长(秒) + * @param codeDigits 生成的验证码位数 + * @param hmacAlgorithm 哈希算法(HmacSHA1、HmacSHA256等) + * @param secretKeyLength 密钥长度(字节,推荐16或32) + */ + public SaTotpTemplate(int timeStep, int codeDigits, String hmacAlgorithm, int secretKeyLength) { + this.timeStep = timeStep; + this.codeDigits = codeDigits; + this.hmacAlgorithm = hmacAlgorithm; + this.secretKeyLength = secretKeyLength; + } + + + /** + * 生成随机密钥(Base32编码) + * + * @return / + */ + public String generateSecretKey() { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[secretKeyLength]; + random.nextBytes(bytes); + return SaBase32Util.encodeBytesToString(bytes).replace("=", ""); + } + + /** + * 生成当前时间的 TOTP 验证码 + * + * @param secretKey Base32 编码的密钥 + * @return / + */ + public String _generateTOTP(String secretKey) { + return _generateTOTP(secretKey, Instant.now().getEpochSecond()); + } + + /** + * 判断用户输入的 TOTP 是否有效 + * + * @param secretKey Base32编码的密钥 + * @param code 用户输入的验证码 + * @param timeWindowOffset 允许的时间窗口偏移量(如1表示允许前后各1个时间窗口) + * @return / + */ + public boolean validateTOTP(String secretKey, String code, int timeWindowOffset) { + long currentWindow = Instant.now().getEpochSecond() / timeStep; + for (int i = -timeWindowOffset; i <= timeWindowOffset; i++) { + String calculatedCode = _generateTOTP(secretKey, (currentWindow + i) * timeStep); + if (calculatedCode.equals(code)) { + return true; + } + } + return false; + } + + /** + * 校验用户输入的TOTP是否有效,如果无效则抛出异常 + * + * @param secretKey Base32编码的密钥 + * @param code 用户输入的验证码 + * @param timeWindowOffset 允许的时间窗口偏移量(如1表示允许前后各1个时间窗口) + */ + public void checkTOTP(String secretKey, String code, int timeWindowOffset) { + if (!validateTOTP(secretKey, code, timeWindowOffset)) { + throw new TotpAuthException(); + } + } + + /** + * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{account}?secret={secretKey}) + * + * @param account 账户名 + * @return / + */ + public String generateGoogleSecretKey(String account) { + return generateGoogleSecretKey(account, generateSecretKey()); + } + + /** + * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{account}?secret={secretKey}) + * + * @param account 账户名 + * @param secretKey TOTP 秘钥 + * @return / + */ + public String generateGoogleSecretKey(String account, String secretKey) { + return StrFormatter.format("otpauth://totp/{}?secret={}", account, secretKey); + } + + /** + * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{issuer}:{account}?secret={secretKey}&issuer={issuer}) + * + * @param account 账户名 + * @param issuer 签发者 + * @param secretKey TOTP 秘钥 + * @return / + */ + public String generateGoogleSecretKey(String account, String issuer, String secretKey) { + return StrFormatter.format("otpauth://totp/{}:{}?secret={}&issuer={}", issuer, account, secretKey, issuer); + } + + protected String _generateTOTP(String secretKey, long time) { + // Base32解码密钥 + byte[] keyBytes = SaBase32Util.decodeStringToBytes(secretKey); + byte[] counterBytes = ByteBuffer.allocate(8).putLong(time / timeStep).array(); + + try { + // 计算HMAC哈希 + Mac hmac = Mac.getInstance(hmacAlgorithm); + hmac.init(new SecretKeySpec(keyBytes, hmacAlgorithm)); + byte[] hash = hmac.doFinal(counterBytes); + + // 动态截断(RFC 6238) + int offset = hash[hash.length - 1] & 0xF; + int binary = ((hash[offset] & 0x7F) << 24) + | ((hash[offset + 1] & 0xFF) << 16) + | ((hash[offset + 2] & 0xFF) << 8) + | (hash[offset + 3] & 0xFF); + + // 生成指定位数的验证码 + int otp = binary % (int) Math.pow(10, codeDigits); + return String.format("%0" + codeDigits + "d", otp); + + } catch (GeneralSecurityException e) { + throw new RuntimeException("TOTP生成失败", e); + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpUtil.java new file mode 100644 index 000000000..958cc9bea --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpUtil.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.secure.totp; + +import cn.dev33.satoken.SaManager; + +/** + * TOTP 工具类,支持 生成/验证 动态一次性密码 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTotpUtil { + + /** + * 生成随机密钥(Base32编码) + * + * @return / + */ + public static String generateSecretKey() { + return SaManager.getSaTotpTemplate().generateSecretKey(); + } + + /** + * 生成当前时间的TOTP验证码 + * + * @param secretKey Base32编码的密钥 + * @return / + */ + public static String generateTOTP(String secretKey) { + return SaManager.getSaTotpTemplate()._generateTOTP(secretKey); + } + + /** + * 判断用户输入的TOTP是否有效 + * + * @param secretKey Base32编码的密钥 + * @param code 用户输入的验证码 + * @param timeWindowOffset 允许的时间窗口偏移量(如1表示允许前后各1个时间窗口) + * @return / + */ + public static boolean validateTOTP(String secretKey, String code, int timeWindowOffset) { + return SaManager.getSaTotpTemplate().validateTOTP(secretKey, code, timeWindowOffset); + } + + /** + * 校验用户输入的TOTP是否有效,如果无效则抛出异常 + * + * @param secretKey Base32编码的密钥 + * @param code 用户输入的验证码 + * @param timeWindowOffset 允许的时间窗口偏移量(如1表示允许前后各1个时间窗口) + */ + public static void checkTOTP(String secretKey, String code, int timeWindowOffset) { + SaManager.getSaTotpTemplate().checkTOTP(secretKey, code, timeWindowOffset); + } + + /** + * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{account}?secret={secretKey}) + * + * @param account 账户名 + * @return / + */ + public static String generateGoogleSecretKey(String account) { + return SaManager.getSaTotpTemplate().generateGoogleSecretKey(account); + } + + /** + * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{account}?secret={secretKey}) + * + * @param account 账户名 + * @param secretKey TOTP 秘钥 + * @return / + */ + public static String generateGoogleSecretKey(String account, String secretKey) { + return SaManager.getSaTotpTemplate().generateGoogleSecretKey(account, secretKey); + } + + /** + * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{issuer}:{account}?secret={secretKey}&issuer={issuer}) + * + * @param account 账户名 + * @param issuer 签发者 + * @param secretKey TOTP 秘钥 + * @return / + */ + public static String generateGoogleSecretKey(String account, String issuer, String secretKey) { + return SaManager.getSaTotpTemplate().generateGoogleSecretKey(account, issuer, secretKey); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/serializer/SaSerializerTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/SaSerializerTemplate.java new file mode 100644 index 000000000..064237cff --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/SaSerializerTemplate.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer; + +/** + * 序列化器 + * + * @author click33 + * @since 1.41.0 + */ +public interface SaSerializerTemplate { + + /** + * 序列化:对象 -> 字符串 + * + * @param obj / + * @return / + */ + String objectToString(Object obj); + + /** + * 反序列化:字符串 → 对象 + * + * @param str / + * @return / + */ + Object stringToObject(String str); + + /** + * 反序列化:字符串 → 对象 (指定类型) + *

+ * 此方法目前仅为 json 序列化实现类 在 反序列化对象 传递类型信息 + *

+ * + * @param str / + * @return / + */ + @SuppressWarnings("unchecked") + default T stringToObject(String str, Class type) { + return (T)stringToObject(str); + }; + + + /** + * 序列化:对象 -> 字节数组 + * + * @param obj / + * @return / + */ + byte[] objectToBytes(Object obj); + + /** + * 反序列化:字节数组 → 对象 + * + * @param bytes / + * @return / + */ + Object bytesToObject(byte[] bytes); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdk.java b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdk.java new file mode 100644 index 000000000..9bca2909f --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdk.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer.impl; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.serializer.SaSerializerTemplate; + +import java.io.*; + +/** + * 序列化器次级实现: jdk序列化 + * + * @author click33 + * @since 1.41.0 + */ +public interface SaSerializerTemplateForJdk extends SaSerializerTemplate { + + @Override + default String objectToString(Object obj) { + byte[] bytes = objectToBytes(obj); + if (bytes == null) { + return null; + } + return bytesToString(bytes); + } + + @Override + default Object stringToObject(String str) { + if(str == null) { + return null; + } + byte[] bytes = stringToBytes(str); + return bytesToObject(bytes); + } + + @Override + default byte[] objectToBytes(Object obj) { + if (obj == null) { + return null; + } + try ( + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos) + ) { + oos.writeObject(obj); + return baos.toByteArray(); + } catch (IOException e) { + throw new SaTokenException(e); + } + } + + @Override + default Object bytesToObject(byte[] bytes) { + if(bytes == null) { + return null; + } + try ( + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais) + ) { + return ois.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new SaTokenException(e); + } + } + + /** + * byte[] 转换为 String + * @param bytes / + * @return / + */ + String bytesToString(byte[] bytes); + + /** + * String 转换为 byte[] + * @param str / + * @return / + */ + byte[] stringToBytes(String str); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseBase64.java b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseBase64.java new file mode 100644 index 000000000..cb99bfa4d --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseBase64.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer.impl; + +import java.util.Base64; + +/** + * 序列化器: jdk序列化、Base64 编码 (体积+33%) + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerTemplateForJdkUseBase64 implements SaSerializerTemplateForJdk { + + @Override + public String bytesToString(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + @Override + public byte[] stringToBytes(String str) { + return Base64.getDecoder().decode(str); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseHex.java b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseHex.java new file mode 100644 index 000000000..69e93f563 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseHex.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer.impl; + +import cn.dev33.satoken.util.SaHexUtil; + +/** + * 序列化器: jdk序列化、16 进制编码 (体积+100%) + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerTemplateForJdkUseHex implements SaSerializerTemplateForJdk { + + @Override + public String bytesToString(byte[] bytes) { + return SaHexUtil.bytesToHex(bytes); + } + + @Override + public byte[] stringToBytes(String str) { + return SaHexUtil.hexToBytes(str); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseISO_8859_1.java b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseISO_8859_1.java new file mode 100644 index 000000000..ee0d23c43 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseISO_8859_1.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer.impl; + +import java.nio.charset.StandardCharsets; + +/** + * 序列化器: jdk序列化、ISO-8859-1 编码 (体积无变化) + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerTemplateForJdkUseISO_8859_1 implements SaSerializerTemplateForJdk { + + @Override + public String bytesToString(byte[] bytes) { + return new String(bytes, StandardCharsets.ISO_8859_1); + } + + @Override + public byte[] stringToBytes(String str) { + return str.getBytes(StandardCharsets.ISO_8859_1); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJson.java b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJson.java new file mode 100644 index 000000000..9e038d503 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJson.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer.impl; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.exception.ApiDisabledException; +import cn.dev33.satoken.serializer.SaSerializerTemplate; + +/** + * 序列化器: 使用 json 转换器 + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerTemplateForJson implements SaSerializerTemplate { + + @Override + public String objectToString(Object obj) { + return SaManager.getSaJsonTemplate().objectToJson(obj); + } + + @Override + public Object stringToObject(String str) { + return SaManager.getSaJsonTemplate().jsonToObject(str); + } + + @Override + public T stringToObject(String str, Class type) { + return SaManager.getSaJsonTemplate().jsonToObject(str, type); + } + + @Override + public byte[] objectToBytes(Object obj) { + throw new ApiDisabledException("json 序列化器不支持 Object -> byte[]"); + } + + @Override + public Object bytesToObject(byte[] bytes) { + throw new ApiDisabledException("json 序列化器不支持 byte[] -> Object"); + } + +} 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 d7bcd047d..ff2798e3f 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 @@ -1,70 +1,145 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session; -import java.io.Serializable; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Vector; -import java.util.concurrent.ConcurrentHashMap; - import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.fun.SaRetFunction; +import cn.dev33.satoken.application.SaSetValueInterface; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.fun.SaTwoParamFunction; +import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.util.SaFoxUtil; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + /** - * Session Model + * Session Model,会话作用域的读取值对象 * - * @author kong + *

在一次会话范围内: 存值、取值。数据在注销登录后失效。

+ *

+ * 在 Sa-Token 中,SaSession 分为三种,分别是:
+ * - Account-Session: 指的是框架为每个 账号id 分配的 SaSession。
+ * - Token-Session: 指的是框架为每个 token 分配的 SaSession。
+ * - Custom-Session: 指的是以一个 特定的值 作为SessionId,来分配的 SaSession。
+ *
+ * 注意:以上分类仅为框架设计层面的概念区分,实际上它们的数据存储格式都是一致的。 + *

* + * @author click33 + * @since 1.10.0 */ -public class SaSession implements Serializable { +public class SaSession implements SaSetValueInterface, Serializable { + /** + * + */ private static final long serialVersionUID = 1L; - /** 此Session的id */ + /** + * 在 SaSession 上存储用户对象时建议使用的 key + */ + public static final String USER = "USER"; + + /** + * 在 SaSession 上存储角色列表时建议使用的 key + */ + public static final String ROLE_LIST = "ROLE_LIST"; + + /** + * 在 SaSession 上存储权限列表时建议使用的 key + */ + public static final String PERMISSION_LIST = "PERMISSION_LIST"; + + /** + * 此 SaSession 的 id + */ private String id; - /** 此Session的创建时间 */ + /** + * 此 SaSession 的 类型 + */ + private String type; + + /** + * 所属 loginType + */ + private String loginType; + + /** + * 所属 loginId (当此 SaSession 属于 Account-Session 时,此值有效) + */ + private Object loginId; + + /** + * 所属 Token (当此 SaSession 属于 Token-Session 时,此值有效) + */ + private String token; + + /** + * 当前账号历史总计登录设备数量 (当此 SaSession 属于 Account-Session 时,此值有效) + */ + private int historyTerminalCount; + + /** + * 此 SaSession 的创建时间(13位时间戳) + */ private long createTime; - /** 此Session的所有挂载数据 */ - private final Map dataMap = new ConcurrentHashMap<>(); + /** + * 所有挂载数据 + */ + private Map dataMap = new ConcurrentHashMap<>(); + // ----------------------- 构建相关 /** - * 构建一个Session对象 + * 构建一个 Session 对象 */ public SaSession() { /* - * 当Session从Redis中反序列化取出时,框架会误以为创建了新的Session, + * 当 Session 从 Redis 中反序列化取出时,框架会误以为创建了新的Session, * 因此此处不可以调用this(null); 避免监听器收到错误的通知 */ // this(null); } /** - * 构建一个Session对象 + * 构建一个 Session 对象 * @param id Session的id */ public SaSession(String id) { this.id = id; this.createTime = System.currentTimeMillis(); - // $$ 通知监听器 - SaManager.getSaTokenListener().doCreateSession(id); + // $$ 发布事件 + SaTokenEventCenter.doCreateSession(id); } /** - * 获取此Session的id - * @return 此会话的id + * 获取:此 SaSession 的 id + * @return / */ public String getId() { - return id; + return this.id; } /** - * 写入此Session的id - * @param id SessionId + * 写入:此 SaSession 的 id + * @param id / * @return 对象自身 */ public SaSession setId(String id) { @@ -73,16 +148,90 @@ public SaSession setId(String id) { } /** - * 返回当前会话创建时间 - * @return 时间戳 + * 获取:此 SaSession 的 类型 + * + * @return / + */ + public String getType() { + return this.type; + } + + /** + * 设置:此 SaSession 的 类型 + * + * @param type / + * @return 对象自身 + */ + public SaSession setType(String type) { + this.type = type; + return this; + } + + /** + * 获取:所属 loginType + * @return / + */ + public String getLoginType() { + return this.loginType; + } + + /** + * 设置:所属 loginType + * @param loginType / + * @return 对象自身 + */ + public SaSession setLoginType(String loginType) { + this.loginType = loginType; + return this; + } + + /** + * 获取:所属 loginId (当此 SaSession 属于 Account-Session 时,此值有效) + * @return / + */ + public Object getLoginId() { + return this.loginId; + } + + /** + * 设置:所属 loginId (当此 SaSession 属于 Account-Session 时,此值有效) + * @param loginId / + * @return 对象自身 + */ + public SaSession setLoginId(Object loginId) { + this.loginId = loginId; + return this; + } + + /** + * 获取:所属 Token (当此 SaSession 属于 Token-Session 时,此值有效) + * @return / + */ + public String getToken() { + return this.token; + } + + /** + * 设置:所属 Token (当此 SaSession 属于 Token-Session 时,此值有效) + * @param token / + * @return 对象自身 + */ + public SaSession setToken(String token) { + this.token = token; + return this; + } + + /** + * 返回:当前 SaSession 的创建时间(13位时间戳) + * @return / */ public long getCreateTime() { - return createTime; + return this.createTime; } /** - * 写入此Session的创建时间 - * @param createTime 时间戳 + * 写入:此 SaSession 的创建时间(13位时间戳) + * @param createTime / * @return 对象自身 */ public SaSession setCreateTime(long createTime) { @@ -91,67 +240,168 @@ public SaSession setCreateTime(long createTime) { } - // ----------------------- TokenSign相关 + // ----------------------- SaTerminalInfo 相关 /** - * 此Session绑定的token签名列表 + * 登录终端信息列表 */ - private final List tokenSignList = new Vector<>(); + private List terminalList = new Vector<>(); /** - * 返回token签名列表的拷贝副本 + * 写入登录终端信息列表 + * @param terminalList / + */ + public void setTerminalList(List terminalList) { + this.terminalList = terminalList; + } + + /** + * 获取登录终端信息列表 * - * @return token签名列表 + * @return / */ - public List getTokenSignList() { - return new Vector<>(tokenSignList); + public List getTerminalList() { + return terminalList; } /** - * 查找一个token签名 + * 获取 登录终端信息列表 (拷贝副本) * - * @param tokenValue token值 - * @return 查找到的tokenSign + * @return / */ - public TokenSign getTokenSign(String tokenValue) { - for (TokenSign tokenSign : getTokenSignList()) { - if (tokenSign.getValue().equals(tokenValue)) { - return tokenSign; + public List terminalListCopy() { + return new ArrayList<>(terminalList); + } + + /** + * 获取 登录终端信息列表 (拷贝副本),根据 deviceType 筛选 + * + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return / + */ + public List getTerminalListByDeviceType(String deviceType) { + // 返回全部 + if(deviceType == null) { + return terminalListCopy(); + } + // 返回筛选后的 + List copyList = terminalListCopy(); + List newList = new ArrayList<>(); + for (SaTerminalInfo terminal : copyList) { + if(SaFoxUtil.equals(terminal.getDeviceType(), deviceType)) { + newList.add(terminal); } } - return null; + return newList; } /** - * 添加一个token签名 + * 获取 登录终端 token 列表 * - * @param tokenSign token签名 + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return 此 loginId 的所有登录 token */ - public void addTokenSign(TokenSign tokenSign) { - // 如果已经存在于列表中,则无需再次添加 - for (TokenSign tokenSign2 : getTokenSignList()) { - if (tokenSign2.getValue().equals(tokenSign.getValue())) { - return; + public List getTokenValueListByDeviceType(String deviceType) { + List tokenValueList = new ArrayList<>(); + for (SaTerminalInfo terminal : getTerminalListByDeviceType(deviceType)) { + tokenValueList.add(terminal.getTokenValue()); + } + return tokenValueList; + } + + /** + * 查找一个终端信息,根据 tokenValue + * + * @param tokenValue / + * @return / + */ + public SaTerminalInfo getTerminal(String tokenValue) { + for (SaTerminalInfo terminal : terminalListCopy()) { + if (SaFoxUtil.equals(terminal.getTokenValue(), tokenValue)) { + return terminal; } } - // 添加并更新 - tokenSignList.add(tokenSign); + return null; + } + + /** + * 添加一个终端信息 + * + * @param terminalInfo / + */ + public void addTerminal(SaTerminalInfo terminalInfo) { + // 根据 tokenValue 值查重,如果存在旧的,则先删除 + SaTerminalInfo oldTerminal = getTerminal(terminalInfo.getTokenValue()); + if(oldTerminal != null) { + terminalList.remove(oldTerminal); + } + // 然后添加新的 + this.historyTerminalCount++; + terminalInfo.setIndex(this.historyTerminalCount); + terminalList.add(terminalInfo); update(); } /** - * 移除一个token签名 + * 移除一个终端信息 * - * @param tokenValue token名称 + * @param tokenValue token值 */ - public void removeTokenSign(String tokenValue) { - TokenSign tokenSign = getTokenSign(tokenValue); - if (tokenSignList.remove(tokenSign)) { + public void removeTerminal(String tokenValue) { + SaTerminalInfo terminalInfo = getTerminal(tokenValue); + if (terminalList.remove(terminalInfo)) { update(); } } - + /** + * 获取 当前账号历史总计登录设备数量 (当此 SaSession 属于 Account-Session 时,此值有效) + * + * @return / + */ + public int getHistoryTerminalCount() { + return this.historyTerminalCount; + } + + /** + * 设置 当前账号历史总计登录设备数量 (当此 SaSession 属于 Account-Session 时,此值有效) + * + * @param historyTerminalCount / + */ + public void setHistoryTerminalCount(int historyTerminalCount) { + this.historyTerminalCount = historyTerminalCount; + } + + /** + * 遍历 terminalList 列表,执行特定函数 + * + * @param function 需要执行的函数 + */ + public void forEachTerminalList(SaTwoParamFunction function) { + for (SaTerminalInfo terminalInfo: terminalListCopy()) { + function.run(this, terminalInfo); + } + } + + + /** + * 判断指定设备 id 是否为可信任设备 + * @param deviceId / + * @return / + */ + public boolean isTrustDeviceId(String deviceId) { + if(SaFoxUtil.isEmpty(deviceId)) { + return false; + } + for (SaTerminalInfo terminal : terminalListCopy()) { + if (SaFoxUtil.equals(terminal.getDeviceId(), deviceId)) { + return true; + } + } + return false; + } + + // ----------------------- 一些操作 /** @@ -164,13 +414,13 @@ public void update() { /** 注销Session (从持久库删除) */ public void logout() { SaManager.getSaTokenDao().deleteSession(this.id); - // $$ 通知监听器 - SaManager.getSaTokenListener().doLogoutSession(id); + // $$ 发布事件 + SaTokenEventCenter.doLogoutSession(id); } - /** 当Session上的tokenSign数量为零时,注销会话 */ - public void logoutByTokenSignCountToZero() { - if (tokenSignList.size() == 0) { + /** 当 Session 上的 SaTerminalInfo 数量为零时,注销会话 */ + public void logoutByTerminalCountToZero() { + if (terminalList.isEmpty()) { logout(); } } @@ -179,7 +429,7 @@ public void logoutByTokenSignCountToZero() { * 获取此Session的剩余存活时间 (单位: 秒) * @return 此Session的剩余存活时间 (单位: 秒) */ - public long getTimeout() { + public long timeout() { return SaManager.getSaTokenDao().getSessionTimeout(this.id); } @@ -192,162 +442,60 @@ public void updateTimeout(long timeout) { } /** - * 修改此Session的最小剩余存活时间 (只有在Session的过期时间低于指定的minTimeout时才会进行修改) + * 修改此Session的最小剩余存活时间 (只有在 Session 的过期时间低于指定的 minTimeout 时才会进行修改) * @param minTimeout 过期时间 (单位: 秒) */ public void updateMinTimeout(long minTimeout) { - if(getTimeout() < minTimeout) { - SaManager.getSaTokenDao().updateSessionTimeout(this.id, minTimeout); + long min = trans(minTimeout); + long curr = trans(timeout()); + if(curr < min) { + updateTimeout(minTimeout); } } /** - * 修改此Session的最大剩余存活时间 (只有在Session的过期时间高于指定的maxTimeout时才会进行修改) + * 修改此Session的最大剩余存活时间 (只有在 Session 的过期时间高于指定的 maxTimeout 时才会进行修改) * @param maxTimeout 过期时间 (单位: 秒) */ public void updateMaxTimeout(long maxTimeout) { - if(getTimeout() > maxTimeout) { - SaManager.getSaTokenDao().updateSessionTimeout(this.id, maxTimeout); + long max = trans(maxTimeout); + long curr = trans(timeout()); + if(curr > max) { + updateTimeout(maxTimeout); } } - - - // ----------------------- 存取值 (类型转换) - - // ---- 取值 /** - * 取值 - * @param key key - * @return 值 + * value为 -1 时返回 Long.MAX_VALUE,否则原样返回 + * @param value / + * @return / */ - public Object get(String key) { - return dataMap.get(key); - } - - /** - * - * 取值 (指定默认值) - * @param 默认值的类型 - * @param key key - * @param defaultValue 取不到值时返回的默认值 - * @return 值 - */ - public T get(String key, T defaultValue) { - return getValueByDefaultValue(get(key), defaultValue); - } - - /** - * - * 取值 (如果值为null,则执行fun函数获取值) - * @param 返回值的类型 - * @param key key - * @param fun 值为null时执行的函数 - * @return 值 - */ - @SuppressWarnings("unchecked") - public T get(String key, SaRetFunction fun) { - Object value = get(key); - if(value == null) { - value = fun.run(); - set(key, value); - } - return (T) value; - } - - /** - * 取值 (转String类型) - * @param key key - * @return 值 - */ - public String getString(String key) { - Object value = get(key); - if(value == null) { - return null; - } - return String.valueOf(value); - } - - /** - * 取值 (转int类型) - * @param key key - * @return 值 - */ - public int getInt(String key) { - return getValueByDefaultValue(get(key), 0); - } - - /** - * 取值 (转long类型) - * @param key key - * @return 值 - */ - public long getLong(String key) { - return getValueByDefaultValue(get(key), 0L); + protected long trans(long value) { + return value == SaTokenDao.NEVER_EXPIRE ? Long.MAX_VALUE : value; } - /** - * 取值 (转double类型) - * @param key key - * @return 值 - */ - public double getDouble(String key) { - return getValueByDefaultValue(get(key), 0.0); - } - - /** - * 取值 (转float类型) - * @param key key - * @return 值 - */ - public float getFloat(String key) { - return getValueByDefaultValue(get(key), 0.0f); - } - /** - * 取值 (指定转换类型) - * @param 泛型 - * @param key key - * @param cs 指定转换类型 - * @return 值 - */ - public T getModel(String key, Class cs) { - return SaFoxUtil.getValueByType(get(key), cs); - } + // ----------------------- 存取值 (类型转换) + // ---- 重写接口方法 + /** - * 取值 (指定转换类型, 并指定值为Null时返回的默认值) - * @param 泛型 + * 取值 * @param key key - * @param cs 指定转换类型 - * @param defaultValue 值为Null时返回的默认值 * @return 值 */ - @SuppressWarnings("unchecked") - public T getModel(String key, Class cs, Object defaultValue) { - Object value = get(key); - if(valueIsNull(value)) { - return (T)defaultValue; - } - return SaFoxUtil.getValueByType(value, cs); - } - - /** - * 返回当前Session的所有key - * - * @return 所有值的key列表 - */ - public Set keys() { - return dataMap.keySet(); + @Override + public Object get(String key) { + return dataMap.get(key); } - // ---- 其他 /** - * 写值 + * 写值 * @param key 名称 * @param value 值 * @return 对象自身 */ + @Override public SaSession set(String key, Object value) { dataMap.put(key, value); update(); @@ -355,190 +503,83 @@ public SaSession set(String key, Object value) { } /** - * 写值(只有在此key原本无值的时候才会写入) + * 写值 (只有在此 key 原本无值的情况下才会写入) * @param key 名称 * @param value 值 * @return 对象自身 */ - public SaSession setDefaultValue(String key, Object value) { - if(has(key) == false) { + @Override + public SaSession setByNull(String key, Object value) { + if( ! has(key)) { dataMap.put(key, value); update(); } return this; } - /** - * 是否含有某个key - * @param key has - * @return 是否含有 - */ - public boolean has(String key) { - return !valueIsNull(get(key)); - } - /** * 删值 * @param key 要删除的key * @return 对象自身 */ + @Override public SaSession delete(String key) { dataMap.remove(key); update(); return this; } - /** - * 清空所有值 - */ - public void clear() { - dataMap.clear(); - update(); - } - - /** - * 获取数据挂载集合(如果更新map里的值,请调用session.update()方法避免产生脏数据 ) - * - * @return 返回底层储存值的map对象 - */ - public Map getDataMap() { - return dataMap; - } - /** - * 写入数据集合 (不改变底层对象,只将此dataMap所有数据进行替换) - * @param dataMap 数据集合 - */ - public void refreshDataMap(Map dataMap) { - this.dataMap.clear(); - this.dataMap.putAll(dataMap); - this.update(); - } - - - // --------- 工具方法 + // ----------------------- 其它方法 /** - * 判断一个值是否为null - * @param value 指定值 - * @return 此value是否为null - */ - public boolean valueIsNull(Object value) { - return value == null || value.equals(""); - } - - /** - * 根据默认值来获取值 - * @param 泛型 - * @param value 值 - * @param defaultValue 默认值 - * @return 转换后的值 + * 返回当前 Session 挂载数据的所有 key + * + * @return key 列表 */ - @SuppressWarnings("unchecked") - protected T getValueByDefaultValue(Object value, T defaultValue) { - - // 如果 obj 为 null,则直接返回默认值 - if(valueIsNull(value)) { - return (T)defaultValue; - } - - // 开始转换 - Class cs = (Class) defaultValue.getClass(); - return SaFoxUtil.getValueByType(value, cs); + public Set keys() { + return dataMap.keySet(); } - - - - // ----------------------- 旧API - /** - *

此函数设计已过时,未来版本可能移除此类,请及时更换为: session.set(key)

- * 写入一个值 - * - * @param key 名称 - * @param value 值 + * 清空所有挂载数据 */ - @Deprecated - public void setAttribute(String key, Object value) { - dataMap.put(key, value); + public void clear() { + dataMap.clear(); update(); } /** - *

此函数设计已过时,未来版本可能移除此类,请及时更换为: session.get(key)

- * 取出一个值 + * 获取数据挂载集合(如果更新map里的值,请调用 session.update() 方法避免产生脏数据 ) * - * @param key 名称 - * @return 值 + * @return 返回底层储存值的map对象 */ - @Deprecated - public Object getAttribute(String key) { - return dataMap.get(key); - } - - /** - *

此函数设计已过时,未来版本可能移除此类,请及时更换为: session.get(key, defaultValue)

- * 取值,并指定取不到值时的默认值 - * - * @param key 名称 - * @param defaultValue 取不到值的时候返回的默认值 - * @return value - */ - @Deprecated - public Object getAttribute(String key, Object defaultValue) { - Object value = getAttribute(key); - if (value != null) { - return value; - } - return defaultValue; + public Map getDataMap() { + return dataMap; } /** - *

此函数设计已过时,未来版本可能移除此类,请及时更换为: session.delete(key)

- * 移除一个值 + * 设置数据挂载集合 (改变底层对象引用,将 dataMap 整个对象替换) + * @param dataMap 数据集合 * - * @param key 要移除的值的名字 - */ - @Deprecated - public void removeAttribute(String key) { - dataMap.remove(key); - update(); - } - - /** - *

此函数设计已过时,未来版本可能移除此类,请及时更换为: session.clear()

- * 清空所有值 + * @return 对象自身 */ - @Deprecated - public void clearAttribute() { - dataMap.clear(); - update(); + public SaSession setDataMap(Map dataMap) { + this.dataMap = dataMap; + return this; } /** - *

此函数设计已过时,未来版本可能移除此类,请及时更换为: session.has(key)

- * 是否含有指定key - * - * @param key 是否含有指定值 - * @return 是否含有 + * 写入数据集合 (不改变底层对象引用,只将此 dataMap 所有数据进行替换) + * @param dataMap 数据集合 */ - @Deprecated - public boolean containsAttribute(String key) { - return dataMap.containsKey(key); + public SaSession refreshDataMap(Map dataMap) { + this.dataMap.clear(); + this.dataMap.putAll(dataMap); + this.update(); + return this; } - /** - *

此函数设计已过时,未来版本可能移除此类,请及时更换为: session.keys()

- * 返回当前session会话所有key - * - * @return 所有值的key列表 - */ - @Deprecated - public Set attributeKeys() { - return dataMap.keySet(); - } + // - - } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionCustomUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionCustomUtil.java index 21c6ec9d7..e10d4b119 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionCustomUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionCustomUtil.java @@ -1,23 +1,55 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session; import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; /** - * 自定义Session工具类 + * 自定义 SaSession 工具类,快捷的读取、操作自定义 SaSession * - * @author kong - * + *

样例: + *

+ * 		// 在一处代码写入数据 
+ * 		SaSession session = SaSessionCustomUtil.getSessionById("role-" + 1001);
+ * 		session.set("count", 1);
+ * 	
+ * 		// 在另一处代码获取数据 
+ * 		SaSession session = SaSessionCustomUtil.getSessionById("role-" + 1001);
+ * 		int count = session.getInt("count");
+ * 		System.out.println("count=" + count);
+ * 
+ * + * @author click33 + * @since 1.10.0 */ public class SaSessionCustomUtil { + private SaSessionCustomUtil() { + } + /** - * 添加上指定前缀,防止恶意伪造Session + * 添加上指定前缀,防止恶意伪造数据 */ public static String sessionKey = "custom"; /** - * 拼接Key: 自定义Session的Id - * + * 拼接Key: 在存储自定义 SaSession 时应该使用的 key + * * @param sessionId 会话id * @return sessionId */ @@ -26,9 +58,9 @@ public static String splicingSessionKey(String sessionId) { } /** - * 指定key的Session是否存在 + * 判断:指定 key 的 SaSession 是否存在 * - * @param sessionId Session的id + * @param sessionId SaSession 的 id * @return 是否存在 */ public static boolean isExists(String sessionId) { @@ -36,35 +68,36 @@ public static boolean isExists(String sessionId) { } /** - * 获取指定key的Session + * 获取指定 key 的 SaSession 对象, 如果此 SaSession 尚未在 DB 创建,isCreate 参数代表是否则新建并返回 * - * @param sessionId key - * @param isCreate 如果此Session尚未在DB创建,是否新建并返回 - * @return SaSession + * @param sessionId SaSession 的 id + * @param isCreate 如果此 SaSession 尚未在 DB 创建,是否新建并返回 + * @return SaSession 对象 */ public static SaSession getSessionById(String sessionId, boolean isCreate) { SaSession session = SaManager.getSaTokenDao().getSession(splicingSessionKey(sessionId)); if (session == null && isCreate) { - session = SaManager.getSaTokenAction().createSession(splicingSessionKey(sessionId)); + session = SaStrategy.instance.createSession.apply(splicingSessionKey(sessionId)); + session.setType(SaTokenConsts.SESSION_TYPE__CUSTOM); SaManager.getSaTokenDao().setSession(session, SaManager.getConfig().getTimeout()); } return session; } /** - * 获取指定key的Session, 如果此Session尚未在DB创建,则新建并返回 + * 获取指定 key 的 SaSession, 如果此 SaSession 尚未在 DB 创建,则新建并返回 * - * @param sessionId key - * @return session对象 + * @param sessionId SaSession 的 id + * @return SaSession 对象 */ public static SaSession getSessionById(String sessionId) { return getSessionById(sessionId, true); } /** - * 删除指定key的Session + * 删除指定 key 的 SaSession * - * @param sessionId 指定key + * @param sessionId SaSession 的 id */ public static void deleteSessionById(String sessionId) { SaManager.getSaTokenDao().deleteSession(splicingSessionKey(sessionId)); diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/session/SaTerminalInfo.java b/sa-token-core/src/main/java/cn/dev33/satoken/session/SaTerminalInfo.java new file mode 100644 index 000000000..f3d9eb264 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/session/SaTerminalInfo.java @@ -0,0 +1,265 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 登录设备终端信息 Model + * + * @author click33 + * @since 1.41.0 + */ +public class SaTerminalInfo implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1406115065849845073L; + + /** + * 登录会话索引值 (该账号第几个登录的设备, 从 1 开始) + */ + private int index; + + /** + * Token 值 + */ + private String tokenValue; + + /** + * 所属设备类型,例如:PC、WEB、HD、MOBILE、APP + */ + private String deviceType; + + /** + * 登录设备唯一标识,例如:kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM + */ + private String deviceId; + + /** + * 此次登录的自定义扩展数据 (只允许在登录前设定,登录后不建议更改) + */ + private Map extraData; + + /** + * 创建时间 + */ + private long createTime; + + /** + * 构建一个 + */ + public SaTerminalInfo() { + } + + /** + * 构建一个 + * + * @param index 登录会话索引值 (该账号第几个登录的设备) + * @param tokenValue Token 值 + * @param deviceType 所属设备类型 + * @param extraData 此客户端登录的挂载数据 + */ + public SaTerminalInfo(int index, String tokenValue, String deviceType, Map extraData) { + this.index = index; + this.tokenValue = tokenValue; + this.deviceType = deviceType; + this.extraData = extraData; + this.createTime = System.currentTimeMillis(); + } + + // 扩展方法 + + /** + * 此次登录的自定义扩展数据 (只允许在登录前设定,登录后不建议更改) + * @param key 键 + * @param value 值 + * @return 对象自身 + */ + public SaTerminalInfo setExtra(String key, Object value) { + if(this.extraData == null) { + this.extraData = new LinkedHashMap<>(); + } + this.extraData.put(key, value); + return this; + } + + /** + * 此次登录的自定义扩展数据 + * @param key 键 + * @return 扩展数据的值 + */ + public Object getExtra(String key) { + if(this.extraData == null) { + return null; + } + return this.extraData.get(key); + } + + /** + * 判断是否设置了扩展数据 + * @return / + */ + public boolean haveExtraData() { + return extraData != null && !extraData.isEmpty(); + } + + + + // -------------------- get/set -------------------- + + /** + * 获取 登录会话索引值 (该账号第几个登录的设备) + * + * @return index 登录会话索引值 (该账号第几个登录的设备) + */ + public int getIndex() { + return this.index; + } + + /** + * 设置 登录会话索引值 (该账号第几个登录的设备) + * + * @param index 登录会话索引值 (该账号第几个登录的设备) + * @return 对象自身 + */ + public SaTerminalInfo setIndex(int index) { + this.index = index; + return this; + } + + /** + * @return Token 值 + */ + public String getTokenValue() { + return tokenValue; + } + + /** + * 写入 Token 值 + * + * @param tokenValue / + * @return 对象自身 + */ + public SaTerminalInfo setTokenValue(String tokenValue) { + this.tokenValue = tokenValue; + return this; + } + + /** + * @return 所属设备类型 + */ + public String getDeviceType() { + return deviceType; + } + + /** + * 写入所属设备类型 + * + * @param deviceType / + * @return 对象自身 + */ + public SaTerminalInfo setDeviceType(String deviceType) { + this.deviceType = deviceType; + return this; + } + + /** + * 获取 登录设备唯一标识 + * + * @return deviceId 登录设备唯一标识 + */ + public String getDeviceId() { + return this.deviceId; + } + + /** + * 设置 登录设备唯一标识,例如:kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM + * + * @param deviceId 登录设备唯一标识,例如:kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM + * @return 对象自身 + */ + public SaTerminalInfo setDeviceId(String deviceId) { + this.deviceId = deviceId; + return this; + } + + /** + * 获取 此客户端登录的挂载数据 + * + * @return / + */ + public Map getExtraData() { + return this.extraData; + } + + /** + * 设置 此客户端登录的挂载数据 + * + * @param extraData / + * @return 对象自身 + */ + public SaTerminalInfo setExtraData(Map extraData) { + this.extraData = extraData; + return this; + } + + /** + * 获取 创建时间 + * + * @return createTime 创建时间 + */ + public long getCreateTime() { + return this.createTime; + } + + /** + * 设置 创建时间 + * + * @param createTime 创建时间 + * @return 对象自身 + */ + public SaTerminalInfo setCreateTime(long createTime) { + this.createTime = createTime; + return this; + } + + // + @Override + public String toString() { + return "SaTerminalInfo [" + + "index=" + index + + ", tokenValue='" + tokenValue + + ", deviceType='" + deviceType + + ", deviceId='" + deviceId + + ", extraData=" + extraData + + ", createTime=" + createTime + + ']'; + } + + /* + * Expand in the future: + * deviceName 登录设备端名称,一般为浏览器名称 + * systemName 登录设备操作系统名称 + * loginIp 登录IP地址 + * address 登录设备地理位置 + * loginTime 登录时间 + */ + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/session/TokenSign.java b/sa-token-core/src/main/java/cn/dev33/satoken/session/TokenSign.java deleted file mode 100644 index 851a888dd..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/session/TokenSign.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.dev33.satoken.session; - -import java.io.Serializable; - -/** - * Token签名 Model - * - * 挂在到SaSession上的token签名 - * - * @author kong - * - */ -public class TokenSign implements Serializable { - - /** - * - */ - private static final long serialVersionUID = 1406115065849845073L; - - /** - * token值 - */ - private String value; - - /** - * 所在设备标识 - */ - private String device; - - /** 构建一个 */ - public TokenSign() { - } - - /** - * 构建一个 - * - * @param value token值 - * @param device 所在设备标识 - */ - public TokenSign(String value, String device) { - this.value = value; - this.device = device; - } - - /** - * @return token value - */ - public String getValue() { - return value; - } - - /** - * @return token登录设备 - */ - public String getDevice() { - return device; - } - - @Override - public String toString() { - return "TokenSign [value=" + value + ", device=" + device + "]"; - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionDelegator.java b/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionDelegator.java new file mode 100644 index 000000000..93b07bd0d --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionDelegator.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session.raw; + +import cn.dev33.satoken.session.SaSession; + +/** + * SaSession 读写工具类 委托 + * + * @author click33 + * @since 1.42.0 + */ +public class SaRawSessionDelegator { + + /** + * raw session 类型 + */ + public String type; + + public SaRawSessionDelegator(String type) { + this.type = type; + } + + /** + * 判断:指定 SaSession 是否存在 + * + * @param valueId / + * @return 是否存在 + */ + public boolean isExists(Object valueId) { + return SaRawSessionUtil.isExists(type, valueId); + } + + /** + * 获取指定 SaSession 对象, 如果此 SaSession 尚未在 Cache 创建,isCreate 参数代表是否则新建并返回 + * + * @param valueId / + * @param isCreate 如果此 SaSession 尚未在 DB 创建,是否新建并返回 + * @return SaSession 对象 + */ + public SaSession getSessionById(Object valueId, boolean isCreate) { + return SaRawSessionUtil.getSessionById(type, valueId, isCreate); + } + + /** + * 获取指定 SaSession, 如果此 SaSession 尚未在 DB 创建,则新建并返回 + * + * @param valueId / + * @return SaSession 对象 + */ + public SaSession getSessionById(Object valueId) { + return SaRawSessionUtil.getSessionById(type, valueId); + } + + /** + * 删除指定 SaSession + * + * @param valueId / + */ + public void deleteSessionById(Object valueId) { + SaRawSessionUtil.deleteSessionById(type, valueId); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionUtil.java new file mode 100644 index 000000000..b1e5190b5 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionUtil.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session.raw; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.strategy.SaStrategy; + +/** + * SaSession 读写工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaRawSessionUtil { + + private SaRawSessionUtil() { + } + + /** + * 拼接Key: 在存储 SaSession 时应该使用的 key + * + * @param type 类型 + * @param valueId 唯一标识 + * @return sessionId + */ + public static String splicingSessionKey(String type, Object valueId) { + return SaManager.getConfig().getTokenName() + ":raw-session:" + type + ":" + valueId; + } + + /** + * 判断:指定 SaSession 是否存在 + * + * @param type / + * @param valueId / + * @return 是否存在 + */ + public static boolean isExists(String type, Object valueId) { + return SaManager.getSaTokenDao().getSession(splicingSessionKey(type, valueId)) != null; + } + + /** + * 获取指定 SaSession 对象, 如果此 SaSession 尚未在 Cache 创建,isCreate 参数代表是否则新建并返回 + * + * @param type / + * @param valueId / + * @param isCreate 如果此 SaSession 尚未在 DB 创建,是否新建并返回 + * @return SaSession 对象 + */ + public static SaSession getSessionById(String type, Object valueId, boolean isCreate) { + String sessionId = splicingSessionKey(type, valueId); + SaSession session = SaManager.getSaTokenDao().getSession(sessionId); + if (session == null && isCreate) { + session = SaStrategy.instance.createSession.apply(sessionId); + session.setType(type); + SaManager.getSaTokenDao().setSession(session, SaManager.getConfig().getTimeout()); + } + return session; + } + + /** + * 获取指定 SaSession, 如果此 SaSession 尚未在 DB 创建,则新建并返回 + * + * @param type / + * @param valueId / + * @return SaSession 对象 + */ + public static SaSession getSessionById(String type, Object valueId) { + return getSessionById(type, valueId, true); + } + + /** + * 删除指定 SaSession + * + * @param type / + * @param valueId / + */ + public static void deleteSessionById(String type, Object valueId) { + SaManager.getSaTokenDao().deleteSession(splicingSessionKey(type, valueId)); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoConsts.java b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoConsts.java deleted file mode 100644 index 8f049daee..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoConsts.java +++ /dev/null @@ -1,77 +0,0 @@ -package cn.dev33.satoken.sso; - -/** - * Sa-Token-SSO模块相关常量 - * @author kong - * - */ -public class SaSsoConsts { - - /** - * 所有API接口 - * @author kong - */ - public static final class Api { - - /** SSO-Server端:授权地址 */ - public static String ssoAuth = "/sso/auth"; - - /** SSO-Server端:RestAPI 登录接口 */ - public static String ssoDoLogin = "/sso/doLogin"; - - /** SSO-Server端:校验ticket 获取账号id */ - public static String ssoCheckTicket = "/sso/checkTicket"; - - /** SSO-Server端 (and Client端):单点注销地址 */ - public static String ssoLogout = "/sso/logout"; - - /** SSO-Client端:登录地址 */ - public static String ssoLogin = "/sso/login"; - - /** SSO-Client端:单点注销的回调 */ - public static String ssoLogoutCall = "/sso/logoutCall"; - - } - - /** - * 所有参数名称 - * @author kong - */ - public static final class ParamName { - - /** redirect参数名称 */ - public static String redirect = "redirect"; - - /** ticket参数名称 */ - public static String ticket = "ticket"; - - /** back参数名称 */ - public static String back = "back"; - - /** loginId参数名称 */ - public static String loginId = "loginId"; - - /** secretkey参数名称 */ - public static String secretkey = "secretkey"; - - /** Client端单点注销时-回调URL 参数名称 */ - public static String ssoLogoutCall = "ssoLogoutCall"; - - public static String name = "name"; - public static String pwd = "pwd"; - - } - - /** Client端单点注销回调URL的Set集合,存储在Session中使用的key */ - public static final String SLO_CALLBACK_SET_KEY = "SLO_CALLBACK_SET_KEY_"; - - /** 表示OK的返回结果 */ - public static final String OK = "ok"; - - /** 表示自己 */ - public static final String SELF = "self"; - - /** 表示请求没有得到任何有效处理 {msg: "not handle"} */ - public static final String NOT_HANDLE = "{\"msg\": \"not handle\"}"; - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoHandle.java b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoHandle.java deleted file mode 100644 index ccb278775..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoHandle.java +++ /dev/null @@ -1,324 +0,0 @@ -package cn.dev33.satoken.sso; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.config.SaSsoConfig; -import cn.dev33.satoken.context.SaHolder; -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.context.model.SaResponse; -import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.sso.SaSsoConsts.Api; -import cn.dev33.satoken.sso.SaSsoConsts.ParamName; -import cn.dev33.satoken.stp.StpLogic; -import cn.dev33.satoken.util.SaFoxUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-Token-SSO 单点登录请求处理类封装 - * @author kong - * - */ -public class SaSsoHandle { - - /** - * 处理所有Server端请求 - * @return 处理结果 - */ - public static Object serverRequest() { - - // 获取对象 - SaRequest req = SaHolder.getRequest(); - SaSsoConfig cfg = SaManager.getConfig().getSso(); - - // ------------------ 路由分发 ------------------ - - // SSO-Server端:授权地址 - if(req.isPath(Api.ssoAuth)) { - return ssoAuth(); - } - - // SSO-Server端:RestAPI 登录接口 - if(req.isPath(Api.ssoDoLogin)) { - return ssoDoLogin(); - } - - // SSO-Server端:校验ticket 获取账号id - if(req.isPath(Api.ssoCheckTicket) && cfg.isHttp) { - return ssoCheckTicket(); - } - - // SSO-Server端:单点注销 - if(req.isPath(Api.ssoLogout) && cfg.isSlo) { - return ssoServerLogout(); - } - - // 默认返回 - return SaSsoConsts.NOT_HANDLE; - } - - /** - * SSO-Server端:授权地址 - * @return 处理结果 - */ - public static Object ssoAuth() { - // 获取对象 - SaRequest req = SaHolder.getRequest(); - SaResponse res = SaHolder.getResponse(); - SaSsoConfig cfg = SaManager.getConfig().getSso(); - StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic; - - // ---------- 此处两种情况分开处理: - // 情况1:在SSO认证中心尚未登录,则先去登登录 - if(stpLogic.isLogin() == false) { - return cfg.notLoginView.get(); - } - // 情况2:在SSO认证中心已经登录,开始构建授权重定向地址,下放ticket - String redirectUrl = SaSsoUtil.buildRedirectUrl(stpLogic.getLoginId(), req.getParam(ParamName.redirect)); - return res.redirect(redirectUrl); - } - - /** - * SSO-Server端:RestAPI 登录接口 - * @return 处理结果 - */ - public static Object ssoDoLogin() { - // 获取对象 - SaRequest req = SaHolder.getRequest(); - SaSsoConfig cfg = SaManager.getConfig().getSso(); - - // 处理 - return cfg.doLoginHandle.apply(req.getParam(ParamName.name), req.getParam(ParamName.pwd)); - } - - /** - * SSO-Server端:校验ticket 获取账号id - * @return 处理结果 - */ - public static Object ssoCheckTicket() { - // 获取对象 - SaRequest req = SaHolder.getRequest(); - - // 获取参数 - String ticket = req.getParam(ParamName.ticket); - String sloCallback = req.getParam(ParamName.ssoLogoutCall); - - // 校验ticket,获取对应的账号id - Object loginId = SaSsoUtil.checkTicket(ticket); - - // 注册此客户端的单点注销回调URL - SaSsoUtil.registerSloCallbackUrl(loginId, sloCallback); - - // 返回给Client端 - return loginId; - } - - /** - * SSO-Server端:单点注销 - * @return 处理结果 - */ - public static Object ssoServerLogout() { - // 获取对象 - SaRequest req = SaHolder.getRequest(); - SaSsoConfig cfg = SaManager.getConfig().getSso(); - StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic; - - // 获取参数 - String loginId = req.getParam(ParamName.loginId); - String secretkey = req.getParam(ParamName.secretkey); - - // 遍历通知Client端注销会话 - // SaSsoUtil.singleLogout(secretkey, loginId, url -> cfg.sendHttp.apply(url)); - // step.1 校验秘钥 - SaSsoUtil.checkSecretkey(secretkey); - - // step.2 遍历通知Client端注销会话 - SaSsoUtil.forEachSloUrl(loginId, url -> cfg.sendHttp.apply(url)); - - // step.3 Server端注销 - stpLogic.logoutByTokenValue(stpLogic.getTokenValueByLoginId(loginId)); - - // 完成 - return SaSsoConsts.OK; - } - - - /** - * 处理所有Client端请求 - * @return 处理结果 - */ - public static Object clientRequest() { - - // 获取对象 - SaRequest req = SaHolder.getRequest(); - SaSsoConfig cfg = SaManager.getConfig().getSso(); - - // ------------------ 路由分发 ------------------ - - // ---------- SSO-Client端:登录地址 - if(req.isPath(Api.ssoLogin)) { - return ssoLogin(); - } - - // ---------- SSO-Client端:单点注销 [模式二] - if(req.isPath(Api.ssoLogout) && cfg.isSlo && cfg.isHttp == false) { - return ssoLogoutType2(); - } - - // ---------- SSO-Client端:单点注销 [模式三] - if(req.isPath(Api.ssoLogout) && cfg.isSlo && cfg.isHttp) { - return ssoLogoutType3(); - } - - // ---------- SSO-Client端:单点注销的回调 [模式三] - if(req.isPath(Api.ssoLogoutCall) && cfg.isSlo && cfg.isHttp) { - return ssoLogoutCall(); - } - - // 默认返回 - return SaSsoConsts.NOT_HANDLE; - } - - /** - * SSO-Client端:登录地址 - * @return 处理结果 - */ - public static Object ssoLogin() { - // 获取对象 - SaRequest req = SaHolder.getRequest(); - SaResponse res = SaHolder.getResponse(); - SaSsoConfig cfg = SaManager.getConfig().getSso(); - StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic; - - // 获取参数 - String back = req.getParam(ParamName.back, "/"); - String ticket = req.getParam(ParamName.ticket); - - // 如果当前Client端已经登录,则无需访问SSO认证中心,可以直接返回 - if(stpLogic.isLogin()) { - return res.redirect(back); - } - /* - * 此时有两种情况: - * 情况1:ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心 - * 情况2:ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录 - */ - if(ticket == null) { - String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back); - return res.redirect(serverAuthUrl); - } else { - // ------- 1、校验ticket,获取账号id - Object loginId = null; - if(cfg.isHttp) { - // 方式1:使用http请求校验ticket - String ssoLogoutCall = null; - if(cfg.isSlo) { - ssoLogoutCall = SaHolder.getRequest().getUrl().replace(Api.ssoLogin, Api.ssoLogoutCall); - } - String checkUrl = SaSsoUtil.buildCheckTicketUrl(ticket, ssoLogoutCall); - Object body = cfg.sendHttp.apply(checkUrl); - loginId = (SaFoxUtil.isEmpty(body) ? null : body); - } else { - // 方式2:直连Redis校验ticket - loginId = SaSsoUtil.checkTicket(ticket); - } - // Be: 如果开发者自定义了处理逻辑 - if(cfg.ticketResultHandle != null) { - return cfg.ticketResultHandle.apply(loginId, back); - } - // ------- 2、如果loginId有值,说明ticket有效,进行登录并重定向至back地址 - if(loginId != null ) { - stpLogic.login(loginId); - return res.redirect(back); - } else { - // 如果ticket无效: - throw new SaTokenException("无效ticket:" + ticket); - } - } - } - - /** - * SSO-Client端:单点注销 [模式二] - * @return 处理结果 - */ - public static Object ssoLogoutType2() { - // 获取对象 - SaRequest req = SaHolder.getRequest(); - SaResponse res = SaHolder.getResponse(); - StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic; - - // 开始处理 - stpLogic.logout(); - - // 返回 - return ssoLogoutBack(req, res); - } - - /** - * SSO-Client端:单点注销 [模式三] - * @return 处理结果 - */ - public static Object ssoLogoutType3() { - // 获取对象 - SaRequest req = SaHolder.getRequest(); - SaResponse res = SaHolder.getResponse(); - SaSsoConfig cfg = SaManager.getConfig().getSso(); - StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic; - - // 如果未登录,则无需注销 - if(stpLogic.isLogin() == false) { - return SaResult.ok(); - } - - // 调用SSO-Server认证中心API,进行注销 - String url = SaSsoUtil.buildSloUrl(stpLogic.getLoginId()); - String body = String.valueOf(cfg.sendHttp.apply(url)); - if(SaSsoConsts.OK.equals(body) == false) { - return SaResult.error("单点注销失败"); - } - - // 返回 - return ssoLogoutBack(req, res); - } - - /** - * SSO-Client端:单点注销的回调 [模式三] - * @return 处理结果 - */ - public static Object ssoLogoutCall() { - // 获取对象 - SaRequest req = SaHolder.getRequest(); - StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic; - - // 获取参数 - String loginId = req.getParam(ParamName.loginId); - String secretkey = req.getParam(ParamName.secretkey); - - SaSsoUtil.checkSecretkey(secretkey); - stpLogic.logoutByTokenValue(stpLogic.getTokenValueByLoginId(loginId)); - return SaSsoConsts.OK; - } - - /** - * 封装:单点注销成功后返回结果 - * @param req SaRequest对象 - * @param res SaResponse对象 - * @return 返回结果 - */ - public static Object ssoLogoutBack(SaRequest req, SaResponse res) { - /* - * 三种情况: - * 1. 有back参数,值为SELF -> 回退一级并刷新 - * 2. 有back参数,值为url -> 跳转back地址 - * 3. 无back参数 -> 返回json数据 - */ - String back = req.getParam(ParamName.back); - if(SaFoxUtil.isNotEmpty(back)) { - if(back.equals(SaSsoConsts.SELF)) { - return ""; - } - return res.redirect(back); - } else { - return SaResult.ok("单点注销成功"); - } - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoTemplate.java deleted file mode 100644 index a30656eed..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoTemplate.java +++ /dev/null @@ -1,419 +0,0 @@ -package cn.dev33.satoken.sso; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.config.SaSsoConfig; -import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.session.SaSession; -import cn.dev33.satoken.sso.SaSsoConsts.ParamName; -import cn.dev33.satoken.stp.StpLogic; -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * Sa-Token-SSO 单点登录模块 - * @author kong - * - */ -public class SaSsoTemplate { - - /** - * 单点登录模块使用的 StpLogic 对象 - */ - public StpLogic stpLogic; - public SaSsoTemplate(StpLogic stpLogic) { - this.stpLogic = stpLogic; - } - - // ---------------------- Ticket 操作 ---------------------- - - /** - * 根据 账号id 创建一个 Ticket码 - * @param loginId 账号id - * @return Ticket码 - */ - public String createTicket(Object loginId) { - // 创建 Ticket - String ticket = randomTicket(loginId); - - // 保存 Ticket - saveTicket(ticket, loginId); - saveTicketIndex(ticket, loginId); - - // 返回 Ticket - return ticket; - } - - /** - * 保存 Ticket - * @param ticket ticket码 - * @param loginId 账号id - */ - public void saveTicket(String ticket, Object loginId) { - long ticketTimeout = SaManager.getConfig().getSso().getTicketTimeout(); - SaManager.getSaTokenDao().set(splicingTicketSaveKey(ticket), String.valueOf(loginId), ticketTimeout); - } - - /** - * 保存 Ticket 索引 - * @param ticket ticket码 - * @param loginId 账号id - */ - public void saveTicketIndex(String ticket, Object loginId) { - long ticketTimeout = SaManager.getConfig().getSso().getTicketTimeout(); - SaManager.getSaTokenDao().set(splicingTicketIndexKey(loginId), String.valueOf(ticket), ticketTimeout); - } - - /** - * 删除 Ticket - * @param ticket Ticket码 - */ - public void deleteTicket(String ticket) { - if(ticket == null) { - return; - } - SaManager.getSaTokenDao().delete(splicingTicketSaveKey(ticket)); - } - - /** - * 删除 Ticket索引 - * @param loginId 账号id - */ - public void deleteTicketIndex(Object loginId) { - if(loginId == null) { - return; - } - SaManager.getSaTokenDao().delete(splicingTicketIndexKey(loginId)); - } - - /** - * 根据 Ticket码 获取账号id,如果Ticket码无效则返回null - * @param ticket Ticket码 - * @return 账号id - */ - public Object getLoginId(String ticket) { - if(SaFoxUtil.isEmpty(ticket)) { - return null; - } - return SaManager.getSaTokenDao().get(splicingTicketSaveKey(ticket)); - } - - /** - * 根据 Ticket码 获取账号id,并转换为指定类型 - * @param 要转换的类型 - * @param ticket Ticket码 - * @param cs 要转换的类型 - * @return 账号id - */ - public T getLoginId(String ticket, Class cs) { - return SaFoxUtil.getValueByType(getLoginId(ticket), cs); - } - - /** - * 查询 指定账号id的 Ticket值 - * @param loginId 账号id - * @return Ticket值 - */ - public String getTicketValue(Object loginId) { - if(loginId == null) { - return null; - } - return SaManager.getSaTokenDao().get(splicingTicketIndexKey(loginId)); - } - - /** - * 校验ticket码,获取账号id,如果此ticket是有效的,则立即删除 - * @param ticket Ticket码 - * @return 账号id - */ - public Object checkTicket(String ticket) { - Object loginId = getLoginId(ticket); - if(loginId != null) { - deleteTicket(ticket); - deleteTicketIndex(loginId); - } - return loginId; - } - - /** - * 随机一个 Ticket码 - * @param loginId 账号id - * @return Ticket码 - */ - public String randomTicket(Object loginId) { - return SaFoxUtil.getRandomString(64); - } - - - // ---------------------- 构建URL ---------------------- - - /** - * 构建URL:Server端 单点登录地址 - * @param clientLoginUrl Client端登录地址 - * @param back 回调路径 - * @return [SSO-Server端-认证地址 ] - */ - public String buildServerAuthUrl(String clientLoginUrl, String back) { - - // 服务端认证地址 - String serverUrl = SaManager.getConfig().getSso().getAuthUrl(); - - // 对back地址编码 - back = (back == null ? "" : back); - back = SaFoxUtil.encodeUrl(back); - - // 拼接最终地址,格式示例:serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com - clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, ParamName.back, back); - String serverAuthUrl = SaFoxUtil.joinParam(serverUrl, ParamName.redirect, clientLoginUrl); - - // 返回 - return serverAuthUrl; - } - - /** - * 构建URL:Server端向Client下放ticke的地址 - * @param loginId 账号id - * @param redirect Client端提供的重定向地址 - * @return see note - */ - public String buildRedirectUrl(Object loginId, String redirect) { - - // 校验 重定向地址 是否合法 - checkRedirectUrl(redirect); - - // 删掉 旧Ticket - deleteTicket(getTicketValue(loginId)); - - // 创建 新Ticket - String ticket = createTicket(loginId); - - // 构建 授权重定向地址 (Server端 根据此地址向 Client端 下放Ticket) - return SaFoxUtil.joinParam(encodeBackParam(redirect), ParamName.ticket, ticket); - } - - /** - * 校验重定向url合法性 - * @param url 下放ticket的url地址 - */ - public void checkRedirectUrl(String url) { - - // 1、是否是一个有效的url - if(SaFoxUtil.isUrl(url) == false) { - throw new SaTokenException("无效redirect:" + url); - } - - // 2、截取掉?后面的部分 - int qIndex = url.indexOf("?"); - if(qIndex != -1) { - url = url.substring(0, qIndex); - } - - // 3、是否在[允许地址列表]之中 - List authUrlList = Arrays.asList(getAllowUrl().replaceAll(" ", "").split(",")); - if(SaManager.getSaTokenAction().hasElement(authUrlList, url) == false) { - throw new SaTokenException("非法redirect:" + url); - } - - // 校验通过 √ - return; - } - - /** - * 获取:所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) - * @return see note - */ - public String getAllowUrl() { - // 默认从配置文件中返回 - return SaManager.getConfig().getSso().getAllowUrl(); - } - - /** - * 对url中的back参数进行URL编码, 解决超链接重定向后参数丢失的bug - * @param url url - * @return 编码过后的url - */ - public String encodeBackParam(String url) { - - // 获取back参数所在位置 - int index = url.indexOf("?" + ParamName.back + "="); - if(index == -1) { - index = url.indexOf("&" + ParamName.back + "="); - if(index == -1) { - return url; - } - } - - // 开始编码 - int length = ParamName.back.length() + 2; - String back = url.substring(index + length); - back = SaFoxUtil.encodeUrl(back); - - // 放回url中 - url = url.substring(0, index + length) + back; - return url; - } - - /** - * 构建URL:Server端 账号资料查询地址 - * @param loginId 账号id - * @return Server端 账号资料查询地址 - */ - public String buildUserinfoUrl(Object loginId) { - // 拼接 - String userinfoUrl = SaManager.getConfig().getSso().getUserinfoUrl(); - userinfoUrl = SaFoxUtil.joinParam(userinfoUrl, ParamName.loginId, loginId); - userinfoUrl = SaFoxUtil.joinParam(userinfoUrl, ParamName.secretkey, SaManager.getConfig().getSso().getSecretkey()); - // 返回 - return userinfoUrl; - } - - - // ------------------- SSO 模式三相关 ------------------- - - /** - * 校验secretkey秘钥是否有效 - * @param secretkey 秘钥 - */ - public void checkSecretkey(String secretkey) { - if(secretkey == null || secretkey.isEmpty() || secretkey.equals(SaManager.getConfig().getSso().getSecretkey()) == false) { - throw new SaTokenException("无效秘钥:" + secretkey); - } - } - - /** - * 构建URL:校验ticket的URL - *

在模式三下,Client端拿到Ticket后根据此地址向Server端发送请求,获取账号id - * @param ticket ticket码 - * @param ssoLogoutCallUrl 单点注销时的回调URL - * @return 构建完毕的URL - */ - public String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) { - // 裸地址 - String url = SaManager.getConfig().getSso().getCheckTicketUrl(); - - // 拼接ticket参数 - url = SaFoxUtil.joinParam(url, ParamName.ticket, ticket); - - // 拼接单点注销时的回调URL - if(ssoLogoutCallUrl != null) { - url = SaFoxUtil.joinParam(url, ParamName.ssoLogoutCall, ssoLogoutCallUrl); - } - - // 返回 - return url; - } - - /** - * 为指定账号id注册单点注销回调URL - * @param loginId 账号id - * @param sloCallbackUrl 单点注销时的回调URL - */ - public void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) { - if(loginId == null || sloCallbackUrl == null || sloCallbackUrl.isEmpty()) { - return; - } - SaSession session = stpLogic.getSessionByLoginId(loginId); - Set urlSet = session.get(SaSsoConsts.SLO_CALLBACK_SET_KEY, ()-> new HashSet()); - urlSet.add(sloCallbackUrl); - session.set(SaSsoConsts.SLO_CALLBACK_SET_KEY, urlSet); - } - - /** - * 循环调用Client端单点注销回调 - * @param loginId 账号id - * @param fun 调用方法 - */ - public void forEachSloUrl(Object loginId, CallSloUrlFunction fun) { - String secretkey = SaManager.getConfig().getSso().getSecretkey(); - Set urlSet = stpLogic.getSessionByLoginId(loginId).get(SaSsoConsts.SLO_CALLBACK_SET_KEY, - () -> new HashSet()); - - for (String url : urlSet) { - // 拼接:login参数、秘钥参数 - url = SaFoxUtil.joinParam(url, ParamName.loginId, loginId); - url = SaFoxUtil.joinParam(url, ParamName.secretkey, secretkey); - // 调用 - fun.run(url); - } - } - - /** - * 构建URL:单点注销URL - * @param loginId 要注销的账号id - * @return 单点注销URL - */ - public String buildSloUrl(Object loginId) { - SaSsoConfig ssoConfig = SaManager.getConfig().getSso(); - String url = ssoConfig.getSloUrl(); - url = SaFoxUtil.joinParam(url, ParamName.loginId, loginId); - url = SaFoxUtil.joinParam(url, ParamName.secretkey, ssoConfig.getSecretkey()); - return url; - } - - /** - * 指定账号单点注销 - * @param secretkey 校验秘钥 - * @param loginId 指定账号 - * @param fun 调用方法 - */ - public void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) { - // step.1 校验秘钥 - checkSecretkey(secretkey); - - // step.2 遍历通知Client端注销会话 - forEachSloUrl(loginId, fun); - - // step.3 Server端注销 - // StpUtil.logoutByLoginId(loginId); - stpLogic.logoutByTokenValue(stpLogic.getTokenValueByLoginId(loginId)); - } - - /** - * 获取:账号资料 - * @param loginId 账号id - * @return 账号资料 - */ - public Object getUserinfo(Object loginId) { - String url = buildUserinfoUrl(loginId); - return SaManager.getConfig().getSso().sendHttp.apply(url); - } - - - - // ------------------- 返回相应key ------------------- - - /** - * 拼接key:Ticket 查 账号Id - * @param ticket ticket值 - * @return key - */ - public String splicingTicketSaveKey(String ticket) { - return SaManager.getConfig().getTokenName() + ":ticket:" + ticket; - } - - /** - * 拼接key:账号Id 反查 Ticket - * @param id 账号id - * @return key - */ - public String splicingTicketIndexKey(Object id) { - return SaManager.getConfig().getTokenName() + ":id-ticket:" + id; - } - - /** - * 单点注销回调函数 - * @author kong - */ - @FunctionalInterface - static interface CallSloUrlFunction{ - /** - * 调用function - * @param url 注销回调URL - */ - public void run(String url); - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoUtil.java deleted file mode 100644 index a48bf95b3..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/sso/SaSsoUtil.java +++ /dev/null @@ -1,189 +0,0 @@ -package cn.dev33.satoken.sso; - -import cn.dev33.satoken.sso.SaSsoTemplate.CallSloUrlFunction; -import cn.dev33.satoken.stp.StpUtil; - -/** - * Sa-Token-SSO 单点登录模块 工具类 - * @author kong - * - */ -public class SaSsoUtil { - - /** - * 底层 SaSsoTemplate 对象 - */ - public static SaSsoTemplate saSsoTemplate = new SaSsoTemplate(StpUtil.stpLogic); - - - // ---------------------- Ticket 操作 ---------------------- - - /** - * 根据 账号id 创建一个 Ticket码 - * @param loginId 账号id - * @return Ticket码 - */ - public static String createTicket(Object loginId) { - return saSsoTemplate.createTicket(loginId); - } - - /** - * 删除 Ticket - * @param ticket Ticket码 - */ - public static void deleteTicket(String ticket) { - saSsoTemplate.deleteTicket(ticket); - } - - /** - * 删除 Ticket索引 - * @param loginId 账号id - */ - public static void deleteTicketIndex(Object loginId) { - saSsoTemplate.deleteTicketIndex(loginId); - } - - /** - * 根据 Ticket码 获取账号id,如果Ticket码无效则返回null - * @param ticket Ticket码 - * @return 账号id - */ - public static Object getLoginId(String ticket) { - return saSsoTemplate.getLoginId(ticket); - } - - /** - * 根据 Ticket码 获取账号id,并转换为指定类型 - * @param 要转换的类型 - * @param ticket Ticket码 - * @param cs 要转换的类型 - * @return 账号id - */ - public static T getLoginId(String ticket, Class cs) { - return saSsoTemplate.getLoginId(ticket, cs); - } - - /** - * 校验ticket码,获取账号id,如果此ticket是有效的,则立即删除 - * @param ticket Ticket码 - * @return 账号id - */ - public static Object checkTicket(String ticket) { - return saSsoTemplate.checkTicket(ticket); - } - - - // ---------------------- 构建URL ---------------------- - - /** - * 构建URL:Server端 单点登录地址 - * @param clientLoginUrl Client端登录地址 - * @param back 回调路径 - * @return [SSO-Server端-认证地址 ] - */ - public static String buildServerAuthUrl(String clientLoginUrl, String back) { - return saSsoTemplate.buildServerAuthUrl(clientLoginUrl, back); - } - - /** - * 构建URL:Server端向Client下放ticke的地址 - * @param loginId 账号id - * @param redirect Client端提供的重定向地址 - * @return see note - */ - public static String buildRedirectUrl(Object loginId, String redirect) { - return saSsoTemplate.buildRedirectUrl(loginId, redirect); - } - - /** - * 校验重定向url合法性 - * @param url 下放ticket的url地址 - */ - public static void checkRedirectUrl(String url) { - saSsoTemplate.checkRedirectUrl(url); - } - - /** - * 获取:所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) - * @return see note - */ - public static String getAllowUrl() { - return saSsoTemplate.getAllowUrl(); - } - - /** - * 构建URL:Server端 账号资料查询地址 - * @param loginId 账号id - * @return Server端 账号资料查询地址 - */ - public static String buildUserinfoUrl(Object loginId) { - return saSsoTemplate.buildUserinfoUrl(loginId); - } - - // ------------------- SSO 模式三 ------------------- - - /** - * 校验secretkey秘钥是否有效 - * @param secretkey 秘钥 - */ - public static void checkSecretkey(String secretkey) { - saSsoTemplate.checkSecretkey(secretkey); - } - - /** - * 构建URL:校验ticket的URL - * @param ticket ticket码 - * @param ssoLogoutCallUrl 单点注销时的回调URL - * @return 构建完毕的URL - */ - public static String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) { - return saSsoTemplate.buildCheckTicketUrl(ticket, ssoLogoutCallUrl); - } - - /** - * 为指定账号id注册单点注销回调URL - * @param loginId 账号id - * @param sloCallbackUrl 单点注销时的回调URL - */ - public static void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) { - saSsoTemplate.registerSloCallbackUrl(loginId, sloCallbackUrl); - } - - /** - * 循环调用Client端单点注销回调 - * @param loginId 账号id - * @param fun 调用方法 - */ - public static void forEachSloUrl(Object loginId, CallSloUrlFunction fun) { - saSsoTemplate.forEachSloUrl(loginId, fun); - } - - /** - * 构建URL:单点注销URL - * @param loginId 要注销的账号id - * @return 单点注销URL - */ - public static String buildSloUrl(Object loginId) { - return saSsoTemplate.buildSloUrl(loginId); - } - - /** - * 指定账号单点注销 - * @param secretkey 校验秘钥 - * @param loginId 指定账号 - * @param fun 调用方法 - */ - public static void singleLogout(String secretkey, Object loginId, CallSloUrlFunction fun) { - saSsoTemplate.singleLogout(secretkey, loginId, fun); - } - - /** - * 获取:账号资料 - * @param loginId 账号id - * @return 账号资料 - */ - public static Object getUserinfo(Object loginId) { - return saSsoTemplate.getUserinfo(loginId); - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginConfig.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginConfig.java new file mode 100644 index 000000000..b51a336be --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginConfig.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; + +import java.util.Map; + +/** + *

请更换为 new SaLoginParameter()

+ * + * 快速、简洁的构建:调用 `StpUtil.login()` 时的 [ 配置参数 SaLoginParameter ] + * + *
+ *     	// 例如:在登录时指定 token 有效期为七天,代码如下:
+ *     	StpUtil.login(10001, SaLoginConfig.setTimeout(60 * 60 * 24 * 7));
+ *
+ *     	// 上面的代码与下面的代码等价
+ *     	StpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7));
+ * 
+ * + * @author click33 + * @since 1.29.0 + */ +@Deprecated +public class SaLoginConfig { + + private SaLoginConfig() { + } + + /** + * @param device 此次登录的客户端设备类型 + * @return 登录参数 Model + */ + public static SaLoginParameter setDevice(String device) { + return create().setDeviceType(device); + } + + /** + * @param isLastingCookie 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + * @return 登录参数 Model + */ + public static SaLoginParameter setIsLastingCookie(Boolean isLastingCookie) { + return create().setIsLastingCookie(isLastingCookie); + } + + /** + * @param timeout 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值) + * @return 登录参数 Model + */ + public static SaLoginParameter setTimeout(long timeout) { + return create().setTimeout(timeout); + } + + /** + * @param activeTimeout 指定此次登录 token 最低活跃频率,单位:秒(如未指定,自动取全局配置的 activeTimeout 值) + * @return 对象自身 + */ + public static SaLoginParameter setActiveTimeout(long activeTimeout) { + return create().setActiveTimeout(activeTimeout); + } + + /** + * @param extraData 扩展信息(只在jwt模式下生效) + * @return 登录参数 Model + */ + public static SaLoginParameter setExtraData(Map extraData) { + return create().setExtraData(extraData); + } + + /** + * @param token 预定Token(预定本次登录生成的Token值) + * @return 登录参数 Model + */ + public static SaLoginParameter setToken(String token) { + return create().setToken(token); + } + + /** + * 写入扩展数据(只在jwt模式下生效) + * @param key 键 + * @param value 值 + * @return 登录参数 Model + */ + public static SaLoginParameter setExtra(String key, Object value) { + return create().setExtra(key, value); + } + + /** + * @param isWriteHeader 是否在登录后将 Token 写入到响应头 + * @return 登录参数 Model + */ + public static SaLoginParameter setIsWriteHeader(Boolean isWriteHeader) { + return create().setIsWriteHeader(isWriteHeader); + } + + /** + * 设置 本次登录挂载到 TokenSign 的数据 + * + * @param tokenSignTag / + * @return 登录参数 Model + */ + public static SaLoginParameter setTokenSignTag(Map tokenSignTag) { + return create().setTerminalExtraData(tokenSignTag); + } + + /** + * 静态方法获取一个 SaLoginParameter 对象 + * @return SaLoginParameter 对象 + */ + public static SaLoginParameter create() { + return new SaLoginParameter(SaManager.getConfig()); + } + +} 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 3fa3192a5..b7bc1c27f 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 @@ -1,136 +1,30 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp; -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.dao.SaTokenDao; -import cn.dev33.satoken.util.SaTokenConsts; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; /** - * 调用 `StpUtil.login()` 时的 [配置参数 Model ] - * @author kong + *

请更改为 SaLoginParameter

+ * 在调用 `StpUtil.login()` 时的 配置参数 Model,决定登录的一些细节行为
* + * @author click33 + * @since 1.13.2 */ -public class SaLoginModel { - - - /** - * 此次登录的客户端设备标识 - */ - public String device; - - /** - * 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) - */ - public Boolean isLastingCookie; - - /** - * 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值) - */ - public Long timeout; - - - /** - * @return 参考 {@link #device} - */ - public String getDevice() { - return device; - } - - /** - * @param device 参考 {@link #device} - * @return 对象自身 - */ - public SaLoginModel setDevice(String device) { - this.device = device; - return this; - } - - /** - * @return 参考 {@link #isLastingCookie} - */ - public Boolean getIsLastingCookie() { - return isLastingCookie; - } - - /** - * @param isLastingCookie 参考 {@link #isLastingCookie} - * @return 对象自身 - */ - public SaLoginModel setIsLastingCookie(Boolean isLastingCookie) { - this.isLastingCookie = isLastingCookie; - return this; - } - - /** - * @return 参考 {@link #timeout} - */ - public Long getTimeout() { - return timeout; - } - - /** - * @param timeout 参考 {@link #timeout} - * @return 对象自身 - */ - public SaLoginModel setTimeout(long timeout) { - this.timeout = timeout; - return this; - } - - - /** - * @return Cookie时长 - */ - public int getCookieTimeout() { - if(isLastingCookie == false) { - return -1; - } - if(timeout == SaTokenDao.NEVER_EXPIRE) { - return Integer.MAX_VALUE; - } - return (int)(long)timeout; - } - - /** - * 构建对象,初始化默认值 - * @return 对象自身 - */ - public SaLoginModel build() { - return build(SaManager.getConfig()); - } - - /** - * 构建对象,初始化默认值 - * @param config 配置对象 - * @return 对象自身 - */ - public SaLoginModel build(SaTokenConfig config) { - if(device == null) { - device = SaTokenConsts.DEFAULT_LOGIN_DEVICE; - } - if(isLastingCookie == null) { - isLastingCookie = true; - } - if(timeout == null) { - timeout = config.getTimeout(); - } - return this; - } - - /** - * 静态方法获取一个 SaLoginModel 对象 - * @return SaLoginModel 对象 - */ - public static SaLoginModel create() { - return new SaLoginModel(); - } - - /** - * toString - */ - @Override - public String toString() { - return "SaLoginModel [device=" + device + ", isLastingCookie=" + isLastingCookie + ", timeout=" + timeout + "]"; - } +@Deprecated +public class SaLoginModel extends SaLoginParameter { } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaTokenInfo.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaTokenInfo.java index 82e2482dd..c17159f3b 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaTokenInfo.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/SaTokenInfo.java @@ -1,197 +1,230 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp; /** - * Token信息Model: 用来描述一个Token的常用参数 - * - * @author kong + * Token 信息 Model: 用来描述一个 Token 的常见参数。 * + *

+ * 例如:
+ *

+ *     {
+ *         "tokenName": "satoken",           // token名称
+ *         "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633",      // token值
+ *         "isLogin": true,                  // 此token是否已经登录
+ *         "loginId": "10001",               // 此token对应的LoginId,未登录时为null
+ *         "loginType": "login",              // 账号类型标识
+ *         "tokenTimeout": 2591977,          // token剩余有效期 (单位: 秒)
+ *         "sessionTimeout": 2591977,        // Account-Session剩余有效时间 (单位: 秒)
+ *         "tokenSessionTimeout": -2,        // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
+ *         "tokenActiveTimeout": -1,       // Token 距离被冻结还剩多少时间 (单位: 秒)
+ *         "loginDevice": "DEF"   // 登录设备类型
+ *     }
+ *     
+ *

+ * + * @author click33 + * @since 1.10.0 */ public class SaTokenInfo { - /** token名称 */ + /** token 名称 */ public String tokenName; - /** token值 */ + /** token 值 */ public String tokenValue; - /** 此token是否已经登录 */ + /** 此 token 是否已经登录 */ public Boolean isLogin; - /** 此token对应的LoginId,未登录时为null */ + /** 此 token 对应的 LoginId,未登录时为 null */ public Object loginId; - /** 账号类型 */ + /** 多账号体系下的账号类型 */ public String loginType; - /** token剩余有效期 (单位: 秒) */ + /** token 剩余有效期(单位: 秒) */ public long tokenTimeout; - /** User-Session剩余有效时间 (单位: 秒) */ + /** Account-Session 剩余有效时间(单位: 秒) */ public long sessionTimeout; - /** Token-Session剩余有效时间 (单位: 秒) */ + /** Token-Session 剩余有效时间(单位: 秒) */ public long tokenSessionTimeout; - /** token剩余无操作有效时间 (单位: 秒) */ - public long tokenActivityTimeout; + /** token 距离被冻结还剩多少时间(单位: 秒) */ + public long tokenActiveTimeout; - /** 登录设备标识 */ - public String loginDevice; + /** 登录设备类型 */ + public String loginDeviceType; - /** 自定义数据 */ + /** 自定义数据(暂无意义,留作扩展) */ public String tag; /** - * @return token名称 + * @return token 名称 */ public String getTokenName() { return tokenName; } /** - * @param tokenName token名称 + * @param tokenName token 名称 */ public void setTokenName(String tokenName) { this.tokenName = tokenName; } /** - * @return token值 + * @return token 值 */ public String getTokenValue() { return tokenValue; } /** - * @param tokenValue token值 + * @param tokenValue token 值 */ public void setTokenValue(String tokenValue) { this.tokenValue = tokenValue; } /** - * @return 此token是否已经登录 + * @return 此 token 是否已经登录 */ public Boolean getIsLogin() { return isLogin; } /** - * @param isLogin 此token是否已经登录 + * @param isLogin 此 token 是否已经登录 */ public void setIsLogin(Boolean isLogin) { this.isLogin = isLogin; } /** - * @return 此token对应的LoginId,未登录时为null + * @return 此 token 对应的LoginId,未登录时为null */ public Object getLoginId() { return loginId; } /** - * @param loginId 此token对应的LoginId,未登录时为null + * @param loginId 此 token 对应的LoginId,未登录时为null */ public void setLoginId(Object loginId) { this.loginId = loginId; } /** - * @return 账号类型 + * @return 多账号体系下的账号类型 */ public String getLoginType() { return loginType; } /** - * @param loginType 账号类型 + * @param loginType 多账号体系下的账号类型 */ public void setLoginType(String loginType) { this.loginType = loginType; } /** - * @return token剩余有效期 (单位: 秒) + * @return token 剩余有效期(单位: 秒) */ public long getTokenTimeout() { return tokenTimeout; } /** - * @param tokenTimeout token剩余有效期 (单位: 秒) + * @param tokenTimeout token剩余有效期(单位: 秒) */ public void setTokenTimeout(long tokenTimeout) { this.tokenTimeout = tokenTimeout; } /** - * @return User-Session剩余有效时间 (单位: 秒) + * @return Account-Session 剩余有效时间(单位: 秒) */ public long getSessionTimeout() { return sessionTimeout; } /** - * @param sessionTimeout User-Session剩余有效时间 (单位: 秒) + * @param sessionTimeout Account-Session剩余有效时间(单位: 秒) */ public void setSessionTimeout(long sessionTimeout) { this.sessionTimeout = sessionTimeout; } /** - * @return Token-Session剩余有效时间 (单位: 秒) + * @return Token-Session剩余有效时间(单位: 秒) */ public long getTokenSessionTimeout() { return tokenSessionTimeout; } /** - * @param tokenSessionTimeout Token-Session剩余有效时间 (单位: 秒) + * @param tokenSessionTimeout Token-Session剩余有效时间(单位: 秒) */ public void setTokenSessionTimeout(long tokenSessionTimeout) { this.tokenSessionTimeout = tokenSessionTimeout; } /** - * @return token剩余无操作有效时间 (单位: 秒) + * @return token 距离被冻结还剩多少时间(单位: 秒) */ - public long getTokenActivityTimeout() { - return tokenActivityTimeout; + public long getTokenActiveTimeout() { + return tokenActiveTimeout; } /** - * @param tokenActivityTimeout token剩余无操作有效时间 (单位: 秒) + * @param tokenActiveTimeout token 距离被冻结还剩多少时间(单位: 秒) */ - public void setTokenActivityTimeout(long tokenActivityTimeout) { - this.tokenActivityTimeout = tokenActivityTimeout; + public void setTokenActiveTimeout(long tokenActiveTimeout) { + this.tokenActiveTimeout = tokenActiveTimeout; } /** - * @return 登录设备标识 + * @return 登录设备类型 */ - public String getLoginDevice() { - return loginDevice; + public String getLoginDeviceType() { + return loginDeviceType; } /** - * @param loginDevice 登录设备标识 + * @param loginDeviceType 登录设备类型 */ - public void setLoginDevice(String loginDevice) { - this.loginDevice = loginDevice; + public void setLoginDeviceType(String loginDeviceType) { + this.loginDeviceType = loginDeviceType; } /** - * @return 自定义数据 + * @return 自定义数据(暂无意义,留作扩展) */ public String getTag() { return tag; } /** - * @param tag 自定义数据 + * @param tag 自定义数据(暂无意义,留作扩展) */ public void setTag(String tag) { this.tag = tag; @@ -205,10 +238,8 @@ public String toString() { return "SaTokenInfo [tokenName=" + tokenName + ", tokenValue=" + tokenValue + ", isLogin=" + isLogin + ", loginId=" + loginId + ", loginType=" + loginType + ", tokenTimeout=" + tokenTimeout + ", sessionTimeout=" + sessionTimeout + ", tokenSessionTimeout=" + tokenSessionTimeout - + ", tokenActivityTimeout=" + tokenActivityTimeout + ", loginDevice=" + loginDevice + ", tag=" + tag + + ", tokenActiveTimeout=" + tokenActiveTimeout + ", loginDeviceType=" + loginDeviceType + ", tag=" + tag + "]"; } - - } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterface.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterface.java index 092beaa9f..bc852796d 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterface.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterface.java @@ -1,11 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp; +import cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo; + import java.util.List; /** - * 权限认证接口,实现此接口即可集成权限认证功能 + * 权限数据源加载接口 + * + *

+ * 在使用权限校验 API 之前,你必须实现此接口,告诉框架哪些用户拥有哪些权限。
+ * 框架默认不对数据进行缓存,如果你的数据是从数据库中读取的,一般情况下你需要手动实现数据的缓存读写。 + *

* - * @author kong + * @author click33 + * @since 1.10.0 */ public interface StpInterface { @@ -16,7 +39,7 @@ public interface StpInterface { * @param loginType 账号类型 * @return 该账号id具有的权限码集合 */ - public List getPermissionList(Object loginId, String loginType); + List getPermissionList(Object loginId, String loginType); /** * 返回指定账号id所拥有的角色标识集合 @@ -25,6 +48,17 @@ public interface StpInterface { * @param loginType 账号类型 * @return 该账号id具有的角色标识集合 */ - public List getRoleList(Object loginId, String loginType); + List getRoleList(Object loginId, String loginType); + + /** + * 返回指定账号 id 是否被封禁 + * + * @param loginId 账号id + * @param service 业务标识符 + * @return 描述该账号是否封禁的包装信息对象 + */ + default SaDisableWrapperInfo isDisabled(Object loginId, String service) { + return SaDisableWrapperInfo.createNotDisabled(); + } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterfaceDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterfaceDefaultImpl.java index b2f7f8de2..35f101d68 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterfaceDefaultImpl.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterfaceDefaultImpl.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp; import java.util.ArrayList; @@ -6,20 +21,21 @@ /** * 对 {@link StpInterface} 接口默认的实现类 *

- * 如果开发者没有实现StpInterface接口,则使用此默认实现 + * 如果开发者没有实现 StpInterface 接口,则框架会使用此默认实现类,所有方法都返回空集合,即:用户不具有任何权限和角色。 * - * @author kong + * @author click33 + * @since 1.10.0 */ public class StpInterfaceDefaultImpl implements StpInterface { @Override public List getPermissionList(Object loginId, String loginType) { - return new ArrayList(); + return new ArrayList<>(); } @Override public List getRoleList(Object loginId, String loginType) { - return new ArrayList(); + return new ArrayList<>(); } } 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 87ed101b7..f5ea2a760 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 @@ -1,167 +1,399 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - import cn.dev33.satoken.SaManager; -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.config.SaCookieConfig; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaCookie; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.dao.SaTokenDao; -import cn.dev33.satoken.exception.DisableLoginException; -import cn.dev33.satoken.exception.NotLoginException; -import cn.dev33.satoken.exception.NotPermissionException; -import cn.dev33.satoken.exception.NotRoleException; -import cn.dev33.satoken.exception.NotSafeException; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.*; import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaTwoParamFunction; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo; import cn.dev33.satoken.session.SaSession; -import cn.dev33.satoken.session.TokenSign; +import cn.dev33.satoken.session.SaTerminalInfo; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutRange; +import cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode; +import cn.dev33.satoken.stp.parameter.enums.SaReplacedRange; +import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTokenConsts; +import cn.dev33.satoken.util.SaValue2Box; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static cn.dev33.satoken.exception.NotLoginException.*; + /** - * Sa-Token 权限验证,逻辑实现类 - * @author kong + * Sa-Token 权限认证,逻辑实现类 + * + *

+ * Sa-Token 的核心,框架大多数功能均由此类提供具体逻辑实现。 + *

+ * + * @author click33 + * @since 1.10.0 */ public class StpLogic { /** - * 账号类型,多账号体系时以此值区分,比如:login、user、admin + * 账号类型标识,多账号体系时(一个系统多套用户表)用此值区分具体要校验的是哪套用户,比如:login、user、admin */ public String loginType; - + /** - * 初始化StpLogic, 并指定账号类型 - * @param loginType 账号体系标识 + * 初始化 StpLogic, 并指定账号类型 + * + * @param loginType 账号类型标识 */ public StpLogic(String loginType) { - this.loginType = loginType; - // 在 SaTokenManager 中记录下此 StpLogic,以便根据 LoginType 进行查找此对象 - SaManager.putStpLogic(this); + setLoginType(loginType); } /** - * 获取当前 StpLogic 的账号类型 - * @return See Note + * 获取当前 StpLogic 账号类型标识 + * + * @return / */ public String getLoginType(){ return loginType; } /** - * 设置当前账号类型 - * @param loginType loginType + * 安全的重置当前账号类型 + * + *

注意:此方法只能在项目启动时调用,项目启动后不可动态更改 loginType。 + * 若在运行时修改,可能造成线程安全问题和严重的逻辑问题。 + * + * @param loginType 账号类型标识 * @return 对象自身 */ public StpLogic setLoginType(String loginType){ + + // 先清除此 StpLogic 在全局 SaManager 中的记录 + if(SaFoxUtil.isNotEmpty(this.loginType)) { + SaManager.removeStpLogic(this.loginType); + } + + // 赋值 this.loginType = loginType; + + // 将新的 loginType -> StpLogic 映射关系 put 到 SaManager 全局集合中,以便后续根据 LoginType 进行查找此对象 SaManager.putStpLogic(this); + + return this; + } + + private SaTokenConfig config; + + /** + * 写入当前 StpLogic 单独使用的配置对象 + * + * @param config 配置对象 + * @return 对象自身 + */ + public StpLogic setConfig(SaTokenConfig config) { + this.config = config; return this; } - - - // ------------------- 获取token 相关 ------------------- - + + /** + * 返回当前 StpLogic 使用的配置对象,如果当前 StpLogic 没有配置,则返回 null + * + * @return / + */ + public SaTokenConfig getConfig() { + return config; + } + + /** + * 返回当前 StpLogic 使用的配置对象,如果当前 StpLogic 没有配置,则返回全局配置对象 + * + * @return / + */ + public SaTokenConfig getConfigOrGlobal() { + SaTokenConfig cfg = getConfig(); + if(cfg != null) { + return cfg; + } + return SaManager.getConfig(); + } + + + + // ------------------- 获取 token 相关 ------------------- + /** - * 返回token名称 - * @return 此StpLogic的token名称 + * 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀 + * + * @return / */ public String getTokenName() { - return splicingKeyTokenName(); - } + return splicingKeyTokenName(); + } + + /** + * 为指定账号创建一个 token (只是把 token 创建出来,并不持久化存储) + * + * @param loginId 账号id + * @param deviceType 设备类型 + * @param timeout 过期时间 + * @param extraData 扩展信息 + * @return 生成的tokenValue + */ + public String createTokenValue(Object loginId, String deviceType, long timeout, Map extraData) { + return SaStrategy.instance.createToken.apply(loginId, loginType); + } /** - * 创建一个TokenValue - * @param loginId loginId - * @return 生成的tokenValue + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 */ - public String createTokenValue(Object loginId) { - return SaManager.getSaTokenAction().createToken(loginId, loginType); + public void setTokenValue(String tokenValue){ + setTokenValue(tokenValue, createSaLoginParameter()); } - - /** - * 在当前会话写入当前TokenValue - * @param tokenValue token值 - * @param cookieTimeout Cookie存活时间(秒) - */ + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + * @param cookieTimeout Cookie存活时间(秒) + */ public void setTokenValue(String tokenValue, int cookieTimeout){ - SaTokenConfig config = getConfig(); - - // 将token保存到[存储器]里 + setTokenValue(tokenValue, createSaLoginParameter().setTimeout(cookieTimeout)); + } + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + * @param loginParameter 登录参数 + */ + public void setTokenValue(String tokenValue, SaLoginParameter loginParameter){ + + // 先判断一下,如果提供 token 为空,则不执行任何动作 + if(SaFoxUtil.isEmpty(tokenValue)) { + return; + } + + // 1、将 token 写入到当前请求的 Storage 存储器里 + setTokenValueToStorage(tokenValue); + + // 2. 将 token 写入到当前会话的 Cookie 里 + if (getConfigOrGlobal().getIsReadCookie()) { + setTokenValueToCookie(tokenValue, loginParameter.getCookie(), loginParameter.getCookieTimeout()); + } + + // 3. 将 token 写入到当前请求的响应头中 + if(loginParameter.getIsWriteHeader()) { + setTokenValueToResponseHeader(tokenValue); + } + } + + /** + * 将 token 写入到当前请求的 Storage 存储器里 + * + * @param tokenValue 要保存的 token 值 + */ + public void setTokenValueToStorage(String tokenValue){ + // 1、获取当前请求的 Storage 存储器 SaStorage storage = SaHolder.getStorage(); - - // 判断是否配置了token前缀 - String tokenPrefix = config.getTokenPrefix(); - if(SaFoxUtil.isEmpty(tokenPrefix)) { - storage.set(splicingKeyJustCreatedSave(), tokenValue); + + // 2、保存 token + // - 如果没有配置前缀模式,直接保存 + // - 如果配置了前缀模式,则拼接上前缀保存 + String tokenPrefix = getConfigOrGlobal().getTokenPrefix(); + if( SaFoxUtil.isEmpty(tokenPrefix) ) { + storage.set(splicingKeyJustCreatedSave(), tokenValue); } else { - // 如果配置了token前缀,则拼接上前缀一起写入 - storage.set(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue); + storage.set(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue); } - - // 注入Cookie - if(config.getIsReadCookie()){ - SaResponse response = SaHolder.getResponse(); - response.addCookie(getTokenName(), tokenValue, "/", config.getCookieDomain(), cookieTimeout); + + // 3、以无前缀的方式再写入一次 + storage.set(SaTokenConsts.JUST_CREATED_NOT_PREFIX, tokenValue); + } + + /** + * 将 token 写入到当前会话的 Cookie 里 + * + * @param tokenValue token 值 + * @param cookieTimeout Cookie存活时间(单位:秒,填-1代表为内存Cookie,浏览器关闭后消失) + */ + public void setTokenValueToCookie(String tokenValue, int cookieTimeout){ + setTokenValueToCookie(tokenValue, null, cookieTimeout); + } + + /** + * 将 token 写入到当前会话的 Cookie 里 + * + * @param tokenValue token 值 + * @param cookieConfig Cookie 配置项 + * @param cookieTimeout Cookie存活时间(单位:秒,填-1代表为内存Cookie,浏览器关闭后消失) + */ + public void setTokenValueToCookie(String tokenValue, SaCookieConfig cookieConfig, int cookieTimeout){ + if(cookieConfig == null) { + cookieConfig = getConfigOrGlobal().getCookie(); } + SaCookie cookie = new SaCookie() + .setName(getTokenName()) + .setValue(tokenValue) + .setMaxAge(cookieTimeout) + .setDomain(cookieConfig.getDomain()) + .setPath(cookieConfig.getPath()) + .setSecure(cookieConfig.getSecure()) + .setHttpOnly(cookieConfig.getHttpOnly()) + .setSameSite(cookieConfig.getSameSite()) + .setExtraAttrs(cookieConfig.getExtraAttrs()) + ; + SaHolder.getResponse().addCookie(cookie); } - + + /** + * 将 token 写入到当前请求的响应头中 + * + * @param tokenValue token 值 + */ + public void setTokenValueToResponseHeader(String tokenValue){ + // 写入到响应头 + String tokenName = getTokenName(); + SaResponse response = SaHolder.getResponse(); + response.setHeader(tokenName, tokenValue); + + // 此处必须在响应头里指定 Access-Control-Expose-Headers: token-name,否则前端无法读取到这个响应头 + response.addHeader(SaResponse.ACCESS_CONTROL_EXPOSE_HEADERS, tokenName); + } + /** - * 获取当前TokenValue + * 获取当前请求的 token 值 + * * @return 当前tokenValue */ public String getTokenValue(){ - // 0. 获取相应对象 + return getTokenValue(false); + } + + /** + * 获取当前请求的 token 值 + * + * @param noPrefixThrowException 如果提交的 token 不带有指定的前缀,是否抛出异常 + * @return 当前tokenValue + */ + public String getTokenValue(boolean noPrefixThrowException){ + + // 1、获取前端提交的 token (包含前缀值) + String tokenValue = getTokenValueNotCut(); + + // 2、如果全局配置打开了前缀模式,则二次处理一下 + String tokenPrefix = getConfigOrGlobal().getTokenPrefix(); + if(SaFoxUtil.isNotEmpty(tokenPrefix)) { + + // 情况2.1:如果提交的 token 为空,则转为 null + if(SaFoxUtil.isEmpty(tokenValue)) { + tokenValue = null; + } + + // 情况2.2:如果 token 有值,但是并不是以指定的前缀开头 + else if(! tokenValue.startsWith(tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT)) { + if(noPrefixThrowException) { + throw NotLoginException.newInstance(loginType, NO_PREFIX, NO_PREFIX_MESSAGE + ",prefix=" + tokenPrefix, null).setCode(SaErrorCode.CODE_11017); + } else { + tokenValue = null; + } + } + + // 情况2.3:代码至此,说明 token 有值,且是以指定的前缀开头的,现在裁剪掉前缀 + else { + tokenValue = tokenValue.substring(tokenPrefix.length() + SaTokenConsts.TOKEN_CONNECTOR_CHAT.length()); + } + } + + // 3、返回 + return tokenValue; + } + + /** + * 获取当前请求的 token 值 (不裁剪前缀) + * + * @return / + */ + public String getTokenValueNotCut(){ + + // 获取相应对象 SaStorage storage = SaHolder.getStorage(); SaRequest request = SaHolder.getRequest(); - SaTokenConfig config = getConfig(); + SaTokenConfig config = getConfigOrGlobal(); String keyTokenName = getTokenName(); String tokenValue = null; - - // 1. 尝试从Storage里读取 + + // 1. 先尝试从 Storage 存储器里读取 if(storage.get(splicingKeyJustCreatedSave()) != null) { tokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave())); } - // 2. 尝试从请求体里面读取 - if(tokenValue == null && config.getIsReadBody()){ + // 2. 再尝试从 请求体 里面读取 + if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadBody()){ tokenValue = request.getParam(keyTokenName); } - // 3. 尝试从header里读取 - if(tokenValue == null && config.getIsReadHead()){ + // 3. 再尝试从 header 头里读取 + if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadHeader()){ tokenValue = request.getHeader(keyTokenName); } - // 4. 尝试从cookie里读取 - if(tokenValue == null && config.getIsReadCookie()){ + // 4. 最后尝试从 cookie 里读取 + if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadCookie()){ tokenValue = request.getCookieValue(keyTokenName); - } - - // 5. 如果打开了前缀模式 - String tokenPrefix = getConfig().getTokenPrefix(); - if(!SaFoxUtil.isEmpty(tokenPrefix) && !SaFoxUtil.isEmpty(tokenValue)) { - // 如果token以指定的前缀开头, 则裁剪掉它, 否则视为未提供token - if(tokenValue.startsWith(tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT)) { - tokenValue = tokenValue.substring(tokenPrefix.length() + SaTokenConsts.TOKEN_CONNECTOR_CHAT.length()); - } else { - tokenValue = null; + if(SaFoxUtil.isNotEmpty(tokenValue) && config.getCookieAutoFillPrefix()) { + tokenValue = config.getTokenPrefix() + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue; } } - - // 6. 返回 + + // 5. 至此,不管有没有读取到,都不再尝试了,直接返回 + return tokenValue; + } + + /** + * 获取当前请求的 token 值,如果获取不到则抛出异常 + * + * @return / + */ + public String getTokenValueNotNull(){ + String tokenValue = getTokenValue(true); + if(SaFoxUtil.isEmpty(tokenValue)) { + throw NotLoginException.newInstance(loginType, NOT_TOKEN, NOT_TOKEN_MESSAGE, null).setCode(SaErrorCode.CODE_11001); + } return tokenValue; } - + /** - * 获取当前会话的Token信息 - * @return token信息 + * 获取当前会话的 token 参数信息 + * + * @return token 参数信息 */ public SaTokenInfo getTokenInfo() { SaTokenInfo info = new SaTokenInfo(); @@ -173,1022 +405,2435 @@ public SaTokenInfo getTokenInfo() { info.tokenTimeout = getTokenTimeout(); info.sessionTimeout = getSessionTimeout(); info.tokenSessionTimeout = getTokenSessionTimeout(); - info.tokenActivityTimeout = getTokenActivityTimeout(); - info.loginDevice = getLoginDevice(); + info.tokenActiveTimeout = getTokenActiveTimeout(); + info.loginDeviceType = getLoginDeviceType(); return info; } - - - // ------------------- 登录相关操作 ------------------- - // 登录与注销 - + + // ------------------- 登录相关操作 ------------------- + + // --- 登录 + /** - * 会话登录 + * 会话登录 + * * @param id 账号id,建议的类型:(long | int | String) */ public void login(Object id) { - login(id, new SaLoginModel()); + login(id, createSaLoginParameter()); } /** - * 会话登录,并指定登录设备 + * 会话登录,并指定登录设备类型 + * * @param id 账号id,建议的类型:(long | int | String) - * @param device 设备标识 + * @param deviceType 设备类型 */ - public void login(Object id, String device) { - login(id, new SaLoginModel().setDevice(device)); + public void login(Object id, String deviceType) { + login(id, createSaLoginParameter().setDeviceType(deviceType)); } /** - * 会话登录,并指定是否 [记住我] + * 会话登录,并指定是否 [记住我] + * * @param id 账号id,建议的类型:(long | int | String) - * @param isLastingCookie 是否为持久Cookie + * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 */ public void login(Object id, boolean isLastingCookie) { - login(id, new SaLoginModel().setIsLastingCookie(isLastingCookie)); - } - - /** - * 会话登录,并指定所有登录参数Model - * @param id 登录id,建议的类型:(long | int | String) - * @param loginModel 此次登录的参数Model - */ - public void login(Object id, SaLoginModel loginModel) { - - // ------ 0、检查此账号是否已被封禁 - if(isDisable(id)) { - throw new DisableLoginException(loginType, id, getDisableTime(id)); - } - - // ------ 1、获取相应对象 - SaTokenConfig config = getConfig(); - SaTokenDao dao = SaManager.getSaTokenDao(); - loginModel.build(config); - - // ------ 2、生成一个token - String tokenValue = null; - // --- 如果允许并发登录 - if(config.getIsConcurrent()) { - // 如果配置为共享token, 则尝试从Session签名记录里取出token - if(config.getIsShare()) { - tokenValue = getTokenValueByLoginId(id, loginModel.getDevice()); + login(id, createSaLoginParameter().setIsLastingCookie(isLastingCookie)); + } + + /** + * 会话登录,并指定此次登录 token 的有效期, 单位:秒 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param timeout 此次登录 token 的有效期, 单位:秒 + */ + public void login(Object id, long timeout) { + login(id, createSaLoginParameter().setTimeout(timeout)); + } + + /** + * 会话登录,并指定所有登录参数 Model + * + * @param id 账号id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + */ + public void login(Object id, SaLoginParameter loginParameter) { + // 1、创建会话 + String token = createLoginSession(id, loginParameter); + + // 2、在当前客户端注入 token + setTokenValue(token, loginParameter); + } + + /** + * 创建指定账号 id 的登录会话数据 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public String createLoginSession(Object id) { + return createLoginSession(id, createSaLoginParameter()); + } + + /** + * 创建指定账号 id 的登录会话数据 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + * @return 返回会话令牌 + */ + public String createLoginSession(Object id, SaLoginParameter loginParameter) { + + // 1、先检查一下,传入的参数是否有效 + checkLoginArgs(id, loginParameter); + + // 2、给这个账号分配一个可用的 token + String tokenValue = distUsableToken(id, loginParameter); + + // 3、获取此账号的 Account-Session , 续期 + SaSession session = getSessionByLoginId(id, true, loginParameter.getTimeout()); + session.updateMinTimeout(loginParameter.getTimeout()); + + // 4、在 Account-Session 上记录本次登录的终端信息 + SaTerminalInfo terminalInfo = new SaTerminalInfo() + .setDeviceType(loginParameter.getDeviceType()) + .setDeviceId(loginParameter.getDeviceId()) + .setTokenValue(tokenValue) + .setExtraData(loginParameter.getTerminalExtraData()) + .setCreateTime(System.currentTimeMillis()); + session.addTerminal(terminalInfo); + + // 5、保存 token -> id 的映射关系,方便日后根据 token 找账号 id + saveTokenToIdMapping(tokenValue, id, loginParameter.getTimeout()); + + // 6、写入这个 token 的最后活跃时间 token-last-active + if(isOpenCheckActiveTimeout()) { + setLastActiveToNow(tokenValue, loginParameter.getActiveTimeout(), loginParameter.getTimeout()); + } + + // 7、如果该 token 对应的 Token-Session 已经存在,则需要给其续期 + SaSession tokenSession = getTokenSessionByToken(tokenValue, loginParameter.getRightNowCreateTokenSession()); + if(tokenSession != null) { + tokenSession.updateMinTimeout(loginParameter.getTimeout()); + } + + // 8、$$ 发布全局事件:账号 xxx 登录成功 + SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginParameter); + + // 9、检查此账号会话数量是否超出最大值,如果超过,则按照登录时间顺序,把最开始登录的给注销掉 + if(loginParameter.getMaxLoginCount() != -1) { + logoutByMaxLoginCount(id, session, null, loginParameter.getMaxLoginCount(), loginParameter.getOverflowLogoutMode()); + } + + // 10、一切处理完毕,返回会话凭证 token + return tokenValue; + } + + /** + * 为指定账号 id 的登录操作,分配一个可用的 token + * + * @param id 账号id + * @param loginParameter 此次登录的参数Model + * @return 返回 token + */ + protected String distUsableToken(Object id, SaLoginParameter loginParameter) { + + // 1、获取全局配置的 isConcurrent 参数 + if( ! loginParameter.getIsConcurrent()) { + // 如果配置为:不允许一个账号多地同时登录,则需要根据配置选择: + // 一.将这个账号的历史登录会话标记为:被顶下线 + // 二.提示错误并拒绝本次登录 + if (loginParameter.getReplacedLoginExitMode() == SaReplacedLoginExitMode.OLD_DEVICE){ + if(loginParameter.getReplacedRange() == SaReplacedRange.CURR_DEVICE_TYPE) { + replaced(id, loginParameter.getDeviceType()); + } + if(loginParameter.getReplacedRange() == SaReplacedRange.ALL_DEVICE_TYPE) { + replaced(id, createSaLogoutParameter()); + } + } else if (loginParameter.getReplacedLoginExitMode() == SaReplacedLoginExitMode.NEW_DEVICE){ + List terminalListByLoginId = getTerminalListByLoginId(id); + // 只有当存在有效地会话时才拒绝登录 + boolean hasActiveSession = terminalListByLoginId.stream() + .anyMatch(terminal -> isValidToken(terminal.getTokenValue())); + if (hasActiveSession) { + throw new SaTokenException("登录失败:当前账号已在其它客户端登录").setCode(SaErrorCode.CODE_11004); + } } - } else { - // --- 如果不允许并发登录 - // 如果此时[user-session]不为null,说明此账号在其他地正在登录,现在需要先把其它地的同设备token标记为被顶下线 - SaSession session = getSessionByLoginId(id, false); - if(session != null) { - List tokenSignList = session.getTokenSignList(); - for (TokenSign tokenSign : tokenSignList) { - if(tokenSign.getDevice().equals(loginModel.getDevice())) { - // 1. 将此token 标记为已顶替 - dao.update(splicingKeyTokenValue(tokenSign.getValue()), NotLoginException.BE_REPLACED); - // 2. 清理掉[token-最后操作时间] - clearLastActivity(tokenSign.getValue()); - // 3. 清理user-session上的token签名记录 - session.removeTokenSign(tokenSign.getValue()); - // $$ 通知监听器 - SaManager.getSaTokenListener().doReplaced(loginType, id, tokenSign.getValue(), tokenSign.getDevice()); - } + } + + // 2、如果调用者预定了要生成的 token,则直接返回这个预定的值,框架无需再操心了 + if(SaFoxUtil.isNotEmpty(loginParameter.getToken())) { + return loginParameter.getToken(); + } + + // 3、只有在配置了 [ 允许一个账号多地同时登录 ] 时,才尝试复用旧 token,这样可以避免不必要地查询,节省开销 + if(loginParameter.getIsConcurrent()) { + + // 3.1、如果配置了允许复用旧 token + if(isSupportShareToken() && loginParameter.getIsShare()) { + + // 根据 账号id + 设备类型,尝试获取旧的 token + String tokenValue = getTokenValueByLoginId(id, loginParameter.getDeviceType()); + + // 如果有值,那就直接复用 + if(SaFoxUtil.isNotEmpty(tokenValue)) { + return tokenValue; + } + + // 如果没值,那还是要继续往下走,尝试新建 token + // ↓↓↓ + } + } + + // 4、如果代码走到此处,说明未能成功复用旧 token,需要根据算法新建 token + return SaStrategy.instance.generateUniqueToken.execute( + "token", + getConfigOfMaxTryTimes(loginParameter), + () -> { + return createTokenValue(id, loginParameter.getDeviceType(), loginParameter.getTimeout(), loginParameter.getExtraData()); + }, + tokenValue -> { + return getLoginIdNotHandle(tokenValue) == null; } + ); + } + + /** + * 校验登录时的参数有效性,如果有问题会打印警告或抛出异常 + * + * @param id 账号id + * @param loginParameter 此次登录的参数Model + */ + protected void checkLoginArgs(Object id, SaLoginParameter loginParameter) { + + // 1、账号 id 不能为空 + if(SaFoxUtil.isEmpty(id)) { + throw new SaTokenException("loginId 不能为空").setCode(SaErrorCode.CODE_11002); + } + + // 2、账号 id 不能是异常标记值 + if(NotLoginException.ABNORMAL_LIST.contains(id.toString())) { + throw new SaTokenException("loginId 不能为以下值:" + NotLoginException.ABNORMAL_LIST); + } + + // 3、账号 id 不能是复杂类型 + if( ! SaFoxUtil.isBasicType(id.getClass())) { + SaManager.log.warn("loginId 应该为简单类型,例如:String | int | long,不推荐使用复杂类型:" + id.getClass()); + } + + // 4、判断当前 StpLogic 是否支持 extra 扩展参数 + if( ! isSupportExtra()) { + // 如果不支持,开发者却传入了 extra 扩展参数,那么就打印警告信息 + if(loginParameter.haveExtraData()) { + SaManager.log.warn("当前 StpLogic 不支持 extra 扩展参数模式,传入的 extra 参数将被忽略"); } } - // 如果至此,仍未成功创建tokenValue, 则开始生成一个 + + // 5、如果全局配置未启动动态 activeTimeout 功能,但是此次登录却传入了 activeTimeout 参数,那么就打印警告信息 + if( ! getConfigOrGlobal().getDynamicActiveTimeout() && loginParameter.getActiveTimeout() != null) { + SaManager.log.warn("当前全局配置未开启动态 activeTimeout 功能,传入的 activeTimeout 参数将被忽略"); + } + + } + + /** + * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public String getOrCreateLoginSession(Object id) { + String tokenValue = getTokenValueByLoginId(id); if(tokenValue == null) { - tokenValue = createTokenValue(id); - } - - // ------ 3. 获取[User-Session], 续期 - SaSession session = getSessionByLoginId(id, true); - session.updateMinTimeout(loginModel.getTimeout()); - - // 在session上记录token签名 - session.addTokenSign(new TokenSign(tokenValue, loginModel.getDevice())); - - // ------ 4. 持久化其它数据 - // token -> uid - dao.set(splicingKeyTokenValue(tokenValue), String.valueOf(id), loginModel.getTimeout()); - - // 写入 [最后操作时间] - setLastActivityToNow(tokenValue); - - // 在当前会话写入当前tokenValue - setTokenValue(tokenValue, loginModel.getCookieTimeout()); - - // $$ 通知监听器 - SaManager.getSaTokenListener().doLogin(loginType, id, loginModel); - } - - /** - * 会话注销 + tokenValue = createLoginSession(id, createSaLoginParameter()); + } + return tokenValue; + } + + // --- 注销 (根据 token) + + /** + * 在当前客户端会话注销 */ public void logout() { - // 如果连token都没有,那么无需执行任何操作 + logout(createSaLogoutParameter()); + } + + /** + * 在当前客户端会话注销,根据注销参数 + */ + public void logout(SaLogoutParameter logoutParameter) { + // 1、如果本次请求连 Token 都没有提交,那么它本身也不属于登录状态,此时无需执行任何操作 String tokenValue = getTokenValue(); - if(tokenValue == null) { - return; - } - // 如果打开了cookie模式,第一步,先把cookie清除掉 - if(getConfig().getIsReadCookie()){ - SaHolder.getResponse().deleteCookie(getTokenName()); + if(SaFoxUtil.isEmpty(tokenValue)) { + return; + } + + // 2、如果打开了 Cookie 模式,则先把 Cookie 数据清除掉 + if(getConfigOrGlobal().getIsReadCookie()){ + SaCookieConfig cfg = getConfigOrGlobal().getCookie(); + SaCookie cookie = new SaCookie() + .setName(getTokenName()) + .setValue(null) + // 有效期指定为0,做到以增代删 + .setMaxAge(0) + .setDomain(cfg.getDomain()) + .setPath(cfg.getPath()) + .setSecure(cfg.getSecure()) + .setHttpOnly(cfg.getHttpOnly()) + .setSameSite(cfg.getSameSite()) + ; + SaHolder.getResponse().addCookie(cookie); + } + + // 3、然后从当前 Storage 存储器里删除 Token + SaStorage storage = SaHolder.getStorage(); + storage.delete(splicingKeyJustCreatedSave()); + + // 4、清除当前上下文的 [ 活跃度校验 check 标记 ] + storage.delete(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY); + + // 5、清除这个 token 的其它相关信息 + if(logoutParameter.getRange() == SaLogoutRange.TOKEN) { + logoutByTokenValue(tokenValue, logoutParameter); + } else { + Object loginId = getLoginIdByTokenNotThinkFreeze(tokenValue); + if(loginId != null) { + if(!logoutParameter.getIsKeepFreezeOps() && isFreeze(tokenValue)) { + return; + } + logout(loginId, logoutParameter); + } } - logoutByTokenValue(tokenValue); } /** - * 会话注销,根据指定Token - * @param tokenValue 指定token + * 注销下线,根据指定 token + * + * @param tokenValue 指定 token */ public void logoutByTokenValue(String tokenValue) { - // 1. 清理掉[token-最后操作时间] - clearLastActivity(tokenValue); - - // 2. 清理Token-Session - SaManager.getSaTokenDao().delete(splicingKeyTokenSession(tokenValue)); - - // 3. 尝试清除token-id键值对 (先从db中获取loginId值,如果根本查不到loginId,那么无需继续操作 ) - String loginId = getLoginIdNotHandle(tokenValue); - if(loginId == null || NotLoginException.ABNORMAL_LIST.contains(loginId)) { - return; - } - SaManager.getSaTokenDao().delete(splicingKeyTokenValue(tokenValue)); - - // $$ 通知监听器 - SaManager.getSaTokenListener().doLogout(loginType, loginId, tokenValue); - - // 4. 尝试清理User-Session上的token签名 (如果为null或已被标记为异常, 那么无需继续执行 ) - SaSession session = getSessionByLoginId(loginId, false); - if(session == null) { - return; - } - session.removeTokenSign(tokenValue); - - // 5. 尝试注销User-Session - session.logoutByTokenSignCountToZero(); - } - - /** - * 会话注销,根据账号id (踢人下线) - *

当对方再次访问系统时,会抛出NotLoginException异常,场景值=-2 - * @param loginId 账号id - */ - public void logoutByLoginId(Object loginId) { - logoutByLoginId(loginId, null); - } - - /** - * 会话注销,根据账号id and 设备标识 (踢人下线) - *

当对方再次访问系统时,会抛出NotLoginException异常,场景值=-2

- * @param loginId 账号id - * @param device 设备标识 (填null代表所有注销设备) - */ - public void logoutByLoginId(Object loginId, String device) { - // 1. 先获取这个账号的[user-session], 如果为null,则不执行任何操作 - SaSession session = getSessionByLoginId(loginId, false); - if(session == null) { + logoutByTokenValue(tokenValue, createSaLogoutParameter()); + } + + /** + * 注销下线,根据指定 token、注销参数 + * + * @param tokenValue 指定 token + * @param logoutParameter / + */ + public void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + _logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.LOGOUT)); + } + + /** + * 踢人下线,根据指定 token + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param tokenValue 指定 token + */ + public void kickoutByTokenValue(String tokenValue) { + kickoutByTokenValue(tokenValue, createSaLogoutParameter()); + } + + /** + * 踢人下线,根据指定 token、注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param tokenValue 指定 token + * @param logoutParameter 注销参数 + */ + public void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + _logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.KICKOUT)); + } + + /** + * 顶人下线,根据指定 token + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param tokenValue 指定 token + */ + public void replacedByTokenValue(String tokenValue) { + replacedByTokenValue(tokenValue, createSaLogoutParameter()); + } + + /** + * 顶人下线,根据指定 token、注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param tokenValue 指定 token + * @param logoutParameter / + */ + public void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + _logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.REPLACED)); + } + + /** + * [work] 注销下线,根据指定 token 、注销参数 + * + * @param tokenValue 指定 token + * @param logoutParameter 注销参数 + */ + public void _logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + + // 1、判断一下:如果此 token 映射的是一个无效 loginId,则此处立即返回,不需要再往下处理了 + // 如果不提前截止,则后续的操作可能会写入意外数据 + Object loginId = getLoginIdByTokenNotThinkFreeze(tokenValue); + if( SaFoxUtil.isEmpty(loginId) ) { return; } - - // 2. 循环token签名列表,开始删除相关信息 - List tokenSignList = session.getTokenSignList(); - for (TokenSign tokenSign : tokenSignList) { - if(device == null || tokenSign.getDevice().equals(device)) { - // 1. 获取token - String tokenValue = tokenSign.getValue(); - // 2. 清理掉[token-最后操作时间] - clearLastActivity(tokenValue); - // 3. 标记:已被踢下线 - SaManager.getSaTokenDao().update(splicingKeyTokenValue(tokenValue), NotLoginException.KICK_OUT); - // 4. 清理账号session上的token签名 - session.removeTokenSign(tokenValue); - // $$ 通知监听器 - SaManager.getSaTokenListener().doLogoutByLoginId(loginType, loginId, tokenValue, tokenSign.getDevice()); - } + if(!logoutParameter.getIsKeepFreezeOps() && isFreeze(tokenValue)) { + return; } - // 3. 尝试注销session - session.logoutByTokenSignCountToZero(); - } - - // 查询相关 - - /** - * 当前会话是否已经登录 - * @return 是否已登录 - */ - public boolean isLogin() { - // 判断条件:不为null,并且不在异常项集合里 - return getLoginIdDefaultNull() != null; - } - - /** - * 检验当前会话是否已经登录,如未登录,则抛出异常 - */ - public void checkLogin() { - getLoginId(); - } - - /** - * 获取当前会话账号id, 如果未登录,则抛出异常 - * @return 账号id - */ - public Object getLoginId() { - // 如果正在[临时身份切换], 则返回临时身份 - if(isSwitch()) { - return getSwitchLoginId(); + + // 2、清除这个 token 的最后活跃时间记录 + if(isOpenCheckActiveTimeout()) { + clearLastActive(tokenValue); } - // 如果获取不到token,则抛出: 无token - String tokenValue = getTokenValue(); - if(tokenValue == null) { - throw NotLoginException.newInstance(loginType, NotLoginException.NOT_TOKEN); - } - // 查找此token对应loginId, 如果找不到则抛出:无效token - String loginId = getLoginIdNotHandle(tokenValue); - if(loginId == null) { - throw NotLoginException.newInstance(loginType, NotLoginException.INVALID_TOKEN); - } - // 如果是已经过期,则抛出已经过期 - if(loginId.equals(NotLoginException.TOKEN_TIMEOUT)) { - throw NotLoginException.newInstance(loginType, NotLoginException.TOKEN_TIMEOUT); - } - // 如果是已经被顶替下去了, 则抛出:已被顶下线 - if(loginId.equals(NotLoginException.BE_REPLACED)) { - throw NotLoginException.newInstance(loginType, NotLoginException.BE_REPLACED); - } - // 如果是已经被踢下线了, 则抛出:已被踢下线 - if(loginId.equals(NotLoginException.KICK_OUT)) { - throw NotLoginException.newInstance(loginType, NotLoginException.KICK_OUT); - } - // 检查是否已经 [临时过期] - checkActivityTimeout(tokenValue); - // 如果配置了自动续签, 则: 更新[最后操作时间] - if(getConfig().getAutoRenew()) { - updateLastActivityToNow(tokenValue); - } - // 至此,返回loginId - return loginId; - } - - /** - * 获取当前会话账号id, 如果未登录,则返回默认值 - * @param 返回类型 - * @param defaultValue 默认值 - * @return 登录id - */ - @SuppressWarnings("unchecked") - public T getLoginId(T defaultValue) { - Object loginId = getLoginIdDefaultNull(); - // 如果loginId为null,则返回默认值 - if(loginId == null) { - return defaultValue; + + // 3、清除 Token-Session + if( ! logoutParameter.getIsKeepTokenSession()) { + deleteTokenSession(tokenValue); + } + + // 4、清理或更改 Token 映射 + // 5、发布事件通知 + // SaLogoutMode.LOGOUT:注销下线 + if(logoutParameter.getMode() == SaLogoutMode.LOGOUT) { + deleteTokenToIdMapping(tokenValue); + SaTokenEventCenter.doLogout(loginType, loginId, tokenValue); } - // 开始尝试类型转换,只尝试三种类型:int、long、String - if(defaultValue instanceof Integer) { - return (T)Integer.valueOf(loginId.toString()); + // SaLogoutMode.LOGOUT:踢人下线 + if(logoutParameter.getMode() == SaLogoutMode.KICKOUT) { + updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT); + SaTokenEventCenter.doKickout(loginType, loginId, tokenValue); } - if(defaultValue instanceof Long) { - return (T)Long.valueOf(loginId.toString()); + // SaLogoutMode.REPLACED:顶人下线 + if(logoutParameter.getMode() == SaLogoutMode.REPLACED) { + updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED); + SaTokenEventCenter.doReplaced(loginType, loginId, tokenValue); } - if(defaultValue instanceof String) { - return (T)loginId.toString(); + + // 6、清理这个账号的 Account-Session 上的 terminal 信息,并且尝试注销掉 Account-Session + SaSession session = getSessionByLoginId(loginId, false); + if(session != null) { + session.removeTerminal(tokenValue); + session.logoutByTerminalCountToZero(); } - return (T)loginId; - } - - /** - * 获取当前会话账号id, 如果未登录,则返回null - * @return 账号id + } + + // --- 注销 (根据 loginId) + + /** + * 会话注销,根据账号id + * + * @param loginId 账号id + */ + public void logout(Object loginId) { + logout(loginId, createSaLogoutParameter()); + } + + /** + * 会话注销,根据账号id 和 设备类型 + * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型) + */ + public void logout(Object loginId, String deviceType) { + logout(loginId, createSaLogoutParameter().setDeviceType(deviceType)); + } + + /** + * 会话注销,根据账号id 和 注销参数 + * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public void logout(Object loginId, SaLogoutParameter logoutParameter) { + _logout(loginId, logoutParameter.setMode(SaLogoutMode.LOGOUT)); + } + + /** + * 踢人下线,根据账号id + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + */ + public void kickout(Object loginId) { + kickout(loginId, createSaLogoutParameter()); + } + + /** + * 踢人下线,根据账号id 和 设备类型 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型) + */ + public void kickout(Object loginId, String deviceType) { + kickout(loginId, createSaLogoutParameter().setDeviceType(deviceType)); + } + + /** + * 踢人下线,根据账号id 和 注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public void kickout(Object loginId, SaLogoutParameter logoutParameter) { + _logout(loginId, logoutParameter.setMode(SaLogoutMode.KICKOUT)); + } + + /** + * 顶人下线,根据账号id + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + */ + public void replaced(Object loginId) { + replaced(loginId, createSaLogoutParameter()); + } + + /** + * 顶人下线,根据账号id 和 设备类型 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表顶替该账号的所有设备类型) + */ + public void replaced(Object loginId, String deviceType) { + replaced(loginId, createSaLogoutParameter().setDeviceType(deviceType)); + } + + /** + * 顶人下线,根据账号id 和 注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public void replaced(Object loginId, SaLogoutParameter logoutParameter) { + _logout(loginId, logoutParameter.setMode(SaLogoutMode.REPLACED)); + } + + /** + * [work] 会话注销,根据账号id 和 注销参数 + * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public void _logout(Object loginId, SaLogoutParameter logoutParameter) { + // 1、获取此账号的 Account-Session,上面记录了此账号的所有登录客户端数据 + SaSession session = getSessionByLoginId(loginId, false); + if(session != null) { + + // 2、遍历此 SaTerminalInfo 客户端列表,清除相关数据 + List terminalList = session.terminalListCopy(); + for (SaTerminalInfo terminal: terminalList) { + // 不符合 deviceType 的跳过 + if( ! SaFoxUtil.isEmpty(logoutParameter.getDeviceType()) && ! logoutParameter.getDeviceType().equals(terminal.getDeviceType())) { + continue; + } + // 不符合 deviceId 的跳过 + if( ! SaFoxUtil.isEmpty(logoutParameter.getDeviceId()) && ! logoutParameter.getDeviceId().equals(terminal.getDeviceId())) { + continue; + } + _removeTerminal(session, terminal, logoutParameter); + } + + // 3、如果代码走到这里的时候,此账号已经没有客户端在登录了,则直接注销掉这个 Account-Session + if(logoutParameter.getMode() == SaLogoutMode.REPLACED) { + // 因为调用顶替下线时,一般都是在新客户端正在登录,所以此种情况不需要清除该账号的 Account-Session + // 如果清除了 Account-Session,将可能导致 Account-Session 被注销后又立刻创建出来,造成不必要的性能浪费 + } else { + session.logoutByTerminalCountToZero(); + } + } + } + + // --- 注销 (会话管理辅助方法) + + /** + * 在 Account-Session 上移除 Terminal 信息 (注销下线方式) + * @param session / + * @param terminal / + */ + public void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) { + _removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.LOGOUT)); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式) + * @param session / + * @param terminal / + */ + public void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) { + _removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.KICKOUT)); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式) + * @param session / + * @param terminal / + */ + public void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) { + _removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.REPLACED)); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (内部方法,仅为减少重复代码,外部调用意义不大) + * @param session Account-Session + * @param terminal 设备信息 + * @param logoutParameter 注销参数 + */ + public void _removeTerminal(SaSession session, SaTerminalInfo terminal, SaLogoutParameter logoutParameter) { + + Object loginId = session.getLoginId(); + String tokenValue = terminal.getTokenValue(); + + // 1、从 Account-Session 上清除此设备信息 + session.removeTerminal(tokenValue); + + // 2、清除这个 token 的最后活跃时间记录 + if(isOpenCheckActiveTimeout()) { + clearLastActive(tokenValue); + } + + // 3、清除这个 token 的 Token-Session 对象 + if( ! logoutParameter.getIsKeepTokenSession()) { + deleteTokenSession(tokenValue); + } + + // 4、清理或更改 Token 映射 + // 5、发布事件通知 + // SaLogoutMode.LOGOUT:注销下线 + if(logoutParameter.getMode() == SaLogoutMode.LOGOUT) { + deleteTokenToIdMapping(tokenValue); + SaTokenEventCenter.doLogout(loginType, loginId, tokenValue); + } + // SaLogoutMode.LOGOUT:踢人下线 + if(logoutParameter.getMode() == SaLogoutMode.KICKOUT) { + updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT); + SaTokenEventCenter.doKickout(loginType, loginId, tokenValue); + } + // SaLogoutMode.REPLACED:顶人下线 + if(logoutParameter.getMode() == SaLogoutMode.REPLACED) { + updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED); + SaTokenEventCenter.doReplaced(loginType, loginId, tokenValue); + } + } + + /** + * 如果指定账号 id、设备类型的登录客户端已经超过了指定数量,则按照登录时间顺序,把最开始登录的给注销掉 + * + * @param loginId 账号id + * @param session 此账号的 Account-Session 对象,可填写 null,框架将自动获取 + * @param deviceType 设备类型(填 null 代表注销此账号所有设备类型的登录) + * @param maxLoginCount 最大登录数量,超过此数量的将被注销 + * @param logoutMode 超出的客户端将以何种方式被注销 + */ + public void logoutByMaxLoginCount(Object loginId, SaSession session, String deviceType, int maxLoginCount, SaLogoutMode logoutMode) { + + // 1、如果调用者提供的 Account-Session 对象为空,则我们先手动获取一下 + if(session == null) { + session = getSessionByLoginId(loginId, false); + if(session == null) { + return; + } + } + + // 2、获取这个账号指定设备类型下的所有登录客户端 + List list = session.getTerminalListByDeviceType(deviceType); + + // 3、按照登录时间倒叙,超过 maxLoginCount 数量的,全部注销掉 + for (int i = 0; i < list.size() - maxLoginCount; i++) { + _removeTerminal(session, list.get(i), createSaLogoutParameter().setMode(logoutMode)); + } + + // 4、如果代码走到这里的时候,此账号已经没有客户端在登录了,则直接注销掉这个 Account-Session + session.logoutByTerminalCountToZero(); + } + + + // ---- 会话查询 + + /** + * 判断当前会话是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ + public boolean isLogin() { + // 判断条件: + // 1、获取到的 loginId 不为 null, + // 2、并且不在异常项集合里(此项在 getLoginIdDefaultNull() 方法里完成判断) + return getLoginIdDefaultNull() != null; + } + + /** + * 判断指定账号是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ + public boolean isLogin(Object loginId) { + // 判断条件:能否根据 loginId 查询到对应的 terminal 值 + return !getTerminalListByLoginId(loginId, null).isEmpty(); + } + + /** + * 检验当前会话是否已经登录,如未登录,则抛出异常 + */ + public void checkLogin() { + // 效果与 getLoginId() 相同,只是 checkLogin() 更加语义化一些 + getLoginId(); + } + + /** + * 获取当前会话账号id,如果未登录,则抛出异常 + * + * @return 账号id + */ + public Object getLoginId() { + + // 1、先判断一下当前会话是否正在 [ 临时身份切换 ], 如果是则返回临时身份 + if(isSwitch()) { + return getSwitchLoginId(); + } + + // 2、如果前端没有提交 token,则抛出异常: 未能读取到有效 token + String tokenValue = getTokenValue(true); + if(SaFoxUtil.isEmpty(tokenValue)) { + throw NotLoginException.newInstance(loginType, NOT_TOKEN, NOT_TOKEN_MESSAGE, null).setCode(SaErrorCode.CODE_11011); + } + + // 3、查找此 token 对应的 loginId,如果找不到则抛出:token 无效 + String loginId = getLoginIdNotHandle(tokenValue); + if(SaFoxUtil.isEmpty(loginId)) { + throw NotLoginException.newInstance(loginType, INVALID_TOKEN, INVALID_TOKEN_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11012); + } + + // 4、如果这个 token 指向的是值是:过期标记,则抛出:token 已过期 + if(loginId.equals(NotLoginException.TOKEN_TIMEOUT)) { + throw NotLoginException.newInstance(loginType, TOKEN_TIMEOUT, TOKEN_TIMEOUT_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11013); + } + + // 5、如果这个 token 指向的是值是:被顶替标记,则抛出:token 已被顶下线 + if(loginId.equals(NotLoginException.BE_REPLACED)) { + throw NotLoginException.newInstance(loginType, BE_REPLACED, BE_REPLACED_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11014); + } + + // 6、如果这个 token 指向的是值是:被踢下线标记,则抛出:token 已被踢下线 + if(loginId.equals(NotLoginException.KICK_OUT)) { + throw NotLoginException.newInstance(loginType, KICK_OUT, KICK_OUT_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11015); + } + + // 7、token 活跃频率检查 + checkActiveTimeoutByConfig(tokenValue); + + // ------ 至此,loginId 已经是一个合法的值,代表当前会话是一个正常的登录状态了 + + // 8、返回 loginId + return loginId; + } + + /** + * 获取当前会话账号id, 如果未登录,则返回默认值 + * + * @param 返回类型 + * @param defaultValue 默认值 + * @return 登录id + */ + @SuppressWarnings("unchecked") + public T getLoginId(T defaultValue) { + // 1、先正常获取一下当前会话的 loginId + Object loginId = getLoginIdDefaultNull(); + + // 2、如果 loginId 为 null,则返回默认值 + if(loginId == null) { + return defaultValue; + } + // 3、loginId 不为 null,则开始尝试类型转换 + if(defaultValue == null) { + return (T) loginId; + } + return (T) SaFoxUtil.getValueByType(loginId, defaultValue.getClass()); + } + + /** + * 获取当前会话账号id, 如果未登录,则返回null + * + * @return 账号id */ public Object getLoginIdDefaultNull() { - // 如果正在[临时身份切换] + + // 1、先判断一下当前会话是否正在 [ 临时身份切换 ], 如果是则返回临时身份 if(isSwitch()) { return getSwitchLoginId(); } - // 如果连token都是空的,则直接返回 - String tokenValue = getTokenValue(); - if(tokenValue == null) { - return null; - } - // loginId为null或者在异常项里面,均视为未登录, 返回null - Object loginId = getLoginIdNotHandle(tokenValue); - if(loginId == null || NotLoginException.ABNORMAL_LIST.contains(loginId)) { - return null; - } - // 如果已经[临时过期] - if(getTokenActivityTimeoutByToken(tokenValue) == SaTokenDao.NOT_VALUE_EXPIRE) { - return null; - } - // 执行到此,证明loginId已经是个正常的账号id了 - return loginId; - } - - /** - * 获取当前会话账号id, 并转换为String类型 - * @return 账号id - */ - public String getLoginIdAsString() { - return String.valueOf(getLoginId()); - } - - /** - * 获取当前会话账号id, 并转换为int类型 - * @return 账号id - */ - public int getLoginIdAsInt() { - return Integer.parseInt(String.valueOf(getLoginId())); - } - - /** - * 获取当前会话账号id, 并转换为long类型 - * @return 账号id - */ - public long getLoginIdAsLong() { - return Long.parseLong(String.valueOf(getLoginId())); - } - - /** - * 获取指定Token对应的账号id,如果未登录,则返回 null - * @param tokenValue token - * @return 账号id - */ - public Object getLoginIdByToken(String tokenValue) { - if(tokenValue == null) { - return null; - } - return getLoginIdNotHandle(tokenValue); - } - - /** - * 获取指定Token对应的账号id (不做任何特殊处理) - * @param tokenValue token值 - * @return 账号id - */ - public String getLoginIdNotHandle(String tokenValue) { - return SaManager.getSaTokenDao().get(splicingKeyTokenValue(tokenValue)); - } - - - // ------------------- Session相关 ------------------- - - /** - * 获取指定key的Session, 如果Session尚未创建,isCreate=是否新建并返回 - * @param sessionId SessionId - * @param isCreate 是否新建 - * @return Session对象 - */ - public SaSession getSessionBySessionId(String sessionId, boolean isCreate) { - SaSession session = SaManager.getSaTokenDao().getSession(sessionId); - if(session == null && isCreate) { - session = SaManager.getSaTokenAction().createSession(sessionId); - SaManager.getSaTokenDao().setSession(session, getConfig().getTimeout()); + + // 2、如果前端连 token 都没有提交,则直接返回 null + String tokenValue = getTokenValue(); + if(tokenValue == null) { + return null; + } + + // 3、根据 token 找到对应的 loginId,如果 loginId 为 null 或者属于异常标记里面,均视为未登录, 统一返回 null + Object loginId = getLoginIdNotHandle(tokenValue); + if( ! isValidLoginId(loginId) ) { + return null; + } + + // 4、如果 token 已被冻结,也返回 null + if(getTokenActiveTimeoutByToken(tokenValue) == SaTokenDao.NOT_VALUE_EXPIRE) { + return null; + } + + // 5、执行到此,证明此 loginId 已经是个正常合法的账号id了,可以返回 + return loginId; + } + + /** + * 获取当前会话账号id, 并转换为 String 类型 + * + * @return 账号id + */ + public String getLoginIdAsString() { + return String.valueOf(getLoginId()); + } + + /** + * 获取当前会话账号id, 并转换为 int 类型 + * + * @return 账号id + */ + public int getLoginIdAsInt() { + return Integer.parseInt(String.valueOf(getLoginId())); + } + + /** + * 获取当前会话账号id, 并转换为 long 类型 + * + * @return 账号id + */ + public long getLoginIdAsLong() { + return Long.parseLong(String.valueOf(getLoginId())); + } + + /** + * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶、被冻结等状态,则返回 null + * + * @param tokenValue token + * @return 账号id + */ + public Object getLoginIdByToken(String tokenValue) { + + Object loginId = getLoginIdByTokenNotThinkFreeze(tokenValue); + + if( SaFoxUtil.isNotEmpty(loginId) ) { + // 如果 token 已被冻结,也返回 null + long activeTimeout = getTokenActiveTimeoutByToken(tokenValue); + if(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) { + return null; + } + } + + return loginId; + } + + /** + * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结),则返回 null + * + * @param tokenValue token + * @return 账号id + */ + public Object getLoginIdByTokenNotThinkFreeze(String tokenValue) { + + // 1、如果提供的 token 为空,则直接返回 null + if(SaFoxUtil.isEmpty(tokenValue)) { + return null; + } + + // 2、查找此 token 对应的 loginId,如果找不到或找的到但属于无效值,则返回 null + String loginId = getLoginIdNotHandle(tokenValue); + if( ! isValidLoginId(loginId) ) { + return null; + } + + // 3、返回 + return loginId; + } + + /** + * 获取指定 token 对应的账号id (不做任何特殊处理) + * + * @param tokenValue token 值 + * @return 账号id + */ + public String getLoginIdNotHandle(String tokenValue) { + return getSaTokenDao().get(splicingKeyTokenValue(tokenValue)); + } + + /** + * 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) + * + * @param key 键值 + * @return 对应的扩展数据 + */ + public Object getExtra(String key) { + throw new ApiDisabledException("只有在集成 sa-token-jwt 插件后才可以使用 extra 扩展参数").setCode(SaErrorCode.CODE_11031); + } + + /** + * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) + * + * @param tokenValue 指定的 Token 值 + * @param key 键值 + * @return 对应的扩展数据 + */ + public Object getExtra(String tokenValue, String key) { + throw new ApiDisabledException("只有在集成 sa-token-jwt 插件后才可以使用 extra 扩展参数").setCode(SaErrorCode.CODE_11031); + } + + // ---- 其它操作 + + /** + * 判断一个 loginId 是否是有效的 (判断标准:不为 null、空字符串,且不在异常标记项里面) + * + * @param loginId 账号id + * @return / + */ + public boolean isValidLoginId(Object loginId) { + return SaFoxUtil.isNotEmpty(loginId) && !NotLoginException.ABNORMAL_LIST.contains(loginId.toString()); + } + + /** + * 判断一个 token 是否是有效的 (判断标准:使用此 token 查询到的 loginId 不为 Empty ) + * + * @param tokenValue / + * @return / + */ + public boolean isValidToken(String tokenValue) { + Object loginId = getLoginIdByToken(tokenValue); + return SaFoxUtil.isNotEmpty(loginId); + } + + /** + * 存储 token - id 映射关系 + * + * @param tokenValue token值 + * @param loginId 账号id + * @param timeout 会话有效期 (单位: 秒) + */ + public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) { + getSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout); + } + + /** + * 更改 token - id 映射关系 + * + * @param tokenValue token值 + * @param loginId 新的账号Id值 + */ + public void updateTokenToIdMapping(String tokenValue, Object loginId) { + // 先判断一下,是否传入了空值 + SaTokenException.notTrue(SaFoxUtil.isEmpty(loginId), "loginId 不能为空", SaErrorCode.CODE_11003); + + // 更新缓存中的 token 指向 + getSaTokenDao().update(splicingKeyTokenValue(tokenValue), loginId.toString()); + } + + /** + * 删除 token - id 映射 + * + * @param tokenValue token值 + */ + public void deleteTokenToIdMapping(String tokenValue) { + getSaTokenDao().delete(splicingKeyTokenValue(tokenValue)); + } + + + // ------------------- Account-Session 相关 ------------------- + + /** + * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,isCreate = 是否立即新建并返回 + * + * @param sessionId SessionId + * @param isCreate 是否新建 + * @param timeout 如果这个 SaSession 是新建的,则使用此值作为过期值(单位:秒),可填 null,代表使用全局 timeout 值 + * @param appendOperation 如果这个 SaSession 是新建的,则要追加执行的动作,可填 null,代表无追加动作 + * @return Session对象 + */ + public SaSession getSessionBySessionId(String sessionId, boolean isCreate, Long timeout, Consumer appendOperation) { + + // 如果提供的 sessionId 为 null,则直接返回 null + if(SaFoxUtil.isEmpty(sessionId)) { + throw new SaTokenException("SessionId 不能为空").setCode(SaErrorCode.CODE_11072); + } + + // 先检查这个 SaSession 是否已经存在,如果不存在且 isCreate=true,则新建并返回 + SaSession session = getSaTokenDao().getSession(sessionId); + + if(session == null && isCreate) { + // 创建这个 SaSession + session = SaStrategy.instance.createSession.apply(sessionId); + + // 追加操作 + if(appendOperation != null) { + appendOperation.accept(session); + } + + // 如果未提供 timeout,则根据相应规则设定默认的 timeout + if(timeout == null) { + // 如果是 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(); + } + } + + // 将这个 SaSession 入库 + getSaTokenDao().setSession(session, timeout); + } + return session; + } + + /** + * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,则返回 null + * + * @param sessionId SessionId + * @return Session对象 + */ + public SaSession getSessionBySessionId(String sessionId) { + return getSessionBySessionId(sessionId, false, null, null); + } + + /** + * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * + * @param loginId 账号id + * @param isCreate 是否新建 + * @param timeout 如果这个 SaSession 是新建的,则使用此值作为过期值(单位:秒),可填 null,代表使用全局 timeout 值 + * @return SaSession 对象 + */ + public SaSession getSessionByLoginId(Object loginId, boolean isCreate, Long timeout) { + if(SaFoxUtil.isEmpty(loginId)) { + throw new SaTokenException("Account-Session 获取失败:loginId 不能为空"); + } + return getSessionBySessionId(splicingKeySession(loginId), isCreate, timeout, session -> { + // 这里是该 Account-Session 首次创建时才会被执行的方法: + // 设定这个 SaSession 的各种基础信息:类型、账号体系、账号id + session.setType(SaTokenConsts.SESSION_TYPE__ACCOUNT); + session.setLoginType(getLoginType()); + session.setLoginId(loginId); + }); + } + + /** + * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * + * @param loginId 账号id + * @param isCreate 是否新建 + * @return SaSession 对象 + */ + public SaSession getSessionByLoginId(Object loginId, boolean isCreate) { + return getSessionByLoginId(loginId, isCreate, null); + } + + /** + * 获取指定账号 id 的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @param loginId 账号id + * @return SaSession 对象 + */ + public SaSession getSessionByLoginId(Object loginId) { + return getSessionByLoginId(loginId, true, null); + } + + /** + * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * + * @param isCreate 是否新建 + * @return Session对象 + */ + public SaSession getSession(boolean isCreate) { + return getSessionByLoginId(getLoginId(), isCreate); + } + + /** + * 获取当前已登录账号的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @return Session对象 + */ + public SaSession getSession() { + return getSession(true); + } + + + // ------------------- Token-Session 相关 ------------------- + + /** + * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,isCreate代表是否新建并返回 + * + * @param tokenValue token值 + * @param isCreate 是否新建 + * @return session对象 + */ + public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) { + // 1、token 为空,不允许创建 + if(SaFoxUtil.isEmpty(tokenValue)) { + throw new SaTokenException("Token-Session 获取失败:token 为空").setCode(SaErrorCode.CODE_11073); + } + + // 2、如果能查询到旧记录,则直接返回 + String sessionId = splicingKeyTokenSession(tokenValue); + SaSession tokenSession = getSaTokenDao().getSession(sessionId); + if(tokenSession != null) { + return tokenSession; + } + // 以下是查不到的情况 + + // 3、指定了不需要创建,返回 null + if( ! isCreate) { + return null; + } + // 以下是需要创建的情况 + + // 4、检查一下这个 token 是否为有效 token,无效 token 不允许创建 + if(getConfigOrGlobal().getTokenSessionCheckLogin() && ! isValidToken(tokenValue)) { + throw new SaTokenException("Token-Session 获取失败,token 无效: " + tokenValue).setCode(SaErrorCode.CODE_11074); + } + + // 5、创建 Token-Session 并返回 + return getSessionBySessionId(sessionId, true, null, session -> { + // 这里是该 Token-Session 首次创建时才会被执行的方法: + // 设定这个 SaSession 的各种基础信息:类型、账号体系、Token 值 + session.setType(SaTokenConsts.SESSION_TYPE__TOKEN); + session.setLoginType(getLoginType()); + session.setToken(tokenValue); + }); + } + + /** + * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @param tokenValue Token值 + * @return Session对象 + */ + public SaSession getTokenSessionByToken(String tokenValue) { + return getTokenSessionByToken(tokenValue, true); + } + + /** + * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,isCreate代表是否新建并返回 + * + * @param isCreate 是否新建 + * @return Session对象 + */ + public SaSession getTokenSession(boolean isCreate) { + String tokenValue = getTokenValue(); + checkActiveTimeoutByConfig(tokenValue); + return getTokenSessionByToken(tokenValue, isCreate); + } + + /** + * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @return Session对象 + */ + public SaSession getTokenSession() { + return getTokenSession(true); + } + + /** + * 获取当前匿名 Token-Session (可在未登录情况下使用的 Token-Session) + * + * @param isCreate 在 Token-Session 尚未创建的情况是否新建并返回 + * @return Token-Session 对象 + */ + public SaSession getAnonTokenSession(boolean isCreate) { + /* + * 情况1、如果调用方提供了有效 Token,则:直接返回其 [Token-Session] + * 情况2、如果调用方提供了无效 Token,或根本没有提供 Token,则:创建新 Token -> 返回 [ Token-Session ] + */ + String tokenValue = getTokenValue(); + + // q1 —— 判断这个 Token 是否有效,两个条件符合其一即可: + /* + * 条件1、能查出 Token-Session + * 条件2、能查出 LoginId + */ + if(SaFoxUtil.isNotEmpty(tokenValue)) { + + // 符合条件1 + SaSession session = getTokenSessionByToken(tokenValue, false); + if(session != null) { + return session; + } + + // 符合条件2 + String loginId = getLoginIdNotHandle(tokenValue); + if(isValidLoginId(loginId)) { + return getTokenSessionByToken(tokenValue, isCreate); + } + } + + // q2 —— 此时q2分两种情况: + /* + * 情况 2.1、isCreate=true:说明调用方想让框架帮其创建一个 SaSession,那框架就创建并返回 + * 情况 2.2、isCreate=false:说明调用方并不想让框架帮其创建一个 SaSession,那框架就直接返回 null + */ + if(isCreate) { + // 随机创建一个 Token + tokenValue = SaStrategy.instance.generateUniqueToken.execute( + "token", + getConfigOfMaxTryTimes(createSaLoginParameter()), + () -> { + return createTokenValue(null, null, getConfigOrGlobal().getTimeout(), null); + }, + token -> { + return getTokenSessionByToken(token, false) == null; + } + ); + + // 写入此 token 的最后活跃时间 + if(isOpenCheckActiveTimeout()) { + setLastActiveToNow(tokenValue, null, null); + } + + // 在当前上下文写入此 TokenValue + setTokenValue(tokenValue); + + // 返回其 Token-Session 对象 + final String finalTokenValue = tokenValue; + return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, getConfigOrGlobal().getTimeout(), session -> { + // 这里是该 Anon-Token-Session 首次创建时才会被执行的方法: + // 设定这个 SaSession 的各种基础信息:类型、账号体系、Token 值 + session.setType(SaTokenConsts.SESSION_TYPE__ANON); + session.setLoginType(getLoginType()); + session.setToken(finalTokenValue); + }); + } + else { + return null; + } + } + + /** + * 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) + * + * @return Token-Session 对象 + */ + public SaSession getAnonTokenSession() { + return getAnonTokenSession(true); + } + + /** + * 删除指定 token 的 Token-Session + * + * @param tokenValue token值 + */ + public void deleteTokenSession(String tokenValue) { + getSaTokenDao().delete(splicingKeyTokenSession(tokenValue)); + } + + + // ------------------- Active-Timeout token 最低活跃度 验证相关 ------------------- + + /** + * 写入指定 token 的 [ 最后活跃时间 ] 为当前时间戳 √√√ + * + * @param tokenValue 指定token + * @param activeTimeout 这个 token 的最低活跃频率,单位:秒,填 null 代表使用全局配置的 activeTimeout 值 + * @param timeout 保存数据时使用的 ttl 值,单位:秒,填 null 代表使用全局配置的 timeout 值 + */ + protected void setLastActiveToNow(String tokenValue, Long activeTimeout, Long timeout) { + + // 如果提供的 timeout 为null,则使用全局配置的 timeout 值 + SaTokenConfig config = getConfigOrGlobal(); + if(timeout == null) { + timeout = config.getTimeout(); + } + // activeTimeout 变量无需赋值默认值,因为当缓存中没有这个值时,会自动使用全局配置的值 + + // 将此 token 的 [ 最后活跃时间 ] 标记为当前时间戳 + String key = splicingKeyLastActiveTime(tokenValue); + String value = String.valueOf(System.currentTimeMillis()); + if(config.getDynamicActiveTimeout() && activeTimeout != null) { + value += "," + activeTimeout; + } + getSaTokenDao().set(key, value, timeout); + } + + /** + * 续签指定 token:将这个 token 的 [ 最后活跃时间 ] 更新为当前时间戳 + * + * @param tokenValue 指定token + */ + public void updateLastActiveToNow(String tokenValue) { + String key = splicingKeyLastActiveTime(tokenValue); + String value = new SaValue2Box(System.currentTimeMillis(), getTokenUseActiveTimeout(tokenValue)).toString(); + getSaTokenDao().update(key, value); + } + + /** + * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) + *

+ * 请注意: 即使 token 已被冻结 也可续签成功, + * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 + *

+ */ + public void updateLastActiveToNow() { + updateLastActiveToNow(getTokenValue()); + } + + /** + * 清除指定 Token 的 [ 最后活跃时间记录 ] + * + * @param tokenValue 指定 token + */ + protected void clearLastActive(String tokenValue) { + getSaTokenDao().delete(splicingKeyLastActiveTime(tokenValue)); + } + + /** + * 判断指定 token 是否已被冻结 + * + * @param tokenValue 指定 token + */ + public boolean isFreeze(String tokenValue) { + + // 1、获取这个 token 的剩余活跃有效期 + long activeTimeout = getTokenActiveTimeoutByToken(tokenValue); + + // 2、值为 -1 代表此 token 已经被设置永不冻结 + if(activeTimeout == SaTokenDao.NEVER_EXPIRE) { + return false; + } + + // 3、值为 -2 代表已被冻结 + if(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) { + return true; + } + return false; + } + + /** + * 根据全局配置决定是否校验指定 token 的活跃度 + * + * @param tokenValue 指定 token + */ + public void checkActiveTimeoutByConfig(String tokenValue) { + if(isOpenCheckActiveTimeout()) { + // storage.get(key, () -> {}) 可以避免一次请求多次校验,造成不必要的性能消耗 + SaHolder.getStorage().get(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY, () -> { + + // 1、检查此 token 的最后活跃时间是否已经超过了 active-timeout 的限制,如果是则代表其已被冻结,需要抛出:token 已被冻结 + checkActiveTimeout(tokenValue); + + // 2、如果配置了自动续签功能, 则: 更新这个 token 的最后活跃时间 (注意此处的续签是在续 active-timeout,而非 timeout) + if(SaStrategy.instance.autoRenew.apply(this)) { + updateLastActiveToNow(tokenValue); + } + + return true; + }); + } + } + + /** + * 检查指定 token 是否已被冻结,如果是则抛出异常 + * + * @param tokenValue 指定 token + */ + public void checkActiveTimeout(String tokenValue) { + if (isFreeze(tokenValue)) { + throw NotLoginException.newInstance(loginType, TOKEN_FREEZE, TOKEN_FREEZE_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11016); + } + } + + /** + * 检查当前 token 是否已被冻结,如果是则抛出异常 + */ + public void checkActiveTimeout() { + checkActiveTimeout(getTokenValue()); + } + + /** + * 获取指定 token 在缓存中的 activeTimeout 值,如果不存在则返回 null + * + * @param tokenValue 指定token + * @return / + */ + public Long getTokenUseActiveTimeout(String tokenValue) { + // 在未启用动态 activeTimeout 功能时,直接返回 null + if( ! getConfigOrGlobal().getDynamicActiveTimeout()) { + return null; + } + + // 先取出这个 token 的最后活跃时间值 + String key = splicingKeyLastActiveTime(tokenValue); + String value = getSaTokenDao().get(key); + + // 解析,无值的情况下返回 null + SaValue2Box box = new SaValue2Box(value); + return box.getValue2AsLong(null); + } + + /** + * 获取指定 token 在缓存中的 activeTimeout 值,如果不存在则返回全局配置的 activeTimeout 值 + * + * @param tokenValue 指定token + * @return / + */ + public long getTokenUseActiveTimeoutOrGlobalConfig(String tokenValue) { + Long activeTimeout = getTokenUseActiveTimeout(tokenValue); + if(activeTimeout == null) { + return getConfigOrGlobal().getActiveTimeout(); + } + return activeTimeout; + } + + /** + * 获取指定 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 + * + * @param tokenValue 指定token + * @return / + */ + public long getTokenLastActiveTime(String tokenValue) { + // 1、如果提供的 token 为 null,则返回 -2 + if(SaFoxUtil.isEmpty(tokenValue)) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + + // 2、获取这个 token 的最后活跃时间,13位时间戳 + String key = splicingKeyLastActiveTime(tokenValue); + String lastActiveTimeString = getSaTokenDao().get(key); + + // 3、查不到,返回-2 + if(lastActiveTimeString == null) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + + // 4、根据逗号切割字符串 + return new SaValue2Box(lastActiveTimeString).getValue1AsLong(); + } + + /** + * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 + * + * @return / + */ + public long getTokenLastActiveTime() { + return getTokenLastActiveTime(getTokenValue()); + } + + + // ------------------- 过期时间相关 ------------------- + + /** + * 获取当前会话 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public long getTokenTimeout() { + return getTokenTimeout(getTokenValue()); + } + + /** + * 获取指定 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @param token 指定token + * @return token剩余有效时间 + */ + public long getTokenTimeout(String token) { + return getSaTokenDao().getTimeout(splicingKeyTokenValue(token)); + } + + /** + * 获取指定账号 id 的 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @param loginId 指定loginId + * @return token剩余有效时间 + */ + public long getTokenTimeoutByLoginId(Object loginId) { + return getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValueByLoginId(loginId))); + } + + /** + * 获取当前登录账号的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public long getSessionTimeout() { + return getSessionTimeoutByLoginId(getLoginIdDefaultNull()); + } + + /** + * 获取指定账号 id 的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @param loginId 指定loginId + * @return token剩余有效时间 + */ + public long getSessionTimeoutByLoginId(Object loginId) { + return getSaTokenDao().getSessionTimeout(splicingKeySession(loginId)); + } + + /** + * 获取当前 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public long getTokenSessionTimeout() { + return getTokenSessionTimeoutByTokenValue(getTokenValue()); + } + + /** + * 获取指定 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @param tokenValue 指定token + * @return token 剩余有效时间 + */ + public long getTokenSessionTimeoutByTokenValue(String tokenValue) { + return getSaTokenDao().getSessionTimeout(splicingKeyTokenSession(tokenValue)); + } + + /** + * 获取当前 token 剩余活跃有效期:当前 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) + * + * @return / + */ + public long getTokenActiveTimeout() { + return getTokenActiveTimeoutByToken(getTokenValue()); + } + + /** + * 获取指定 token 剩余活跃有效期:这个 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) + * + * @param tokenValue 指定 token + * @return / + */ + public long getTokenActiveTimeoutByToken(String tokenValue) { + + // 如果全局配置了永不冻结, 则返回 -1 + if( ! isOpenCheckActiveTimeout() ) { + return SaTokenDao.NEVER_EXPIRE; + } + + // ------ 开始查询 + + // 先获取这个 token 的最后活跃时间,13位时间戳 + long lastActiveTime = getTokenLastActiveTime(tokenValue); + if(lastActiveTime == SaTokenDao.NOT_VALUE_EXPIRE) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + + // 实际时间差 + long timeDiff = (System.currentTimeMillis() - lastActiveTime) / 1000; + // 该 token 允许的时间差 + long allowTimeDiff = getTokenUseActiveTimeoutOrGlobalConfig(tokenValue); + if(allowTimeDiff == SaTokenDao.NEVER_EXPIRE) { + // 如果允许的时间差为 -1 ,则代表永不冻结,此处需要立即返回 -1 ,无需后续计算 + return SaTokenDao.NEVER_EXPIRE; + } + + // 校验这个时间差是否超过了允许的值 + // 计算公式为: 允许的最大时间差 - 实际时间差,判断是否 < 0, 如果是则代表已经被冻结 ,返回-2 + long activeTimeout = allowTimeDiff - timeDiff; + if(activeTimeout < 0) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } else { + // 否则代表没冻结,返回剩余有效时间 + return activeTimeout; + } + } + + /** + * 重置当前 token 的 timeout 有效期 + * + * @param timeout 要修改成为的有效时间 (单位: 秒) + */ + public void renewTimeout(long timeout) { + // 1、续期缓存数据 + String tokenValue = getTokenValue(); + renewTimeout(tokenValue, timeout); + + // 2、续期客户端 Cookie 有效期 + if(getConfigOrGlobal().getIsReadCookie()) { + // 如果 timeout = -1,代表永久,但是一般浏览器不支持永久 Cookie,所以此处设置为 int 最大值 + // 如果 timeout 大于 int 最大值,会造成数据溢出,所以也要将其设置为 int 最大值 + if(timeout == SaTokenDao.NEVER_EXPIRE || timeout > Integer.MAX_VALUE) { + timeout = Integer.MAX_VALUE; + } + setTokenValueToCookie(tokenValue, (int)timeout); + } + } + + /** + * 重置指定 token 的 timeout 有效期 + * + * @param tokenValue 指定 token + * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) + */ + public void renewTimeout(String tokenValue, long timeout) { + + // 1、如果 token 指向的 loginId 为空,或者属于异常项时,不进行续期操作 + Object loginId = getLoginIdByToken(tokenValue); + if(loginId == null) { + return; + } + + // 2、检查 token 合法性 + SaSession session = getSessionByLoginId(loginId); + if(session == null) { + throw new SaTokenException("未能查询到对应 Access-Session 会话,无法续期"); + } + if(session.getTerminal(tokenValue) == null) { + throw new SaTokenException("未能查询到对应终端信息,无法续期"); + } + + // 3、续期此 token 本身的有效期 (改 ttl) + SaTokenDao dao = getSaTokenDao(); + dao.updateTimeout(splicingKeyTokenValue(tokenValue), timeout); + + // 4、续期此 token 的 Token-Session 有效期 + SaSession tokenSession = getTokenSessionByToken(tokenValue, false); + if(tokenSession != null) { + tokenSession.updateTimeout(timeout); + } + + // 5、续期此 token 指向的账号的 Account-Session 有效期 + session.updateMinTimeout(timeout); + + // 6、更新此 token 的最后活跃时间 + if(isOpenCheckActiveTimeout()) { + dao.updateTimeout(splicingKeyLastActiveTime(tokenValue), timeout); + } + + // 7、$$ 发布事件:某某 token 被续期了 + SaTokenEventCenter.doRenewTimeout(loginType, loginId, tokenValue, timeout); + } + + + // ------------------- 角色认证操作 ------------------- + + /** + * 获取:当前账号的角色集合 + * + * @return / + */ + public List getRoleList() { + return getRoleList(getLoginId()); + } + + /** + * 获取:指定账号的角色集合 + * + * @param loginId 指定账号id + * @return / + */ + public List getRoleList(Object loginId) { + return SaManager.getStpInterface().getRoleList(loginId, loginType); + } + + /** + * 判断:当前账号是否拥有指定角色, 返回 true 或 false + * + * @param role 角色 + * @return / + */ + public boolean hasRole(String role) { + try { + return hasRole(getLoginId(), role); + } catch (NotLoginException e) { + return false; + } + } + + /** + * 判断:指定账号是否含有指定角色标识, 返回 true 或 false + * + * @param loginId 账号id + * @param role 角色标识 + * @return 是否含有指定角色标识 + */ + public boolean hasRole(Object loginId, String role) { + return hasElement(getRoleList(loginId), role); + } + + /** + * 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] + * + * @param roleArray 角色标识数组 + * @return true或false + */ + public boolean hasRoleAnd(String... roleArray){ + try { + checkRoleAnd(roleArray); + return true; + } catch (NotLoginException | NotRoleException e) { + return false; + } + } + + /** + * 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] + * + * @param roleArray 角色标识数组 + * @return true或false + */ + public boolean hasRoleOr(String... roleArray){ + try { + checkRoleOr(roleArray); + return true; + } catch (NotLoginException | NotRoleException e) { + return false; + } + } + + /** + * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException + * + * @param role 角色标识 + */ + public void checkRole(String role) { + if( ! hasRole(getLoginId(), role)) { + throw new NotRoleException(role, this.loginType).setCode(SaErrorCode.CODE_11041); + } + } + + /** + * 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] + * + * @param roleArray 角色标识数组 + */ + public void checkRoleAnd(String... roleArray){ + // 先获取当前是哪个账号id + Object loginId = getLoginId(); + + // 如果没有指定要校验的角色,那么直接跳过 + if(roleArray == null || roleArray.length == 0) { + return; + } + + // 开始校验 + List roleList = getRoleList(loginId); + for (String role : roleArray) { + if(!hasElement(roleList, role)) { + throw new NotRoleException(role, this.loginType).setCode(SaErrorCode.CODE_11041); + } + } + } + + /** + * 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] + * + * @param roleArray 角色标识数组 + */ + public void checkRoleOr(String... roleArray){ + // 先获取当前是哪个账号id + Object loginId = getLoginId(); + + // 如果没有指定权限,那么直接跳过 + if(roleArray == null || roleArray.length == 0) { + return; + } + + // 开始校验 + List roleList = getRoleList(loginId); + for (String role : roleArray) { + if(hasElement(roleList, role)) { + // 有的话提前退出 + return; + } + } + + // 代码至此,说明一个都没通过,需要抛出无角色异常 + throw new NotRoleException(roleArray[0], this.loginType).setCode(SaErrorCode.CODE_11041); + } + + + // ------------------- 权限认证操作 ------------------- + + /** + * 获取:当前账号的权限码集合 + * + * @return / + */ + public List getPermissionList() { + return getPermissionList(getLoginId()); + } + + /** + * 获取:指定账号的权限码集合 + * + * @param loginId 指定账号id + * @return / + */ + public List getPermissionList(Object loginId) { + return SaManager.getStpInterface().getPermissionList(loginId, loginType); + } + + /** + * 判断:当前账号是否含有指定权限, 返回 true 或 false + * + * @param permission 权限码 + * @return 是否含有指定权限 + */ + public boolean hasPermission(String permission) { + try { + return hasPermission(getLoginId(), permission); + } catch (NotLoginException e) { + return false; + } + } + + /** + * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false + * + * @param loginId 账号 id + * @param permission 权限码 + * @return 是否含有指定权限 + */ + public boolean hasPermission(Object loginId, String permission) { + return hasElement(getPermissionList(loginId), permission); + } + + /** + * 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ] + * + * @param permissionArray 权限码数组 + * @return true 或 false + */ + public boolean hasPermissionAnd(String... permissionArray){ + try { + checkPermissionAnd(permissionArray); + return true; + } catch (NotLoginException | NotPermissionException e) { + return false; + } + } + + /** + * 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] + * + * @param permissionArray 权限码数组 + * @return true 或 false + */ + public boolean hasPermissionOr(String... permissionArray){ + try { + checkPermissionOr(permissionArray); + return true; + } catch (NotLoginException | NotPermissionException e) { + return false; + } + } + + /** + * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param permission 权限码 + */ + public void checkPermission(String permission) { + if( ! hasPermission(getLoginId(), permission)) { + throw new NotPermissionException(permission, this.loginType).setCode(SaErrorCode.CODE_11051); + } + } + + /** + * 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ] + * + * @param permissionArray 权限码数组 + */ + public void checkPermissionAnd(String... permissionArray){ + // 先获取当前是哪个账号id + Object loginId = getLoginId(); + + // 如果没有指定权限,那么直接跳过 + if(permissionArray == null || permissionArray.length == 0) { + return; + } + + // 开始校验 + List permissionList = getPermissionList(loginId); + for (String permission : permissionArray) { + if(!hasElement(permissionList, permission)) { + throw new NotPermissionException(permission, this.loginType).setCode(SaErrorCode.CODE_11051); + } + } + } + + /** + * 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] + * + * @param permissionArray 权限码数组 + */ + public void checkPermissionOr(String... permissionArray){ + // 先获取当前是哪个账号id + Object loginId = getLoginId(); + + // 如果没有指定要校验的权限,那么直接跳过 + if(permissionArray == null || permissionArray.length == 0) { + return; + } + + // 开始校验 + List permissionList = getPermissionList(loginId); + for (String permission : permissionArray) { + if(hasElement(permissionList, permission)) { + // 有的话提前退出 + return; + } + } + + // 代码至此,说明一个都没通过,需要抛出无权限异常 + throw new NotPermissionException(permissionArray[0], this.loginType).setCode(SaErrorCode.CODE_11051); + } + + + + // ------------------- id 反查 token 相关操作 ------------------- + + /** + * 获取指定账号 id 的 token + *

+ * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + *

+ * + * @param loginId 账号id + * @return token值 + */ + public String getTokenValueByLoginId(Object loginId) { + return getTokenValueByLoginId(loginId, null); + } + + /** + * 获取指定账号 id 指定设备类型端的 token + *

+ * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + *

+ * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return token值 + */ + public String getTokenValueByLoginId(Object loginId, String deviceType) { + List tokenValueList = getTokenValueListByLoginId(loginId, deviceType); + return tokenValueList.isEmpty() ? null : tokenValueList.get(tokenValueList.size() - 1); + } + + /** + * 获取指定账号 id 的 token 集合 + * + * @param loginId 账号id + * @return 此 loginId 的所有相关 token + */ + public List getTokenValueListByLoginId(Object loginId) { + return getTokenValueListByLoginId(loginId, null); + } + + /** + * 获取指定账号 id 指定设备类型端的 token 集合 + * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return 此 loginId 的所有登录 token + */ + public List getTokenValueListByLoginId(Object loginId, String deviceType) { + // 如果该账号的 Account-Session 为 null,说明此账号尚没有客户端在登录,此时返回空集合 + SaSession session = getSessionByLoginId(loginId, false); + if(session == null) { + return new ArrayList<>(); + } + + // 按照设备类型进行筛选 + return session.getTokenValueListByDeviceType(deviceType); + } + + /** + * 获取指定账号 id 已登录设备信息集合 + * + * @param loginId 账号id + * @return 此 loginId 的所有登录 token + */ + public List getTerminalListByLoginId(Object loginId) { + return getTerminalListByLoginId(loginId, null); + } + + /** + * 获取指定账号 id 指定设备类型端的已登录设备信息集合 + * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return 此 loginId 的所有登录 token + */ + public List getTerminalListByLoginId(Object loginId, String deviceType) { + // 如果该账号的 Account-Session 为 null,说明此账号尚没有客户端在登录,此时返回空集合 + SaSession session = getSessionByLoginId(loginId, false); + if(session == null) { + return new ArrayList<>(); + } + + // 按照设备类型进行筛选 + return session.getTerminalListByDeviceType(deviceType); + } + + /** + * 获取指定账号 id 已登录设备信息集合,执行特定函数 + * + * @param loginId 账号id + * @param function 需要执行的函数 + */ + public void forEachTerminalList(Object loginId, SaTwoParamFunction function) { + // 如果该账号的 Account-Session 为 null,说明此账号尚没有客户端在登录,此时无需遍历 + SaSession session = getSessionByLoginId(loginId, false); + if(session == null) { + return; + } + + // 遍历 + session.forEachTerminalList(function); + } + + /** + * 返回当前 token 指向的 SaTerminalInfo 设备信息,如果 token 无效则返回 null + * + * @return / + */ + public SaTerminalInfo getTerminalInfo() { + return getTerminalInfoByToken(getTokenValue()); + } + + /** + * 返回指定 token 指向的 SaTerminalInfo 设备信息,如果 Token 无效则返回 null + * + * @param tokenValue 指定 token + * @return / + */ + public SaTerminalInfo getTerminalInfoByToken(String tokenValue) { + // 1、如果 token 为 null,直接提前返回 + if(SaFoxUtil.isEmpty(tokenValue)) { + return null; + } + + // 2、判断 Token 是否有效 + Object loginId = getLoginIdNotHandle(tokenValue); + if( ! isValidLoginId(loginId)) { + return null; + } + + // 3、判断 Account-Session 是否存在 + SaSession session = getSessionByLoginId(loginId, false); + if(session == null) { + return null; + } + + // 4、判断 Token 是否已被冻结 + if(isFreeze(tokenValue)) { + return null; } - return session; + + // 5、遍历 Account-Session 上的客户端 token 列表,寻找当前 token 对应的设备类型 + List terminalList = session.terminalListCopy(); + for (SaTerminalInfo terminal : terminalList) { + if(terminal.getTokenValue().equals(tokenValue)) { + return terminal; + } + } + + // 6、没有找到,还是返回 null + return null; } - /** - * 获取指定key的Session, 如果Session尚未创建,则返回null - * @param sessionId SessionId - * @return Session对象 + /** + * 返回当前会话的登录设备类型 + * + * @return 当前令牌的登录设备类型 */ - public SaSession getSessionBySessionId(String sessionId) { - return getSessionBySessionId(sessionId, false); + public String getLoginDeviceType() { + return getLoginDeviceTypeByToken(getTokenValue()); } - /** - * 获取指定账号id的Session, 如果Session尚未创建,isCreate=是否新建并返回 - * @param loginId 账号id - * @param isCreate 是否新建 - * @return Session对象 + /** + * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 */ - public SaSession getSessionByLoginId(Object loginId, boolean isCreate) { - return getSessionBySessionId(splicingKeySession(loginId), isCreate); + public String getLoginDeviceTypeByToken(String tokenValue) { + SaTerminalInfo terminalInfo = getTerminalInfoByToken(tokenValue); + return terminalInfo == null ? null : terminalInfo.getDeviceType(); } - /** - * 获取指定账号id的Session,如果Session尚未创建,则新建并返回 - * @param loginId 账号id - * @return Session对象 + /** + * 返回当前会话的登录设备 ID + * + * @return / */ - public SaSession getSessionByLoginId(Object loginId) { - return getSessionByLoginId(loginId, true); + public String getLoginDeviceId() { + return getLoginDeviceIdByToken(getTokenValue()); } - /** - * 获取当前会话的Session, 如果Session尚未创建,isCreate=是否新建并返回 - * @param isCreate 是否新建 - * @return Session对象 + /** + * 返回指定 token 会话的登录设备 ID + * + * @param tokenValue 指定token + * @return / */ - public SaSession getSession(boolean isCreate) { - return getSessionByLoginId(getLoginId(), isCreate); + public String getLoginDeviceIdByToken(String tokenValue) { + SaTerminalInfo terminalInfo = getTerminalInfoByToken(tokenValue); + return terminalInfo == null ? null : terminalInfo.getDeviceId(); } - - /** - * 获取当前会话的Session,如果Session尚未创建,则新建并返回 - * @return Session对象 + + /** + * 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 + * @param deviceId / + * @return / */ - public SaSession getSession() { - return getSession(true); + public boolean isTrustDeviceId(Object userId, String deviceId) { + // 先查询此账号的 Account-Session,如果连 Account-Session 都没有,那么此账号尚未登录,直接返回 false + SaSession session = getSessionByLoginId(userId, false); + if(session == null) { + return false; + } + + // 判断 + return session.isTrustDeviceId(deviceId); } - - // ------------------- token专属session ------------------- - /** - * 获取指定Token-Session,如果Session尚未创建,isCreate代表是否新建并返回 - * @param tokenValue token值 - * @param isCreate 是否新建 - * @return session对象 - */ - public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) { - return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate); + // ------------------- 会话管理 ------------------- + + /** + * 根据条件查询缓存中所有的 token + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return token集合 + */ + public List searchTokenValue(String keyword, int start, int size, boolean sortType) { + return getSaTokenDao().searchData(splicingKeyTokenValue(""), (keyword == null ? "" : keyword), start, size, sortType); } - - /** - * 获取指定Token-Session,如果Session尚未创建,则新建并返回 - * @param tokenValue Token值 - * @return Session对象 - */ - public SaSession getTokenSessionByToken(String tokenValue) { - return getSessionBySessionId(splicingKeyTokenSession(tokenValue), true); + + /** + * 根据条件查询缓存中所有的 SessionId + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return sessionId集合 + */ + public List searchSessionId(String keyword, int start, int size, boolean sortType) { + return getSaTokenDao().searchData(splicingKeySession(""), (keyword == null ? "" : keyword), start, size, sortType); } - /** - * 获取当前Token-Session,如果Session尚未创建,isCreate代表是否新建并返回 - * @param isCreate 是否新建 - * @return Session对象 - */ - public SaSession getTokenSession(boolean isCreate) { - // 如果配置了需要校验登录状态,则验证一下 - if(getConfig().getTokenSessionCheckLogin()) { - checkLogin(); - } else { - // 如果配置忽略token登录校验,则必须保证token不为null (token为null的时候随机创建一个) - String tokenValue = getTokenValue(); - if(tokenValue == null || Objects.equals(tokenValue, "")) { - // 随机一个token送给Ta - tokenValue = createTokenValue(null); - // 写入 [最后操作时间] - setLastActivityToNow(tokenValue); - // 在当前会话写入这个tokenValue - int cookieTimeout = (int)(getConfig().getTimeout() == SaTokenDao.NEVER_EXPIRE ? Integer.MAX_VALUE : getConfig().getTimeout()); - setTokenValue(tokenValue, cookieTimeout); - } - } - // 返回这个token对应的专属session - return getSessionBySessionId(splicingKeyTokenSession(getTokenValue()), isCreate); + /** + * 根据条件查询缓存中所有的 Token-Session-Id + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return sessionId集合 + */ + public List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { + return getSaTokenDao().searchData(splicingKeyTokenSession(""), (keyword == null ? "" : keyword), start, size, sortType); } - - /** - * 获取当前Token-Session,如果Session尚未创建,则新建并返回 - * @return Session对象 - */ - public SaSession getTokenSession() { - return getTokenSession(true); + + + // ------------------- 账号封禁 ------------------- + + /** + * 封禁:指定账号 + *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) + * + * @param loginId 指定账号id + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public void disable(Object loginId, long time) { + disableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.DEFAULT_DISABLE_LEVEL, time); } - - // ------------------- [临时过期] 验证相关 ------------------- - - /** - * 写入指定token的 [最后操作时间] 为当前时间戳 - * @param tokenValue 指定token - */ - protected void setLastActivityToNow(String tokenValue) { - // 如果token == null 或者 设置了[永不过期], 则立即返回 - if(tokenValue == null || getConfig().getActivityTimeout() == SaTokenDao.NEVER_EXPIRE) { - return; - } - // 将[最后操作时间]标记为当前时间戳 - SaManager.getSaTokenDao().set(splicingKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()), getConfig().getTimeout()); - } - - /** - * 清除指定token的 [最后操作时间] - * @param tokenValue 指定token - */ - protected void clearLastActivity(String tokenValue) { - // 如果token == null 或者 设置了[永不过期], 则立即返回 - if(tokenValue == null || getConfig().getActivityTimeout() == SaTokenDao.NEVER_EXPIRE) { - return; - } - // 删除[最后操作时间] - SaManager.getSaTokenDao().delete(splicingKeyLastActivityTime(tokenValue)); - // 清除标记 - SaHolder.getStorage().delete((SaTokenConsts.TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY)); - } - - /** - * 检查指定token 是否已经[临时过期],如果已经过期则抛出异常 - * @param tokenValue 指定token - */ - public void checkActivityTimeout(String tokenValue) { - // 如果token == null 或者 设置了[永不过期], 则立即返回 - if(tokenValue == null || getConfig().getActivityTimeout() == SaTokenDao.NEVER_EXPIRE) { - return; - } - // 如果本次请求已经有了[检查标记], 则立即返回 - SaStorage storage = SaHolder.getStorage(); - if(storage.get(SaTokenConsts.TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY) != null) { - return; - } - // ------------ 验证是否已经 [临时过期] - // 获取 [临时剩余时间] - long timeout = getTokenActivityTimeoutByToken(tokenValue); - // -1 代表此token已经被设置永不过期,无须继续验证 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - return; - } - // -2 代表已过期,抛出异常 - if(timeout == SaTokenDao.NOT_VALUE_EXPIRE) { - throw NotLoginException.newInstance(loginType, NotLoginException.TOKEN_TIMEOUT); - } - // --- 至此,验证已通过 - - // 打上[检查标记],标记一下当前请求已经通过验证,避免一次请求多次验证,造成不必要的性能消耗 - storage.set(SaTokenConsts.TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY, true); - } - - /** - * 检查当前token 是否已经[临时过期],如果已经过期则抛出异常 - */ - public void checkActivityTimeout() { - checkActivityTimeout(getTokenValue()); - } - - /** - * 续签指定token:(将 [最后操作时间] 更新为当前时间戳) - * @param tokenValue 指定token - */ - public void updateLastActivityToNow(String tokenValue) { - // 如果token == null 或者 设置了[永不过期], 则立即返回 - if(tokenValue == null || getConfig().getActivityTimeout() == SaTokenDao.NEVER_EXPIRE) { - return; - } - SaManager.getSaTokenDao().update(splicingKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis())); - } - - /** - * 续签当前token:(将 [最后操作时间] 更新为当前时间戳) - *

请注意: 即时token已经 [临时过期] 也可续签成功, - * 如果此场景下需要提示续签失败,可在此之前调用 checkActivityTimeout() 强制检查是否过期即可

- */ - public void updateLastActivityToNow() { - updateLastActivityToNow(getTokenValue()); - } - - - // ------------------- 过期时间相关 ------------------- - - /** - * 获取当前登录者的token剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ - public long getTokenTimeout() { - return SaManager.getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValue())); - } - - /** - * 获取指定loginId的token剩余有效时间 (单位: 秒) - * @param loginId 指定loginId - * @return token剩余有效时间 - */ - public long getTokenTimeoutByLoginId(Object loginId) { - return SaManager.getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValueByLoginId(loginId))); - } - - /** - * 获取当前登录者的Session剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ - public long getSessionTimeout() { - return getSessionTimeoutByLoginId(getLoginIdDefaultNull()); - } - - /** - * 获取指定loginId的Session剩余有效时间 (单位: 秒) - * @param loginId 指定loginId - * @return token剩余有效时间 - */ - public long getSessionTimeoutByLoginId(Object loginId) { - return SaManager.getSaTokenDao().getSessionTimeout(splicingKeySession(loginId)); - } - - /** - * 获取当前token的专属Session剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ - public long getTokenSessionTimeout() { - return getTokenSessionTimeoutByTokenValue(getTokenValue()); - } - - /** - * 获取指定token的专属Session剩余有效时间 (单位: 秒) - * @param tokenValue 指定token - * @return token剩余有效时间 - */ - public long getTokenSessionTimeoutByTokenValue(String tokenValue) { - return SaManager.getSaTokenDao().getSessionTimeout(splicingKeyTokenSession(tokenValue)); - } - - /** - * 获取当前token[临时过期]剩余有效时间 (单位: 秒) - * @return token[临时过期]剩余有效时间 - */ - public long getTokenActivityTimeout() { - return getTokenActivityTimeoutByToken(getTokenValue()); - } - - /** - * 获取指定token[临时过期]剩余有效时间 (单位: 秒) - * @param tokenValue 指定token - * @return token[临时过期]剩余有效时间 - */ - public long getTokenActivityTimeoutByToken(String tokenValue) { - // 如果token为null , 则返回 -2 - if(tokenValue == null) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - // 如果设置了永不过期, 则返回 -1 - if(getConfig().getActivityTimeout() == SaTokenDao.NEVER_EXPIRE) { - return SaTokenDao.NEVER_EXPIRE; - } - // ------ 开始查询 - // 获取相关数据 - String keyLastActivityTime = splicingKeyLastActivityTime(tokenValue); - String lastActivityTimeString = SaManager.getSaTokenDao().get(keyLastActivityTime); - // 查不到,返回-2 - if(lastActivityTimeString == null) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - // 计算相差时间 - long lastActivityTime = Long.parseLong(lastActivityTimeString); - long apartSecond = (System.currentTimeMillis() - lastActivityTime) / 1000; - long timeout = getConfig().getActivityTimeout() - apartSecond; - // 如果 < 0, 代表已经过期 ,返回-2 - if(timeout < 0) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - return timeout; - } - - - // ------------------- 角色验证操作 ------------------- - - /** - * 指定账号id是否含有角色标识, 返回true或false - * @param loginId 账号id - * @param role 角色标识 - * @return 是否含有指定角色标识 - */ - public boolean hasRole(Object loginId, String role) { - List roleList = SaManager.getStpInterface().getRoleList(loginId, loginType); - return SaManager.getSaTokenAction().hasElement(roleList, role); -// return !(roleList == null || roleList.contains(role) == false); - } - - /** - * 当前账号是否含有指定角色标识, 返回true或false - * @param role 角色标识 - * @return 是否含有指定角色标识 - */ - public boolean hasRole(String role) { - return hasRole(getLoginId(), role); - } - - /** - * 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException - * @param role 角色标识 - */ - public void checkRole(String role) { - if(!hasRole(role)) { - throw new NotRoleException(role, this.loginType); - } - } - - /** - * 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] - * @param roleArray 角色标识数组 - */ - public void checkRoleAnd(String... roleArray){ - Object loginId = getLoginId(); - List roleList = SaManager.getStpInterface().getRoleList(loginId, loginType); - for (String role : roleArray) { - if(!SaManager.getSaTokenAction().hasElement(roleList, role)) { - throw new NotRoleException(role, this.loginType); - } - } - } - - /** - * 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] - * @param roleArray 角色标识数组 - */ - public void checkRoleOr(String... roleArray){ - Object loginId = getLoginId(); - List roleList = SaManager.getStpInterface().getRoleList(loginId, loginType); - for (String role : roleArray) { - if(SaManager.getSaTokenAction().hasElement(roleList, role)) { - // 有的话提前退出 - return; - } - } - if(roleArray.length > 0) { - throw new NotRoleException(roleArray[0], this.loginType); - } - } - - - // ------------------- 权限验证操作 ------------------- - - /** - * 指定账号id是否含有指定权限, 返回true或false - * @param loginId 账号id - * @param permission 权限码 - * @return 是否含有指定权限 - */ - public boolean hasPermission(Object loginId, String permission) { - List permissionList = SaManager.getStpInterface().getPermissionList(loginId, loginType); - return SaManager.getSaTokenAction().hasElement(permissionList, permission); -// return !(permissionList == null || permissionList.contains(permission) == false); - } - - /** - * 当前账号是否含有指定权限, 返回true或false - * @param permission 权限码 - * @return 是否含有指定权限 - */ - public boolean hasPermission(String permission) { - return hasPermission(getLoginId(), permission); - } - - /** - * 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException - * @param permission 权限码 - */ - public void checkPermission(String permission) { - if(hasPermission(permission) == false) { - throw new NotPermissionException(permission, this.loginType); - } - } - - /** - * 当前账号是否含有指定权限 [指定多个,必须全部验证通过] - * @param permissionArray 权限码数组 - */ - public void checkPermissionAnd(String... permissionArray){ - Object loginId = getLoginId(); - List permissionList = SaManager.getStpInterface().getPermissionList(loginId, loginType); - for (String permission : permissionArray) { - if(!SaManager.getSaTokenAction().hasElement(permissionList, permission)) { - throw new NotPermissionException(permission, this.loginType); - } - } - } - - /** - * 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] - * @param permissionArray 权限码数组 - */ - public void checkPermissionOr(String... permissionArray){ - Object loginId = getLoginId(); - List permissionList = SaManager.getStpInterface().getPermissionList(loginId, loginType); - for (String permission : permissionArray) { - if(SaManager.getSaTokenAction().hasElement(permissionList, permission)) { - // 有的话提前退出 - return; - } - } - if(permissionArray.length > 0) { - throw new NotPermissionException(permissionArray[0], this.loginType); - } - } - - - // ------------------- id 反查token 相关操作 ------------------- - - /** - * 获取指定账号id的tokenValue - *

在配置为允许并发登录时,此方法只会返回队列的最后一个token, - * 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId + /** + * 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) + * * @param loginId 账号id - * @return token值 + * @return / */ - public String getTokenValueByLoginId(Object loginId) { - return getTokenValueByLoginId(loginId, SaTokenConsts.DEFAULT_LOGIN_DEVICE); + public boolean isDisable(Object loginId) { + return isDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.MIN_DISABLE_LEVEL); } - /** - * 获取指定账号id指定设备端的tokenValue - *

在配置为允许并发登录时,此方法只会返回队列的最后一个token, - * 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId + /** + * 校验:指定账号是否已被封禁,如果被封禁则抛出异常 + * * @param loginId 账号id - * @param device 设备标识 - * @return token值 - */ - public String getTokenValueByLoginId(Object loginId, String device) { - List tokenValueList = getTokenValueListByLoginId(loginId, device); - return tokenValueList.size() == 0 ? null : tokenValueList.get(tokenValueList.size() - 1); - } - - /** - * 获取指定账号id的tokenValue集合 - * @param loginId 账号id - * @return 此loginId的所有相关token - */ - public List getTokenValueListByLoginId(Object loginId) { - return getTokenValueListByLoginId(loginId, SaTokenConsts.DEFAULT_LOGIN_DEVICE); + */ + public void checkDisable(Object loginId) { + checkDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.MIN_DISABLE_LEVEL); } - /** - * 获取指定账号id指定设备端的tokenValue 集合 - * @param loginId 账号id - * @param device 设备标识 - * @return 此loginId的所有相关token - */ - public List getTokenValueListByLoginId(Object loginId, String device) { - // 如果session为null的话直接返回空集合 - SaSession session = getSessionByLoginId(loginId, false); - if(session == null) { - return Collections.emptyList(); - } - // 遍历解析 - List tokenSignList = session.getTokenSignList(); - List tokenValueList = new ArrayList<>(); - for (TokenSign tokenSign : tokenSignList) { - if(tokenSign.getDevice().equals(device)) { - tokenValueList.add(tokenSign.getValue()); - } - } - return tokenValueList; + /** + * 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * + * @param loginId 账号id + * @return / + */ + public long getDisableTime(Object loginId) { + return getDisableTime(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE); } - + /** - * 返回当前会话的登录设备 - * @return 当前令牌的登录设备 + * 解封:指定账号 + * + * @param loginId 账号id */ - public String getLoginDevice() { - // 如果没有token,直接返回 - String tokenValue = getTokenValue(); - if(tokenValue == null) { - return null; - } - // 如果还未登录,直接返回 null - if(!isLogin()) { - return null; - } - // 如果session为null的话直接返回 null - SaSession session = getSessionByLoginId(getLoginIdDefaultNull(), false); - if(session == null) { - return null; - } - // 遍历解析 - List tokenSignList = session.getTokenSignList(); - for (TokenSign tokenSign : tokenSignList) { - if(tokenSign.getValue().equals(tokenValue)) { - return tokenSign.getDevice(); - } - } - return null; + public void untieDisable(Object loginId) { + untieDisable(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE); } - - // ------------------- 会话管理 ------------------- + + // ------------------- 分类封禁 ------------------- /** - * 根据条件查询Token - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return token集合 - */ - public List searchTokenValue(String keyword, int start, int size) { - return SaManager.getSaTokenDao().searchData(splicingKeyTokenValue(""), keyword, start, size); + * 封禁:指定账号的指定服务 + *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) + * + * @param loginId 指定账号id + * @param service 指定服务 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public void disable(Object loginId, String service, long time) { + disableLevel(loginId, service, SaTokenConsts.DEFAULT_DISABLE_LEVEL, time); } - + /** - * 根据条件查询SessionId - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return sessionId集合 + * 判断:指定账号的指定服务 是否已被封禁(true=已被封禁, false=未被封禁) + * + * @param loginId 账号id + * @param service 指定服务 + * @return / */ - public List searchSessionId(String keyword, int start, int size) { - return SaManager.getSaTokenDao().searchData(splicingKeySession(""), keyword, start, size); + public boolean isDisable(Object loginId, String service) { + return isDisableLevel(loginId, service, SaTokenConsts.MIN_DISABLE_LEVEL); } /** - * 根据条件查询Token专属Session的Id - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return sessionId集合 + * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 + * + * @param loginId 账号id + * @param services 指定服务,可以指定多个 */ - public List searchTokenSessionId(String keyword, int start, int size) { - return SaManager.getSaTokenDao().searchData(splicingKeyTokenSession(""), keyword, start, size); + public void checkDisable(Object loginId, String... services) { + if(services != null) { + for (String service : services) { + checkDisableLevel(loginId, service, SaTokenConsts.MIN_DISABLE_LEVEL); + } + } } - - - // ------------------- 其它方法 ------------------- /** - * 根据注解(@SaCheckLogin)鉴权 - * @param at 注解对象 + * 获取:指定账号 指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * + * @param loginId 账号id + * @param service 指定服务 + * @return see note */ - public void checkByAnnotation(SaCheckLogin at) { - this.checkLogin(); + public long getDisableTime(Object loginId, String service) { + return getSaTokenDao().getTimeout(splicingKeyDisable(loginId, service)); } /** - * 根据注解(@SaCheckRole)鉴权 - * @param at 注解对象 + * 解封:指定账号、指定服务 + * + * @param loginId 账号id + * @param services 指定服务,可以指定多个 */ - public void checkByAnnotation(SaCheckRole at) { - String[] roleArray = at.value(); - if(at.mode() == SaMode.AND) { - this.checkRoleAnd(roleArray); - } else { - this.checkRoleOr(roleArray); + public void untieDisable(Object loginId, String... services) { + + // 先检查提供的参数是否有效 + if(SaFoxUtil.isEmpty(loginId)) { + throw new SaTokenException("请提供要解禁的账号").setCode(SaErrorCode.CODE_11062); + } + if(services == null || services.length == 0) { + throw new SaTokenException("请提供要解禁的服务").setCode(SaErrorCode.CODE_11063); + } + + // 遍历逐个解禁 + for (String service : services) { + // 解封 + getSaTokenDao().delete(splicingKeyDisable(loginId, service)); + + // $$ 发布事件 + SaTokenEventCenter.doUntieDisable(loginType, loginId, service); } } - + + + // ------------------- 阶梯封禁 ------------------- + /** - * 根据注解(@SaCheckPermission)鉴权 - * @param at 注解对象 - */ - public void checkByAnnotation(SaCheckPermission at) { - String[] permissionArray = at.value(); - if(at.mode() == SaMode.AND) { - this.checkPermissionAnd(permissionArray); - } else { - this.checkPermissionOr(permissionArray); + * 封禁:指定账号,并指定封禁等级 + * + * @param loginId 指定账号id + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public void disableLevel(Object loginId, int level, long time) { + disableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level, time); + } + + /** + * 封禁:指定账号的指定服务,并指定封禁等级 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public void disableLevel(Object loginId, String service, int level, long time) { + // 先检查提供的参数是否有效 + if(SaFoxUtil.isEmpty(loginId)) { + throw new SaTokenException("请提供要封禁的账号").setCode(SaErrorCode.CODE_11062); + } + if(SaFoxUtil.isEmpty(service)) { + throw new SaTokenException("请提供要封禁的服务").setCode(SaErrorCode.CODE_11063); } + if(level < SaTokenConsts.MIN_DISABLE_LEVEL && level != 0) { + throw new SaTokenException("封禁等级不可以小于最小值:" + SaTokenConsts.MIN_DISABLE_LEVEL + " (0除外)").setCode(SaErrorCode.CODE_11064); + } + + // 打上封禁标记 + getSaTokenDao().set(splicingKeyDisable(loginId, service), String.valueOf(level), time); + + // $$ 发布事件 + SaTokenEventCenter.doDisable(loginType, loginId, service, level, time); } /** - * 根据注解(@SaCheckSafe)鉴权 - * @param at 注解对象 - */ - public void checkByAnnotation(SaCheckSafe at) { - this.checkSafe(); + * 判断:指定账号是否已被封禁到指定等级 + * + * @param loginId 指定账号id + * @param level 指定封禁等级 + * @return / + */ + public boolean isDisableLevel(Object loginId, int level) { + return isDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level); } - - // ------------------- 账号封禁 ------------------- + /** + * 判断:指定账号的指定服务,是否已被封禁到指定等级 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 指定封禁等级 + * @return / + */ + public boolean isDisableLevel(Object loginId, String service, int level) { + // 1、先前置检查一下这个账号是否被封禁了 + int disableLevel = getDisableLevel(loginId, service); + if(disableLevel == SaTokenConsts.NOT_DISABLE_LEVEL) { + return false; + } + + // 2、再判断被封禁的等级是否达到了指定级别 + return disableLevel >= level; + } /** - * 封禁指定账号 - *

此方法不会直接将此账号id踢下线,而是在对方再次登录时抛出`DisableLoginException`异常 - * @param loginId 指定账号id - * @param disableTime 封禁时间, 单位: 秒 (-1=永久封禁) + * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) + * + * @param loginId 指定账号id + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ - public void disable(Object loginId, long disableTime) { - // 标注为已被封禁 - SaManager.getSaTokenDao().set(splicingKeyDisable(loginId), DisableLoginException.BE_VALUE, disableTime); - - // $$ 通知监听器 - SaManager.getSaTokenListener().doDisable(loginType, loginId, disableTime); + public void checkDisableLevel(Object loginId, int level) { + checkDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level); } - + /** - * 指定账号是否已被封禁 (true=已被封禁, false=未被封禁) - * @param loginId 账号id - * @return see note - */ - public boolean isDisable(Object loginId) { - return SaManager.getSaTokenDao().get(splicingKeyDisable(loginId)) != null; + * 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常) + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + */ + public void checkDisableLevel(Object loginId, String service, int level) { + // 1、先前置检查一下这个账号是否被封禁了 + int disableLevel = getDisableLevel(loginId, service); + if(disableLevel == SaTokenConsts.NOT_DISABLE_LEVEL) { + return; + } + + // 2、再判断被封禁的等级是否达到了指定级别 + if(disableLevel >= level) { + throw new DisableServiceException(loginType, loginId, service, disableLevel, level, getDisableTime(loginId, service)) + .setCode(SaErrorCode.CODE_11061); + } } /** - * 获取指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) - * @param loginId 账号id - * @return see note + * 获取:指定账号被封禁的等级,如果未被封禁则返回-2 + * + * @param loginId 指定账号id + * @return / */ - public long getDisableTime(Object loginId) { - return SaManager.getSaTokenDao().getTimeout(splicingKeyDisable(loginId)); + public int getDisableLevel(Object loginId) { + return getDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE); } - + /** - * 解封指定账号 - * @param loginId 账号id - */ - public void untieDisable(Object loginId) { - SaManager.getSaTokenDao().delete(splicingKeyDisable(loginId)); - - // $$ 通知监听器 - SaManager.getSaTokenListener().doUntieDisable(loginType, loginId); + * 获取:指定账号的 指定服务 被封禁的等级,如果未被封禁则返回-2 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @return / + */ + public int getDisableLevel(Object loginId, String service) { + // 1、先从缓存中查询数据,缓存中有值,以缓存值优先 + String value = getSaTokenDao().get(splicingKeyDisable(loginId, service)); + if(SaFoxUtil.isNotEmpty(value)) { + return SaFoxUtil.getValueByType(value, int.class); + } + + // 2、如果缓存中无数据,则从"数据加载器"中再次查询 + SaDisableWrapperInfo disableWrapperInfo = SaManager.getStpInterface().isDisabled(loginId, service); + + // 如果返回值 disableTime 有效,则代表返回结果需要写入缓存 + if(disableWrapperInfo.disableTime == SaTokenDao.NEVER_EXPIRE || disableWrapperInfo.disableTime > 0) { + disableLevel(loginId, service, disableWrapperInfo.disableLevel, disableWrapperInfo.disableTime); + } + + // 返回查询结果 + return disableWrapperInfo.disableLevel; } - - - // ------------------- 身份切换 ------------------- + + + // ------------------- 临时身份切换 ------------------- /** - * 临时切换身份为指定账号id - * @param loginId 指定loginId + * 临时切换身份为指定账号id + * + * @param loginId 指定loginId */ public void switchTo(Object loginId) { SaHolder.getStorage().set(splicingKeySwitch(), loginId); } - + /** * 结束临时切换身份 */ @@ -1197,138 +2842,243 @@ public void endSwitch() { } /** - * 当前是否正处于[身份临时切换]中 - * @return 是否正处于[身份临时切换]中 + * 判断当前请求是否正处于 [ 身份临时切换 ] 中 + * + * @return / */ public boolean isSwitch() { return SaHolder.getStorage().get(splicingKeySwitch()) != null; } - + /** - * 返回[身份临时切换]的loginId - * @return 返回[身份临时切换]的loginId + * 返回 [ 身份临时切换 ] 的 loginId + * + * @return / */ public Object getSwitchLoginId() { return SaHolder.getStorage().get(splicingKeySwitch()); } - + /** - * 在一个代码段里方法内,临时切换身份为指定账号id - * @param loginId 指定账号id - * @param function 要执行的方法 + * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 + * + * @param loginId 指定账号id + * @param function 要执行的方法 */ public void switchTo(Object loginId, SaFunction function) { try { switchTo(loginId); function.run(); - } catch (Exception e) { - throw e; } finally { endSwitch(); } } - // ------------------- 二级认证 ------------------- - + // ------------------- 二级认证 ------------------- + /** - * 在当前会话 开启二级认证 - * @param safeTime 维持时间 (单位: 秒) + * 在当前会话 开启二级认证 + * + * @param safeTime 维持时间 (单位: 秒) */ public void openSafe(long safeTime) { - long eff = System.currentTimeMillis() + safeTime * 1000; - getTokenSession().set(SaTokenConsts.SAFE_AUTH_SAVE_KEY, eff); + openSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE, safeTime); + } + + /** + * 在当前会话 开启二级认证 + * + * @param service 业务标识 + * @param safeTime 维持时间 (单位: 秒) + */ + public void openSafe(String service, long safeTime) { + // 1、开启二级认证前必须处于登录状态,否则抛出异常 + checkLogin(); + + // 2、写入指定的 可以 标记,打开二级认证 + String tokenValue = getTokenValueNotNull(); + getSaTokenDao().set(splicingKeySafe(tokenValue, service), SaTokenConsts.SAFE_AUTH_SAVE_VALUE, safeTime); + + // 3、$$ 发布事件,某某 token 令牌开启了二级认证 + SaTokenEventCenter.doOpenSafe(loginType, tokenValue, service, safeTime); } /** - * 当前会话 是否处于二级认证时间内 - * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + * 判断:当前会话是否处于二级认证时间内 + * + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public boolean isSafe() { - long eff = getTokenSession().get(SaTokenConsts.SAFE_AUTH_SAVE_KEY, 0L); - if(eff == 0 || eff < System.currentTimeMillis()) { + return isSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE); + } + + /** + * 判断:当前会话 是否处于指定业务的二级认证时间内 + * + * @param service 业务标识 + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public boolean isSafe(String service) { + return isSafe(getTokenValue(), service); + } + + /** + * 判断:指定 token 是否处于二级认证时间内 + * + * @param tokenValue Token 值 + * @param service 业务标识 + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public boolean isSafe(String tokenValue, String service) { + // 1、如果提供的 Token 为空,则直接视为未认证 + if(SaFoxUtil.isEmpty(tokenValue)) { return false; } - return true; + + // 2、如果此 token 不处于登录状态,也将其视为未认证 + Object loginId = getLoginIdNotHandle(tokenValue); + if( ! isValidLoginId(loginId) ) { + return false; + } + + // 3、如果缓存中可以查询出指定的键值,则代表已认证,否则视为未认证 + String value = getSaTokenDao().get(splicingKeySafe(tokenValue, service)); + return !(SaFoxUtil.isEmpty(value)); } /** - * 检查当前会话是否已通过二级认证,如未通过则抛出异常 + * 校验:当前会话是否已通过二级认证,如未通过则抛出异常 */ public void checkSafe() { - if (isSafe() == false) { - throw new NotSafeException(); + checkSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE); + } + + /** + * 校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常 + * + * @param service 业务标识 + */ + public void checkSafe(String service) { + // 1、必须先通过登录校验 + checkLogin(); + + // 2、再进行二级认证校验 + // 如果缓存中可以查询出指定的键值,则代表已认证,否则视为未认证 + String tokenValue = getTokenValue(); + String value = getSaTokenDao().get(splicingKeySafe(tokenValue, service)); + if(SaFoxUtil.isEmpty(value)) { + throw new NotSafeException(loginType, tokenValue, service).setCode(SaErrorCode.CODE_11071); } } - + /** - * 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) + * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) + * * @return 剩余有效时间 */ public long getSafeTime() { - long eff = getTokenSession().get(SaTokenConsts.SAFE_AUTH_SAVE_KEY, 0L); - if(eff == 0 || eff < System.currentTimeMillis()) { + return getSafeTime(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE); + } + + /** + * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) + * + * @param service 业务标识 + * @return 剩余有效时间 + */ + public long getSafeTime(String service) { + // 1、如果前端没有提交 Token,则直接视为未认证 + String tokenValue = getTokenValue(); + if(SaFoxUtil.isEmpty(tokenValue)) { return SaTokenDao.NOT_VALUE_EXPIRE; } - return (eff - System.currentTimeMillis()) / 1000; + + // 2、从缓存中查询这个 key 的剩余有效期 + return getSaTokenDao().getTimeout(splicingKeySafe(tokenValue, service)); } /** - * 在当前会话 结束二级认证 + * 在当前会话 结束二级认证 */ public void closeSafe() { - getTokenSession().delete(SaTokenConsts.SAFE_AUTH_SAVE_KEY); + closeSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE); + } + + /** + * 在当前会话 结束指定业务标识的二级认证 + * + * @param service 业务标识 + */ + public void closeSafe(String service) { + // 1、如果前端没有提交 Token,则无需任何操作 + String tokenValue = getTokenValue(); + if(SaFoxUtil.isEmpty(tokenValue)) { + return; + } + + // 2、删除 key + getSaTokenDao().delete(splicingKeySafe(tokenValue, service)); + + // 3、$$ 发布事件,某某 token 令牌关闭了二级认证 + SaTokenEventCenter.doCloseSafe(loginType, tokenValue, service); } - // ------------------- 返回相应key ------------------- + // ------------------- 拼接相应key ------------------- /** - * 拼接key:客户端 tokenName + * 获取:客户端 tokenName + * * @return key */ public String splicingKeyTokenName() { - return getConfig().getTokenName(); - } - - /** - * 拼接key: tokenValue 持久化 token-id + return getConfigOrGlobal().getTokenName(); + } + + /** + * 拼接: 在保存 token - id 映射关系时,应该使用的key + * * @param tokenValue token值 * @return key */ public String splicingKeyTokenValue(String tokenValue) { - return getConfig().getTokenName() + ":" + loginType + ":token:" + tokenValue; + return getConfigOrGlobal().getTokenName() + ":" + loginType + ":token:" + tokenValue; } - - /** - * 拼接key: Session 持久化 + + /** + * 拼接: 在保存 Account-Session 时,应该使用的 key + * * @param loginId 账号id * @return key */ public String splicingKeySession(Object loginId) { - return getConfig().getTokenName() + ":" + loginType + ":session:" + loginId; + return getConfigOrGlobal().getTokenName() + ":" + loginType + ":session:" + loginId; } - - /** - * 拼接key: tokenValue的专属session + + /** + * 拼接:在保存 Token-Session 时,应该使用的 key + * * @param tokenValue token值 * @return key */ public String splicingKeyTokenSession(String tokenValue) { - return getConfig().getTokenName() + ":" + loginType + ":token-session:" + tokenValue; + return getConfigOrGlobal().getTokenName() + ":" + loginType + ":token-session:" + tokenValue; } - - /** - * 拼接key: 指定token的最后操作时间 持久化 + + /** + * 拼接: 在保存 token 最后活跃时间时,应该使用的 key + * * @param tokenValue token值 * @return key */ - public String splicingKeyLastActivityTime(String tokenValue) { - return getConfig().getTokenName() + ":" + loginType + ":last-activity:" + tokenValue; + public String splicingKeyLastActiveTime(String tokenValue) { + return getConfigOrGlobal().getTokenName() + ":" + loginType + ":last-active:" + tokenValue; } /** - * 在进行身份切换时,使用的存储key + * 拼接:在进行临时身份切换时,应该使用的 key + * * @return key */ public String splicingKeySwitch() { @@ -1336,33 +3086,156 @@ public String splicingKeySwitch() { } /** - * 如果token为本次请求新创建的,则以此字符串为key存储在当前request中 + * 如果 token 为本次请求新创建的,则以此字符串为 key 存储在当前 request 中 + * * @return key */ public String splicingKeyJustCreatedSave() { - return SaTokenConsts.JUST_CREATED_SAVE_KEY + loginType; + // return SaTokenConsts.JUST_CREATED_SAVE_KEY + loginType; + return SaTokenConsts.JUST_CREATED; } - /** - * 拼接key: 账号封禁 + /** + * 拼接: 在保存服务封禁标记时,应该使用的 key + * * @param loginId 账号id - * @return key + * @param service 具体封禁的服务 + * @return key */ - public String splicingKeyDisable(Object loginId) { - return getConfig().getTokenName() + ":" + loginType + ":disable:" + loginId; + public String splicingKeyDisable(Object loginId, String service) { + return getConfigOrGlobal().getTokenName() + ":" + loginType + ":disable:" + service + ":" + loginId; } - - // ------------------- Bean对象代理 ------------------- - /** - * 返回配置对象 - * @return 配置对象 + * 拼接: 在保存业务二级认证标记时,应该使用的 key + * + * @param tokenValue 要认证的 Token + * @param service 要认证的业务标识 + * @return key */ - public SaTokenConfig getConfig() { - // 为什么再次代理一层? 为某些极端业务场景下[需要不同StpLogic不同配置]提供便利 - return SaManager.getConfig(); + public String splicingKeySafe(String tokenValue, String service) { + // 格式::<账号类型>::<业务标识>: + // 形如:satoken:login:safe:important:gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__ + return getConfigOrGlobal().getTokenName() + ":" + loginType + ":safe:" + service + ":" + tokenValue; + } + + + // ------------------- Bean 对象、字段代理 ------------------- + + /** + * 返回当前 StpLogic 使用的持久化对象 + * + * @return / + */ + public SaTokenDao getSaTokenDao() { + return SaManager.getSaTokenDao(); + } + + /** + * 返回当前 StpLogic 是否支持共享 token 策略 + * + * @return / + */ + public boolean isSupportShareToken() { + return getConfigOrGlobal().getIsShare(); + } + + /** + * 返回全局配置是否开启了 Token 活跃度校验,返回 true 代表已打开,返回 false 代表不打开,此时永不冻结 token + * + * @return / + */ + public boolean isOpenCheckActiveTimeout() { + SaTokenConfig cfg = getConfigOrGlobal(); + return cfg.getActiveTimeout() != SaTokenDao.NEVER_EXPIRE || cfg.getDynamicActiveTimeout(); + } + + /** + * 返回全局配置的 Cookie 保存时长,单位:秒 (根据全局 timeout 计算) + * + * @return Cookie 应该保存的时长 + */ + public int getConfigOfCookieTimeout() { + long timeout = getConfigOrGlobal().getTimeout(); + if(timeout == SaTokenDao.NEVER_EXPIRE) { + return Integer.MAX_VALUE; + } + return (int) timeout; + } + + /** + * 返回全局配置的 maxTryTimes 值,在每次创建 token 时,对其唯一性测试的最高次数(-1=不测试) + * + * @param loginParameter / + * @return / + */ + public int getConfigOfMaxTryTimes(SaLoginParameter loginParameter) { + return loginParameter.getMaxTryTimes(); + } + + /** + * 判断:集合中是否包含指定元素(模糊匹配) + * + * @param list 集合 + * @param element 元素 + * @return / + */ + public boolean hasElement(List list, String element) { + return SaStrategy.instance.hasElement.apply(list, element); + } + + /** + * 当前 StpLogic 对象是否支持 token 扩展参数 + * + * @return / + */ + public boolean isSupportExtra() { + return false; + } + + /** + * 根据当前配置对象创建一个 SaLoginParameter 对象 + * + * @return / + */ + public SaLoginParameter createSaLoginParameter() { + return new SaLoginParameter(getConfigOrGlobal()); + } + + /** + * 根据当前配置对象创建一个 SaLogoutParameter 对象 + * + * @return / + */ + public SaLogoutParameter createSaLogoutParameter() { + return new SaLogoutParameter(getConfigOrGlobal()); + } + + + + // ------------------- 过期方法 ------------------- + + /** + *

请更换为 getLoginDeviceType

+ * 返回当前会话的登录设备类型 + * + * @return 当前令牌的登录设备类型 + */ + @Deprecated + public String getLoginDevice() { + return getLoginDeviceType(); + } + + /** + *

请更换为 getLoginDeviceTypeByToken

+ * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 + */ + @Deprecated + public String getLoginDeviceByToken(String tokenValue) { + return getLoginDeviceTypeByToken(tokenValue); } - - + } 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 3d8837a9b..24ce91532 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 @@ -1,56 +1,143 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp; -import java.util.List; - +import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaTwoParamFunction; +import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.SaTerminalInfo; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; + +import java.util.List; /** - * Sa-Token 权限验证工具类 - * @author kong + * Sa-Token 权限认证工具类 + * + * @author click33 + * @since 1.0.0 */ public class StpUtil { + private StpUtil() {} + /** - * 账号类型标识 + * 多账号体系下的类型标识 */ public static final String TYPE = "login"; /** - * 底层的 StpLogic 对象 + * 底层使用的 StpLogic 对象 */ - public static StpLogic stpLogic = new StpLogic(TYPE); + public static StpLogic stpLogic = new StpLogic(TYPE); /** * 获取当前 StpLogic 的账号类型 - * @return See Note + * + * @return / */ public static String getLoginType(){ return stpLogic.getLoginType(); } + /** + * 安全的重置 StpLogic 对象 + * + *
1、更改此账户的 StpLogic 对象 + *
2、put 到全局 StpLogic 集合中 + *
3、发送日志 + * + * @param newStpLogic / + */ + public static void setStpLogic(StpLogic newStpLogic) { + // 1、重置此账户的 StpLogic 对象 + stpLogic = newStpLogic; + + // 2、添加到全局 StpLogic 集合中 + // 以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic + SaManager.putStpLogic(newStpLogic); + + // 3、$$ 发布事件:更新了 stpLogic 对象 + SaTokenEventCenter.doSetStpLogic(stpLogic); + } + + /** + * 获取 StpLogic 对象 + * + * @return / + */ + public static StpLogic getStpLogic() { + return stpLogic; + } + - // =================== 获取token 相关 =================== + // ------------------- 获取 token 相关 ------------------- /** - * 返回token名称 - * @return 此StpLogic的token名称 + * 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀 + * + * @return / */ public static String getTokenName() { return stpLogic.getTokenName(); } - /** - * 在当前会话写入当前TokenValue - * @param tokenValue token值 - * @param cookieTimeout Cookie存活时间(秒) - */ + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + */ + public static void setTokenValue(String tokenValue){ + stpLogic.setTokenValue(tokenValue); + } + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + * @param cookieTimeout Cookie存活时间(秒) + */ public static void setTokenValue(String tokenValue, int cookieTimeout){ stpLogic.setTokenValue(tokenValue, cookieTimeout); } - + /** - * 获取当前TokenValue + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + * @param loginParameter 登录参数 + */ + public static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){ + stpLogic.setTokenValue(tokenValue, loginParameter); + } + + /** + * 将 token 写入到当前请求的 Storage 存储器里 + * + * @param tokenValue 要保存的 token 值 + */ + public static void setTokenValueToStorage(String tokenValue){ + stpLogic.setTokenValueToStorage(tokenValue); + } + + /** + * 获取当前请求的 token 值 + * * @return 当前tokenValue */ public static String getTokenValue() { @@ -58,18 +145,31 @@ public static String getTokenValue() { } /** - * 获取当前会话的Token信息 - * @return token信息 + * 获取当前请求的 token 值 (不裁剪前缀) + * + * @return / + */ + public static String getTokenValueNotCut(){ + return stpLogic.getTokenValueNotCut(); + } + + /** + * 获取当前会话的 token 参数信息 + * + * @return token 参数信息 */ public static SaTokenInfo getTokenInfo() { return stpLogic.getTokenInfo(); } - // =================== 登录相关操作 =================== + // ------------------- 登录相关操作 ------------------- + + // --- 登录 /** - * 会话登录 + * 会话登录 + * * @param id 账号id,建议的类型:(long | int | String) */ public static void login(Object id) { @@ -77,496 +177,1172 @@ public static void login(Object id) { } /** - * 会话登录,并指定登录设备 + * 会话登录,并指定登录设备类型 + * * @param id 账号id,建议的类型:(long | int | String) - * @param device 设备标识 + * @param deviceType 设备类型 */ - public static void login(Object id, String device) { - stpLogic.login(id, device); + public static void login(Object id, String deviceType) { + stpLogic.login(id, deviceType); } /** - * 会话登录,并指定是否 [记住我] + * 会话登录,并指定是否 [记住我] + * * @param id 账号id,建议的类型:(long | int | String) - * @param isLastingCookie 是否为持久Cookie + * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 */ public static void login(Object id, boolean isLastingCookie) { stpLogic.login(id, isLastingCookie); } /** - * 会话登录,并指定所有登录参数Model - * @param id 登录id,建议的类型:(long | int | String) - * @param loginModel 此次登录的参数Model + * 会话登录,并指定此次登录 token 的有效期, 单位:秒 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param timeout 此次登录 token 的有效期, 单位:秒 */ - public static void login(Object id, SaLoginModel loginModel) { - stpLogic.login(id, loginModel); + public static void login(Object id, long timeout) { + stpLogic.login(id, timeout); } - - /** - * 会话注销 + + /** + * 会话登录,并指定所有登录参数 Model + * + * @param id 账号id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + */ + public static void login(Object id, SaLoginParameter loginParameter) { + stpLogic.login(id, loginParameter); + } + + /** + * 创建指定账号 id 的登录会话数据 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public static String createLoginSession(Object id) { + return stpLogic.createLoginSession(id); + } + + /** + * 创建指定账号 id 的登录会话数据 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + * @return 返回会话令牌 + */ + public static String createLoginSession(Object id, SaLoginParameter loginParameter) { + return stpLogic.createLoginSession(id, loginParameter); + } + + /** + * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public static String getOrCreateLoginSession(Object id) { + return stpLogic.getOrCreateLoginSession(id); + } + + // --- 注销 (根据 token) + + /** + * 在当前客户端会话注销 */ public static void logout() { stpLogic.logout(); } /** - * 会话注销,根据指定Token - * @param tokenValue 指定token + * 在当前客户端会话注销,根据注销参数 + */ + public static void logout(SaLogoutParameter logoutParameter) { + stpLogic.logout(logoutParameter); + } + + /** + * 注销下线,根据指定 token + * + * @param tokenValue 指定 token */ public static void logoutByTokenValue(String tokenValue) { stpLogic.logoutByTokenValue(tokenValue); } - + + /** + * 注销下线,根据指定 token、注销参数 + * + * @param tokenValue 指定 token + * @param logoutParameter / + */ + public static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.logoutByTokenValue(tokenValue, logoutParameter); + } + + /** + * 踢人下线,根据指定 token + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param tokenValue 指定 token + */ + public static void kickoutByTokenValue(String tokenValue) { + stpLogic.kickoutByTokenValue(tokenValue); + } + /** - * 会话注销,根据账号id (踢人下线) - *

当对方再次访问系统时,会抛出NotLoginException异常,场景值=-2

- * @param loginId 账号id + * 踢人下线,根据指定 token、注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param tokenValue 指定 token + * @param logoutParameter 注销参数 */ - public static void logoutByLoginId(Object loginId) { - stpLogic.logoutByLoginId(loginId); + public static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.kickoutByTokenValue(tokenValue, logoutParameter); } /** - * 会话注销,根据账号id and 设备标识 (踢人下线) - *

当对方再次访问系统时,会抛出NotLoginException异常,场景值=-2 - * @param loginId 账号id - * @param device 设备标识 + * 顶人下线,根据指定 token + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param tokenValue 指定 token */ - public static void logoutByLoginId(Object loginId, String device) { - stpLogic.logoutByLoginId(loginId, device); + public static void replacedByTokenValue(String tokenValue) { + stpLogic.replacedByTokenValue(tokenValue); } - // 查询相关 + /** + * 顶人下线,根据指定 token、注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param tokenValue 指定 token + * @param logoutParameter / + */ + public static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.replacedByTokenValue(tokenValue, logoutParameter); + } - /** - * 当前会话是否已经登录 - * @return 是否已登录 - */ + // --- 注销 (根据 loginId) + + /** + * 会话注销,根据账号id + * + * @param loginId 账号id + */ + public static void logout(Object loginId) { + stpLogic.logout(loginId); + } + + /** + * 会话注销,根据账号id 和 设备类型 + * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型) + */ + public static void logout(Object loginId, String deviceType) { + stpLogic.logout(loginId, deviceType); + } + + /** + * 会话注销,根据账号id 和 注销参数 + * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void logout(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.logout(loginId, logoutParameter); + } + + /** + * 踢人下线,根据账号id + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + */ + public static void kickout(Object loginId) { + stpLogic.kickout(loginId); + } + + /** + * 踢人下线,根据账号id 和 设备类型 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型) + */ + public static void kickout(Object loginId, String deviceType) { + stpLogic.kickout(loginId, deviceType); + } + + /** + * 踢人下线,根据账号id 和 注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void kickout(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.kickout(loginId, logoutParameter); + } + + /** + * 顶人下线,根据账号id + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + */ + public static void replaced(Object loginId) { + stpLogic.replaced(loginId); + } + + /** + * 顶人下线,根据账号id 和 设备类型 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表顶替该账号的所有设备类型) + */ + public static void replaced(Object loginId, String deviceType) { + stpLogic.replaced(loginId, deviceType); + } + + /** + * 顶人下线,根据账号id 和 注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void replaced(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.replaced(loginId, logoutParameter); + } + + // --- 注销 (会话管理辅助方法) + + /** + * 在 Account-Session 上移除 Terminal 信息 (注销下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByLogout(session, terminal); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByKickout(session, terminal); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByReplaced(session, terminal); + } + + + // 会话查询 + + /** + * 判断当前会话是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ public static boolean isLogin() { return stpLogic.isLogin(); } - /** - * 检验当前会话是否已经登录,如未登录,则抛出异常 - */ + /** + * 判断指定账号是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ + public static boolean isLogin(Object loginId) { + return stpLogic.isLogin(loginId); + } + + /** + * 检验当前会话是否已经登录,如未登录,则抛出异常 + */ public static void checkLogin() { stpLogic.checkLogin(); } - /** - * 获取当前会话账号id, 如果未登录,则抛出异常 - * @return 账号id - */ + /** + * 获取当前会话账号id,如果未登录,则抛出异常 + * + * @return 账号id + */ public static Object getLoginId() { return stpLogic.getLoginId(); } - /** - * 获取当前会话账号id, 如果未登录,则返回默认值 + /** + * 获取当前会话账号id, 如果未登录,则返回默认值 + * * @param 返回类型 * @param defaultValue 默认值 - * @return 登录id + * @return 登录id */ public static T getLoginId(T defaultValue) { return stpLogic.getLoginId(defaultValue); } - /** - * 获取当前会话账号id, 如果未登录,则返回null - * @return 账号id + /** + * 获取当前会话账号id, 如果未登录,则返回null + * + * @return 账号id */ public static Object getLoginIdDefaultNull() { return stpLogic.getLoginIdDefaultNull(); } - /** - * 获取当前会话账号id, 并转换为String类型 - * @return 账号id + /** + * 获取当前会话账号id, 并转换为 String 类型 + * + * @return 账号id */ public static String getLoginIdAsString() { return stpLogic.getLoginIdAsString(); } - /** - * 获取当前会话账号id, 并转换为int类型 - * @return 账号id + /** + * 获取当前会话账号id, 并转换为 int 类型 + * + * @return 账号id */ public static int getLoginIdAsInt() { return stpLogic.getLoginIdAsInt(); } /** - * 获取当前会话账号id, 并转换为long类型 - * @return 账号id + * 获取当前会话账号id, 并转换为 long 类型 + * + * @return 账号id */ public static long getLoginIdAsLong() { return stpLogic.getLoginIdAsLong(); } - /** - * 获取指定Token对应的账号id,如果未登录,则返回 null - * @param tokenValue token - * @return 账号id - */ + /** + * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶、被冻结等状态,则返回 null + * + * @param tokenValue token + * @return 账号id + */ public static Object getLoginIdByToken(String tokenValue) { return stpLogic.getLoginIdByToken(tokenValue); } - + + /** + * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结),则返回 null + * + * @param tokenValue token + * @return 账号id + */ + public static Object getLoginIdByTokenNotThinkFreeze(String tokenValue) { + return stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue); + } + + /** + * 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) + * + * @param key 键值 + * @return 对应的扩展数据 + */ + public static Object getExtra(String key) { + return stpLogic.getExtra(key); + } + + /** + * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) + * + * @param tokenValue 指定的 Token 值 + * @param key 键值 + * @return 对应的扩展数据 + */ + public static Object getExtra(String tokenValue, String key) { + return stpLogic.getExtra(tokenValue, key); + } + - // =================== session相关 =================== + // ------------------- Account-Session 相关 ------------------- - /** - * 获取指定账号id的Session, 如果Session尚未创建,isCreate=是否新建并返回 + /** + * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * * @param loginId 账号id * @param isCreate 是否新建 - * @return Session对象 + * @return SaSession 对象 */ public static SaSession getSessionByLoginId(Object loginId, boolean isCreate) { return stpLogic.getSessionByLoginId(loginId, isCreate); } - /** - * 获取指定key的Session, 如果Session尚未创建,则返回null + /** + * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,则返回 null + * * @param sessionId SessionId - * @return Session对象 + * @return Session对象 */ public static SaSession getSessionBySessionId(String sessionId) { return stpLogic.getSessionBySessionId(sessionId); } - /** - * 获取指定账号id的Session,如果Session尚未创建,则新建并返回 - * @param loginId 账号id - * @return Session对象 + /** + * 获取指定账号 id 的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @param loginId 账号id + * @return SaSession 对象 */ public static SaSession getSessionByLoginId(Object loginId) { return stpLogic.getSessionByLoginId(loginId); } - /** - * 获取当前会话的Session, 如果Session尚未创建,isCreate=是否新建并返回 + /** + * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * * @param isCreate 是否新建 - * @return Session对象 + * @return Session对象 */ public static SaSession getSession(boolean isCreate) { return stpLogic.getSession(isCreate); } - /** - * 获取当前会话的Session,如果Session尚未创建,则新建并返回 - * @return Session对象 + /** + * 获取当前已登录账号的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @return Session对象 */ public static SaSession getSession() { return stpLogic.getSession(); } - // =================== token专属session =================== - - /** - * 获取指定Token-Session,如果Session尚未创建,则新建并返回 + // ------------------- Token-Session 相关 ------------------- + + /** + * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 + * * @param tokenValue Token值 - * @return Session对象 + * @return Session对象 */ public static SaSession getTokenSessionByToken(String tokenValue) { return stpLogic.getTokenSessionByToken(tokenValue); } - - /** - * 获取当前Token-Session,如果Session尚未创建,则新建并返回 - * @return Session对象 + + /** + * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @return Session对象 */ public static SaSession getTokenSession() { return stpLogic.getTokenSession(); } + /** + * 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) + * + * @return Token-Session 对象 + */ + public static SaSession getAnonTokenSession() { + return stpLogic.getAnonTokenSession(); + } + - // =================== [临时过期] 验证相关 =================== + // ------------------- Active-Timeout token 最低活跃度 验证相关 ------------------- /** - * 检查当前token 是否已经[临时过期],如果已经过期则抛出异常 - */ - public static void checkActivityTimeout() { - stpLogic.checkActivityTimeout(); - } + * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) + *

+ * 请注意: 即使 token 已被冻结 也可续签成功, + * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 + *

+ */ + public static void updateLastActiveToNow() { + stpLogic.updateLastActiveToNow(); + } - /** - * 续签当前token:(将 [最后操作时间] 更新为当前时间戳) - *

请注意: 即时token已经 [临时过期] 也可续签成功, - * 如果此场景下需要提示续签失败,可在此之前调用 checkActivityTimeout() 强制检查是否过期即可

- */ - public static void updateLastActivityToNow() { - stpLogic.updateLastActivityToNow(); + /** + * 检查当前 token 是否已被冻结,如果是则抛出异常 + */ + public static void checkActiveTimeout() { + stpLogic.checkActiveTimeout(); } - - // =================== 过期时间相关 =================== + /** + * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 + * + * @return / + */ + public static long getTokenLastActiveTime() { + return stpLogic.getTokenLastActiveTime(); + } + - /** - * 获取当前登录者的token剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ + // ------------------- 过期时间相关 ------------------- + + /** + * 获取当前会话 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ public static long getTokenTimeout() { return stpLogic.getTokenTimeout(); } - - /** - * 获取当前登录者的Session剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ + + /** + * 获取指定 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @param token 指定token + * @return token剩余有效时间 + */ + public static long getTokenTimeout(String token) { + return stpLogic.getTokenTimeout(token); + } + + /** + * 获取当前登录账号的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ public static long getSessionTimeout() { return stpLogic.getSessionTimeout(); } - /** - * 获取当前token的专属Session剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ + /** + * 获取当前 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ public static long getTokenSessionTimeout() { return stpLogic.getTokenSessionTimeout(); } - - /** - * 获取当前token[临时过期]剩余有效时间 (单位: 秒) - * @return token[临时过期]剩余有效时间 - */ - public static long getTokenActivityTimeout() { - return stpLogic.getTokenActivityTimeout(); + + /** + * 获取当前 token 剩余活跃有效期:当前 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) + * + * @return / + */ + public static long getTokenActiveTimeout() { + return stpLogic.getTokenActiveTimeout(); } - - - // =================== 角色验证操作 =================== - - /** - * 指定账号id是否含有角色标识, 返回true或false - * @param loginId 账号id - * @param role 角色标识 - * @return 是否含有指定角色标识 - */ - public static boolean hasRole(Object loginId, String role) { - return stpLogic.hasRole(loginId, role); + /** + * 重置当前 token 的 timeout 有效期 + * + * @param timeout 要修改成为的有效时间 (单位: 秒) + */ + public static void renewTimeout(long timeout) { + stpLogic.renewTimeout(timeout); } + + /** + * 重置指定 token 的 timeout 有效期 + * + * @param tokenValue 指定 token + * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) + */ + public static void renewTimeout(String tokenValue, long timeout) { + stpLogic.renewTimeout(tokenValue, timeout); + } + - /** - * 当前账号是否含有指定角色标识, 返回true或false - * @param role 角色标识 - * @return 是否含有指定角色标识 - */ + // ------------------- 角色认证操作 ------------------- + + /** + * 获取:当前账号的角色集合 + * + * @return / + */ + public static List getRoleList() { + return stpLogic.getRoleList(); + } + + /** + * 获取:指定账号的角色集合 + * + * @param loginId 指定账号id + * @return / + */ + public static List getRoleList(Object loginId) { + return stpLogic.getRoleList(loginId); + } + + /** + * 判断:当前账号是否拥有指定角色, 返回 true 或 false + * + * @param role 角色 + * @return / + */ public static boolean hasRole(String role) { return stpLogic.hasRole(role); } - - /** - * 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException - * @param role 角色标识 - */ + + /** + * 判断:指定账号是否含有指定角色标识, 返回 true 或 false + * + * @param loginId 账号id + * @param role 角色标识 + * @return 是否含有指定角色标识 + */ + public static boolean hasRole(Object loginId, String role) { + return stpLogic.hasRole(loginId, role); + } + + /** + * 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] + * + * @param roleArray 角色标识数组 + * @return true或false + */ + public static boolean hasRoleAnd(String... roleArray){ + return stpLogic.hasRoleAnd(roleArray); + } + + /** + * 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] + * + * @param roleArray 角色标识数组 + * @return true或false + */ + public static boolean hasRoleOr(String... roleArray){ + return stpLogic.hasRoleOr(roleArray); + } + + /** + * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException + * + * @param role 角色标识 + */ public static void checkRole(String role) { stpLogic.checkRole(role); } - /** - * 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] - * @param roleArray 角色标识数组 - */ + /** + * 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] + * + * @param roleArray 角色标识数组 + */ public static void checkRoleAnd(String... roleArray){ stpLogic.checkRoleAnd(roleArray); } - /** - * 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] - * @param roleArray 角色标识数组 - */ + /** + * 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] + * + * @param roleArray 角色标识数组 + */ public static void checkRoleOr(String... roleArray){ stpLogic.checkRoleOr(roleArray); } - + - // =================== 权限验证操作 =================== - - /** - * 指定账号id是否含有指定权限, 返回true或false - * @param loginId 账号id - * @param permission 权限码 - * @return 是否含有指定权限 - */ - public static boolean hasPermission(Object loginId, String permission) { - return stpLogic.hasPermission(loginId, permission); + // ------------------- 权限认证操作 ------------------- + + /** + * 获取:当前账号的权限码集合 + * + * @return / + */ + public static List getPermissionList() { + return stpLogic.getPermissionList(); + } + + /** + * 获取:指定账号的权限码集合 + * + * @param loginId 指定账号id + * @return / + */ + public static List getPermissionList(Object loginId) { + return stpLogic.getPermissionList(loginId); } - /** - * 当前账号是否含有指定权限, 返回true或false - * @param permission 权限码 - * @return 是否含有指定权限 - */ + /** + * 判断:当前账号是否含有指定权限, 返回 true 或 false + * + * @param permission 权限码 + * @return 是否含有指定权限 + */ public static boolean hasPermission(String permission) { return stpLogic.hasPermission(permission); } - /** - * 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException - * @param permission 权限码 - */ + /** + * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false + * + * @param loginId 账号 id + * @param permission 权限码 + * @return 是否含有指定权限 + */ + public static boolean hasPermission(Object loginId, String permission) { + return stpLogic.hasPermission(loginId, permission); + } + + /** + * 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ] + * + * @param permissionArray 权限码数组 + * @return true 或 false + */ + public static boolean hasPermissionAnd(String... permissionArray){ + return stpLogic.hasPermissionAnd(permissionArray); + } + + /** + * 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] + * + * @param permissionArray 权限码数组 + * @return true 或 false + */ + public static boolean hasPermissionOr(String... permissionArray){ + return stpLogic.hasPermissionOr(permissionArray); + } + + /** + * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param permission 权限码 + */ public static void checkPermission(String permission) { stpLogic.checkPermission(permission); } - /** - * 当前账号是否含有指定权限 [指定多个,必须全部验证通过] - * @param permissionArray 权限码数组 - */ + /** + * 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ] + * + * @param permissionArray 权限码数组 + */ public static void checkPermissionAnd(String... permissionArray) { stpLogic.checkPermissionAnd(permissionArray); } - /** - * 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] - * @param permissionArray 权限码数组 - */ + /** + * 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] + * + * @param permissionArray 权限码数组 + */ public static void checkPermissionOr(String... permissionArray) { stpLogic.checkPermissionOr(permissionArray); } - // =================== id 反查token 相关操作 =================== - - /** - * 获取指定账号id的tokenValue - *

在配置为允许并发登录时,此方法只会返回队列的最后一个token, - * 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId + // ------------------- id 反查 token 相关操作 ------------------- + + /** + * 获取指定账号 id 的 token + *

+ * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + *

+ * * @param loginId 账号id - * @return token值 + * @return token值 */ public static String getTokenValueByLoginId(Object loginId) { return stpLogic.getTokenValueByLoginId(loginId); } - /** - * 获取指定账号id指定设备端的tokenValue - *

在配置为允许并发登录时,此方法只会返回队列的最后一个token, - * 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId + /** + * 获取指定账号 id 指定设备类型端的 token + *

+ * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + *

+ * * @param loginId 账号id - * @param device 设备标识 - * @return token值 + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return token值 */ - public static String getTokenValueByLoginId(Object loginId, String device) { - return stpLogic.getTokenValueByLoginId(loginId, device); + public static String getTokenValueByLoginId(Object loginId, String deviceType) { + return stpLogic.getTokenValueByLoginId(loginId, deviceType); } - - /** - * 获取指定账号id的tokenValue集合 - * @param loginId 账号id - * @return 此loginId的所有相关token - */ + + /** + * 获取指定账号 id 的 token 集合 + * + * @param loginId 账号id + * @return 此 loginId 的所有相关 token + */ public static List getTokenValueListByLoginId(Object loginId) { return stpLogic.getTokenValueListByLoginId(loginId); } - /** - * 获取指定账号id指定设备端的tokenValue 集合 - * @param loginId 账号id - * @param device 设备标识 - * @return 此loginId的所有相关token - */ - public static List getTokenValueListByLoginId(Object loginId, String device) { - return stpLogic.getTokenValueListByLoginId(loginId, device); + /** + * 获取指定账号 id 指定设备类型端的 token 集合 + * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return 此 loginId 的所有登录 token + */ + public static List getTokenValueListByLoginId(Object loginId, String deviceType) { + return stpLogic.getTokenValueListByLoginId(loginId, deviceType); } - + /** - * 返回当前会话的登录设备 - * @return 当前令牌的登录设备 + * 获取指定账号 id 已登录设备信息集合 + * + * @param loginId 账号id + * @return 此 loginId 的所有登录 token */ - public static String getLoginDevice() { - return stpLogic.getLoginDevice(); + public static List getTerminalListByLoginId(Object loginId) { + return stpLogic.getTerminalListByLoginId(loginId); } - - // =================== 会话管理 =================== + /** + * 获取指定账号 id 指定设备类型端的已登录设备信息集合 + * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return / + */ + public static List getTerminalListByLoginId(Object loginId, String deviceType) { + return stpLogic.getTerminalListByLoginId(loginId, deviceType); + } /** - * 根据条件查询Token - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return token集合 + * 获取指定账号 id 已登录设备信息集合,执行特定函数 + * + * @param loginId 账号id + * @param function 需要执行的函数 */ - public static List searchTokenValue(String keyword, int start, int size) { - return stpLogic.searchTokenValue(keyword, start, size); + public static void forEachTerminalList(Object loginId, SaTwoParamFunction function) { + stpLogic.forEachTerminalList(loginId, function); } - + + /** + * 返回当前 token 指向的 SaTerminalInfo 设备信息,如果 token 无效则返回 null + * + * @return / + */ + public static SaTerminalInfo getTerminalInfo() { + return stpLogic.getTerminalInfo(); + } + + /** + * 返回指定 token 指向的 SaTerminalInfo 设备信息,如果 Token 无效则返回 null + * + * @param tokenValue 指定 token + * @return / + */ + public static SaTerminalInfo getTerminalInfoByToken(String tokenValue) { + return stpLogic.getTerminalInfoByToken(tokenValue); + } + + /** + * 返回当前会话的登录设备类型 + * + * @return 当前令牌的登录设备类型 + */ + public static String getLoginDeviceType() { + return stpLogic.getLoginDeviceType(); + } + + /** + * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 + */ + public static String getLoginDeviceTypeByToken(String tokenValue) { + return stpLogic.getLoginDeviceTypeByToken(tokenValue); + } + + /** + * 返回当前会话的登录设备 ID + * + * @return / + */ + public static String getLoginDeviceId() { + return stpLogic.getLoginDeviceId(); + } + + /** + * 返回指定 token 会话的登录设备 ID + * + * @param tokenValue 指定token + * @return / + */ + public static String getLoginDeviceIdByToken(String tokenValue) { + return stpLogic.getLoginDeviceIdByToken(tokenValue); + } + + /** + * 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 + * @param deviceId / + * @return / + */ + public static boolean isTrustDeviceId(Object userId, String deviceId) { + return stpLogic.isTrustDeviceId(userId, deviceId); + } + + + + // ------------------- 会话管理 ------------------- + + /** + * 根据条件查询缓存中所有的 token + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return token集合 + */ + public static List searchTokenValue(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchTokenValue(keyword, start, size, sortType); + } + /** - * 根据条件查询SessionId - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return sessionId集合 + * 根据条件查询缓存中所有的 SessionId + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return sessionId集合 */ - public static List searchSessionId(String keyword, int start, int size) { - return stpLogic.searchSessionId(keyword, start, size); + public static List searchSessionId(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchSessionId(keyword, start, size, sortType); } /** - * 根据条件查询Token专属Session的Id - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return sessionId集合 + * 根据条件查询缓存中所有的 Token-Session-Id + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return sessionId集合 */ - public static List searchTokenSessionId(String keyword, int start, int size) { - return stpLogic.searchTokenSessionId(keyword, start, size); + public static List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchTokenSessionId(keyword, start, size, sortType); } // ------------------- 账号封禁 ------------------- /** - * 封禁指定账号 - *

此方法不会直接将此账号id踢下线,而是在对方再次登录时抛出`DisableLoginException`异常 + * 封禁:指定账号 + *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) + * * @param loginId 指定账号id - * @param disableTime 封禁时间, 单位: 秒 (-1=永久封禁) + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ - public static void disable(Object loginId, long disableTime) { - stpLogic.disable(loginId, disableTime); + public static void disable(Object loginId, long time) { + stpLogic.disable(loginId, time); } - + /** - * 指定账号是否已被封禁 (true=已被封禁, false=未被封禁) + * 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) + * * @param loginId 账号id - * @return see note + * @return / */ public static boolean isDisable(Object loginId) { return stpLogic.isDisable(loginId); } - + /** - * 获取指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * 校验:指定账号是否已被封禁,如果被封禁则抛出异常 + * * @param loginId 账号id - * @return see note + */ + public static void checkDisable(Object loginId) { + stpLogic.checkDisable(loginId); + } + + /** + * 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * + * @param loginId 账号id + * @return / */ public static long getDisableTime(Object loginId) { return stpLogic.getDisableTime(loginId); } /** - * 解封指定账号 - * @param loginId 账号id + * 解封:指定账号 + * + * @param loginId 账号id */ public static void untieDisable(Object loginId) { stpLogic.untieDisable(loginId); } + + + // ------------------- 分类封禁 ------------------- + + /** + * 封禁:指定账号的指定服务 + *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) + * + * @param loginId 指定账号id + * @param service 指定服务 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disable(Object loginId, String service, long time) { + stpLogic.disable(loginId, service, time); + } + + /** + * 判断:指定账号的指定服务 是否已被封禁(true=已被封禁, false=未被封禁) + * + * @param loginId 账号id + * @param service 指定服务 + * @return / + */ + public static boolean isDisable(Object loginId, String service) { + return stpLogic.isDisable(loginId, service); + } + + /** + * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 + * + * @param loginId 账号id + * @param services 指定服务,可以指定多个 + */ + public static void checkDisable(Object loginId, String... services) { + stpLogic.checkDisable(loginId, services); + } + + /** + * 获取:指定账号 指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * + * @param loginId 账号id + * @param service 指定服务 + * @return see note + */ + public static long getDisableTime(Object loginId, String service) { + return stpLogic.getDisableTime(loginId, service); + } + + /** + * 解封:指定账号、指定服务 + * + * @param loginId 账号id + * @param services 指定服务,可以指定多个 + */ + public static void untieDisable(Object loginId, String... services) { + stpLogic.untieDisable(loginId, services); + } + + + // ------------------- 阶梯封禁 ------------------- + + /** + * 封禁:指定账号,并指定封禁等级 + * + * @param loginId 指定账号id + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disableLevel(Object loginId, int level, long time) { + stpLogic.disableLevel(loginId, level, time); + } + + /** + * 封禁:指定账号的指定服务,并指定封禁等级 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disableLevel(Object loginId, String service, int level, long time) { + stpLogic.disableLevel(loginId, service, level, time); + } + + /** + * 判断:指定账号是否已被封禁到指定等级 + * + * @param loginId 指定账号id + * @param level 指定封禁等级 + * @return / + */ + public static boolean isDisableLevel(Object loginId, int level) { + return stpLogic.isDisableLevel(loginId, level); + } + + /** + * 判断:指定账号的指定服务,是否已被封禁到指定等级 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 指定封禁等级 + * @return / + */ + public static boolean isDisableLevel(Object loginId, String service, int level) { + return stpLogic.isDisableLevel(loginId, service, level); + } + + /** + * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) + * + * @param loginId 指定账号id + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + */ + public static void checkDisableLevel(Object loginId, int level) { + stpLogic.checkDisableLevel(loginId, level); + } + + /** + * 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常) + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + */ + public static void checkDisableLevel(Object loginId, String service, int level) { + stpLogic.checkDisableLevel(loginId, service, level); + } + + /** + * 获取:指定账号被封禁的等级,如果未被封禁则返回-2 + * + * @param loginId 指定账号id + * @return / + */ + public static int getDisableLevel(Object loginId) { + return stpLogic.getDisableLevel(loginId); + } + + /** + * 获取:指定账号的 指定服务 被封禁的等级,如果未被封禁则返回-2 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @return / + */ + public static int getDisableLevel(Object loginId, String service) { + return stpLogic.getDisableLevel(loginId, service); + } - // =================== 身份切换 =================== + // ------------------- 临时身份切换 ------------------- /** - * 临时切换身份为指定账号id + * 临时切换身份为指定账号id + * * @param loginId 指定loginId */ public static void switchTo(Object loginId) { stpLogic.switchTo(loginId); } - + /** * 结束临时切换身份 */ @@ -575,15 +1351,17 @@ public static void endSwitch() { } /** - * 当前是否正处于[身份临时切换]中 - * @return 是否正处于[身份临时切换]中 + * 判断当前请求是否正处于 [ 身份临时切换 ] 中 + * + * @return / */ public static boolean isSwitch() { return stpLogic.isSwitch(); } /** - * 在一个代码段里方法内,临时切换身份为指定账号id + * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 + * * @param loginId 指定账号id * @param function 要执行的方法 */ @@ -593,9 +1371,10 @@ public static void switchTo(Object loginId, SaFunction function) { // ------------------- 二级认证 ------------------- - + /** - * 在当前会话 开启二级认证 + * 在当前会话 开启二级认证 + * * @param safeTime 维持时间 (单位: 秒) */ public static void openSafe(long safeTime) { @@ -603,7 +1382,18 @@ public static void openSafe(long safeTime) { } /** - * 当前会话 是否处于二级认证时间内 + * 在当前会话 开启二级认证 + * + * @param service 业务标识 + * @param safeTime 维持时间 (单位: 秒) + */ + public static void openSafe(String service, long safeTime) { + stpLogic.openSafe(service, safeTime); + } + + /** + * 判断:当前会话是否处于二级认证时间内 + * * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe() { @@ -611,14 +1401,45 @@ public static boolean isSafe() { } /** - * 检查当前会话是否已通过二级认证,如未通过则抛出异常 + * 判断:当前会话 是否处于指定业务的二级认证时间内 + * + * @param service 业务标识 + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public static boolean isSafe(String service) { + return stpLogic.isSafe(service); + } + + /** + * 判断:指定 token 是否处于二级认证时间内 + * + * @param tokenValue Token 值 + * @param service 业务标识 + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public static boolean isSafe(String tokenValue, String service) { + return stpLogic.isSafe(tokenValue, service); + } + + /** + * 校验:当前会话是否已通过二级认证,如未通过则抛出异常 */ public static void checkSafe() { stpLogic.checkSafe(); } - + + /** + * 校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常 + * + * @param service 业务标识 + */ + public static void checkSafe(String service) { + stpLogic.checkSafe(service); + } + /** - * 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) + * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) + * * @return 剩余有效时间 */ public static long getSafeTime() { @@ -626,66 +1447,67 @@ public static long getSafeTime() { } /** - * 在当前会话 结束二级认证 + * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) + * + * @param service 业务标识 + * @return 剩余有效时间 */ - public static void closeSafe() { - stpLogic.closeSafe(); + public static long getSafeTime(String service) { + return stpLogic.getSafeTime(service); } - - // =================== 历史API,兼容旧版本 =================== - /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.getLoginType() ,使用方式保持不变

- * 获取当前StpLogin的loginKey - * @return 当前StpLogin的loginKey + * 在当前会话 结束二级认证 */ - @Deprecated - public static String getLoginKey(){ - return stpLogic.getLoginType(); + public static void closeSafe() { + stpLogic.closeSafe(); } /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.login() ,使用方式保持不变

- * 在当前会话上登录id - * @param loginId 登录id,建议的类型:(long | int | String) + * 在当前会话 结束指定业务标识的二级认证 + * + * @param service 业务标识 */ - @Deprecated - public static void setLoginId(Object loginId) { - stpLogic.login(loginId); + public static void closeSafe(String service) { + stpLogic.closeSafe(service); } + + // ------------------- Bean 对象、字段代理 ------------------- + /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.login() ,使用方式保持不变

- * 在当前会话上登录id, 并指定登录设备 - * @param loginId 登录id,建议的类型:(long | int | String) - * @param device 设备标识 + * 根据当前配置对象创建一个 SaLoginParameter 对象 + * + * @return / */ - @Deprecated - public static void setLoginId(Object loginId, String device) { - stpLogic.login(loginId, device); + public static SaLoginParameter createSaLoginParameter() { + return stpLogic.createSaLoginParameter(); } + + // ------------------- 过期方法 ------------------- + /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.login() ,使用方式保持不变

- * 在当前会话上登录id, 并指定登录设备 - * @param loginId 登录id,建议的类型:(long | int | String) - * @param isLastingCookie 是否为持久Cookie + *

请更换为 getLoginDeviceType

+ * 返回当前会话的登录设备类型 + * + * @return 当前令牌的登录设备类型 */ @Deprecated - public static void setLoginId(Object loginId, boolean isLastingCookie) { - stpLogic.login(loginId, isLastingCookie); + public static String getLoginDevice() { + return stpLogic.getLoginDevice(); } - + /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.login() ,使用方式保持不变

- * 在当前会话上登录id, 并指定所有登录参数Model - * @param loginId 登录id,建议的类型:(long | int | String) - * @param loginModel 此次登录的参数Model + *

请更换为 getLoginDeviceTypeByToken

+ * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 */ @Deprecated - public static void setLoginId(Object loginId, SaLoginModel loginModel) { - stpLogic.login(loginId, loginModel); + public static String getLoginDeviceByToken(String tokenValue) { + return stpLogic.getLoginDeviceByToken(tokenValue); } - + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLoginParameter.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLoginParameter.java new file mode 100644 index 000000000..cbb14f7b3 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLoginParameter.java @@ -0,0 +1,647 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp.parameter; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaCookieConfig; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; +import cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode; +import cn.dev33.satoken.stp.parameter.enums.SaReplacedRange; +import cn.dev33.satoken.util.SaTokenConsts; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 在调用 `StpUtil.login()` 时的 配置参数对象,决定登录的一些细节行为
+ * + *
+ *     	// 例如:在登录时指定 token 有效期为七天,代码如下:
+ *     	StpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7));
+ * 
+ * + * @author click33 + * @since 1.13.2 + */ +public class SaLoginParameter { + + // --------- 单独参数 + + /** + * 此次登录的客户端设备类型 + */ + private String deviceType; + + /** + * 此次登录的客户端设备id + */ + private String deviceId; + + /** + * 扩展信息(只在 jwt 模式下生效) + */ + private Map extraData; + + /** + * 预定Token(预定本次登录生成的Token值) + */ + private String token; + + /** + * 本次登录挂载到 SaTerminalInfo 的自定义扩展数据 + */ + private Map terminalExtraData; + + + // --------- 覆盖性参数 + + /** + * 指定此次登录 token 有效期,单位:秒 (如未指定,自动取全局配置的 timeout 值) + */ + private long timeout; + + /** + * 指定此次登录 token 最低活跃频率,单位:秒(如未指定,则使用全局配置的 activeTimeout 值) + */ + private Long activeTimeout; + + /** + * 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + */ + private Boolean isConcurrent; + + /** + * 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + */ + private Boolean isShare; + + /** + * 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + */ + private int maxLoginCount; + + /** + * 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) + */ + private int maxTryTimes; + + /** + * 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + */ + private Boolean isLastingCookie; + + /** + * 是否在登录后将 Token 写入到响应头 + */ + private Boolean isWriteHeader; + + /** + * 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 + */ + private SaReplacedLoginExitMode replacedLoginExitMode; + + /** + * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) + */ + private SaReplacedRange replacedRange; + + /** + * 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + */ + private SaLogoutMode overflowLogoutMode; + + /** + * 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) + */ + private Boolean rightNowCreateTokenSession; + + /** + * Cookie 配置对象 + */ + public SaCookieConfig cookie = new SaCookieConfig(); + + + // ------ 附加方法 + + public SaLoginParameter() { + this(SaManager.getConfig()); + } + public SaLoginParameter(SaTokenConfig config) { + setDefaultValues(config); + } + + /** + * 根据 SaTokenConfig 对象初始化默认值 + * + * @param config 使用的配置对象 + * @return 对象自身 + */ + public SaLoginParameter setDefaultValues(SaTokenConfig config) { + this.deviceType = SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE; + this.timeout = config.getTimeout(); + this.isConcurrent = config.getIsConcurrent(); + this.isShare = config.getIsShare(); + this.maxLoginCount = config.getMaxLoginCount(); + this.maxTryTimes = config.getMaxTryTimes(); + this.isLastingCookie = config.getIsLastingCookie(); + this.isWriteHeader = config.getIsWriteHeader(); + this.replacedRange = config.getReplacedRange(); + this.overflowLogoutMode = config.getOverflowLogoutMode(); + this.rightNowCreateTokenSession = config.getRightNowCreateTokenSession(); + this.replacedLoginExitMode = config.getReplacedLoginExitMode(); + + this.setupCookieConfig(cookie -> { + SaCookieConfig gCookie = config.getCookie(); + cookie.setDomain(gCookie.getDomain()); + cookie.setPath(gCookie.getPath()); + cookie.setSecure(gCookie.getSecure()); + cookie.setHttpOnly(gCookie.getHttpOnly()); + cookie.setSameSite(gCookie.getSameSite()); + cookie.setExtraAttrs(new LinkedHashMap<>(gCookie.getExtraAttrs())); + }); + + return this; + } + + /** + * 写入扩展数据(只在jwt模式下生效) + * @param key 键 + * @param value 值 + * @return 对象自身 + */ + public SaLoginParameter setExtra(String key, Object value) { + if(this.extraData == null) { + this.extraData = new LinkedHashMap<>(); + } + this.extraData.put(key, value); + return this; + } + + /** + * 获取扩展数据(只在jwt模式下生效) + * @param key 键 + * @return 扩展数据的值 + */ + public Object getExtra(String key) { + if(this.extraData == null) { + return null; + } + return this.extraData.get(key); + } + + /** + * 判断是否设置了扩展数据(只在jwt模式下生效) + * @return / + */ + public boolean haveExtraData() { + return extraData != null && !extraData.isEmpty(); + } + + /** + * 写入本次登录挂载到 SaTerminalInfo 的自定义扩展数据 + * @param key 键 + * @param value 值 + * @return 对象自身 + */ + public SaLoginParameter setTerminalExtra(String key, Object value) { + if(this.terminalExtraData == null) { + this.terminalExtraData = new LinkedHashMap<>(); + } + this.terminalExtraData.put(key, value); + return this; + } + + /** + * 获取本次登录挂载到 SaTerminalInfo 的自定义扩展数据 + * @param key 键 + * @return 扩展数据的值 + */ + public Object getTerminalExtra(String key) { + if(this.terminalExtraData == null) { + return null; + } + return this.terminalExtraData.get(key); + } + + /** + * 判断是否设置了本次登录挂载到 SaTerminalInfo 的自定义扩展数据 + * @return / + */ + public boolean haveTerminalExtraData() { + return terminalExtraData != null && !terminalExtraData.isEmpty(); + } + + /** + * 计算 Cookie 时长 + * @return / + */ + public int getCookieTimeout() { + if( ! getIsLastingCookie()) { + return -1; + } + long _timeout = getTimeout(); + if(_timeout == SaTokenDao.NEVER_EXPIRE || _timeout > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int)_timeout; + } + + /** + * 静态方法获取一个 SaLoginParameter 对象 + * @return SaLoginParameter 对象 + */ + public static SaLoginParameter create() { + return new SaLoginParameter(SaManager.getConfig()); + } + + /** + * 设置 Cookie 配置项 + * @param fun / + * @return 对象自身 + */ + public SaLoginParameter setupCookieConfig(SaParamFunction fun) { + fun.run(this.cookie); + return this; + } + + + + // ---------------- get set + + /** + * @return 此次登录的客户端设备类型 + */ + public String getDeviceType() { + return deviceType; + } + + /** + * @param deviceType 此次登录的客户端设备类型 + * @return 对象自身 + */ + public SaLoginParameter setDeviceType(String deviceType) { + this.deviceType = deviceType; + return this; + } + + /** + * 获取 此次登录的客户端设备id + * + * @return deviceId 此次登录的客户端设备id + */ + public String getDeviceId() { + return this.deviceId; + } + + /** + * 设置 此次登录的客户端设备id + * + * @param deviceId 此次登录的客户端设备id + */ + public SaLoginParameter setDeviceId(String deviceId) { + this.deviceId = deviceId; + return this; + } + + /** + * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) + * + * @return replacedMode 顶人下线的范围 + */ + public SaReplacedRange getReplacedRange() { + return this.replacedRange; + } + + /** + * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) + * + * @param replacedRange / + * @return 对象自身 + */ + public SaLoginParameter setReplacedRange(SaReplacedRange replacedRange) { + this.replacedRange = replacedRange; + return this; + } + + /** + * 获取 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + * + * @return overflowLogoutMode / + */ + public SaLogoutMode getOverflowLogoutMode() { + return this.overflowLogoutMode; + } + + /** + * 设置 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + * + * @param overflowLogoutMode / + * @return 对象自身 + */ + public SaLoginParameter setOverflowLogoutMode(SaLogoutMode overflowLogoutMode) { + this.overflowLogoutMode = overflowLogoutMode; + return this; + } + + /** + * @return 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + */ + public Boolean getIsLastingCookie() { + return isLastingCookie; + } + + /** + * @param isLastingCookie 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + * @return 对象自身 + */ + public SaLoginParameter setIsLastingCookie(Boolean isLastingCookie) { + this.isLastingCookie = isLastingCookie; + return this; + } + + /** + * @return 指定此次登录 token 有效期,单位:秒 + */ + public long getTimeout() { + return timeout; + } + + /** + * @param timeout 指定此次登录 token 有效期,单位:秒 (如未指定,自动取全局配置的 timeout 值) + * @return 对象自身 + */ + public SaLoginParameter setTimeout(long timeout) { + this.timeout = timeout; + return this; + } + + /** + * @return 此次登录 token 最低活跃频率,单位:秒(如未指定,则使用全局配置的 activeTimeout 值) + */ + public Long getActiveTimeout() { + return activeTimeout; + } + + /** + * @param activeTimeout 指定此次登录 token 最低活跃频率,单位:秒(如未指定,则使用全局配置的 activeTimeout 值) + * @return 对象自身 + */ + public SaLoginParameter setActiveTimeout(long activeTimeout) { + this.activeTimeout = activeTimeout; + return this; + } + + /** + * @return 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + */ + public Boolean getIsConcurrent() { + return isConcurrent; + } + + /** + * @param isConcurrent 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + * @return 对象自身 + */ + public SaLoginParameter setIsConcurrent(Boolean isConcurrent) { + this.isConcurrent = isConcurrent; + return this; + } + + /** + * @return 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) + */ + public Boolean getIsShare() { + return isShare; + } + + /** + * @param isShare 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) + * @return 对象自身 + */ + public SaLoginParameter setIsShare(Boolean isShare) { + this.isShare = isShare; + return this; + } + + /** + * @return 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + */ + public int getMaxLoginCount() { + return maxLoginCount; + } + + /** + * @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + * @return 对象自身 + */ + public SaLoginParameter setMaxLoginCount(int maxLoginCount) { + this.maxLoginCount = maxLoginCount; + return this; + } + + /** + * @return 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) + */ + public int getMaxTryTimes() { + return maxTryTimes; + } + + /** + * @param maxTryTimes 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) + * @return 对象自身 + */ + public SaLoginParameter setMaxTryTimes(int maxTryTimes) { + this.maxTryTimes = maxTryTimes; + return this; + } + + /** + * @return 扩展信息(只在jwt模式下生效) + */ + public Map getExtraData() { + return extraData; + } + + /** + * @param extraData 扩展信息(只在jwt模式下生效) + * @return 对象自身 + */ + public SaLoginParameter setExtraData(Map extraData) { + this.extraData = extraData; + return this; + } + + /** + * @return 预定Token(预定本次登录生成的Token值) + */ + public String getToken() { + return token; + } + + /** + * @param token 预定Token(预定本次登录生成的Token值) + * @return 对象自身 + */ + public SaLoginParameter setToken(String token) { + this.token = token; + return this; + } + + /** + * @return 是否在登录后将 Token 写入到响应头 + */ + public Boolean getIsWriteHeader() { + return isWriteHeader; + } + + /** + * @param isWriteHeader 是否在登录后将 Token 写入到响应头 + * @return 对象自身 + */ + public SaLoginParameter setIsWriteHeader(Boolean isWriteHeader) { + this.isWriteHeader = isWriteHeader; + return this; + } + + /** + * 获取 本次登录挂载到 SaTerminalInfo 的自定义扩展数据 + * + * @return / + */ + public Map getTerminalExtraData() { + return this.terminalExtraData; + } + + /** + * 设置 本次登录挂载到 SaTerminalInfo 的自定义扩展数据 + * + * @param terminalExtraData / + * @return 对象自身 + */ + public SaLoginParameter setTerminalExtraData(Map terminalExtraData) { + this.terminalExtraData = terminalExtraData; + return this; + } + + /** + * 获取 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) + * + * @return / + */ + public Boolean getRightNowCreateTokenSession() { + return this.rightNowCreateTokenSession; + } + + /** + * 设置 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) + * + * @param rightNowCreateTokenSession / + * @return 对象自身 + */ + public SaLoginParameter setRightNowCreateTokenSession(Boolean rightNowCreateTokenSession) { + this.rightNowCreateTokenSession = rightNowCreateTokenSession; + return this; + } + + /** + * @return Cookie 配置对象 + */ + public SaCookieConfig getCookie() { + return cookie; + } + + /** + * @param cookie Cookie 配置对象 + * @return 对象自身 + */ + public SaLoginParameter setCookie(SaCookieConfig cookie) { + this.cookie = cookie; + return this; + } + + + /** + * 获取:在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) + * @return / + */ + public SaReplacedLoginExitMode getReplacedLoginExitMode() { + return replacedLoginExitMode; + } + + /** + * 设置:在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) + * @param replacedLoginExitMode / + * @return 对象自身 + */ + public SaLoginParameter setReplacedLoginExitMode(SaReplacedLoginExitMode replacedLoginExitMode) { + this.replacedLoginExitMode = replacedLoginExitMode; + return this; + } + + /* + * toString + */ + @Override + public String toString() { + return "SaLoginParameter [" + + "deviceType=" + deviceType + + ", deviceId=" + deviceId + + ", replacedRange=" + replacedRange + + ", replacedLoginExitMode=" + replacedLoginExitMode + + ", overflowLogoutMode=" + overflowLogoutMode + + ", isLastingCookie=" + isLastingCookie + + ", timeout=" + timeout + + ", activeTimeout=" + activeTimeout + + ", isConcurrent=" + isConcurrent + + ", isShare=" + isShare + + ", maxLoginCount=" + maxLoginCount + + ", maxTryTimes=" + maxTryTimes + + ", extraData=" + extraData + + ", token=" + token + + ", isWriteHeader=" + isWriteHeader + + ", terminalTag=" + terminalExtraData + + ", rightNowCreateTokenSession=" + rightNowCreateTokenSession + + ", cookie=" + cookie + + "]"; + } + + + + + /** + *

请更换为 getDeviceType

+ * @return 此次登录的客户端设备类型 + */ + @Deprecated + public String getDevice() { + return deviceType; + } + + /** + *

请更换为 setDeviceType

+ * @param device 此次登录的客户端设备类型 + * @return 对象自身 + */ + @Deprecated + public SaLoginParameter setDevice(String device) { + this.deviceType = device; + return this; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLogoutParameter.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLogoutParameter.java new file mode 100644 index 000000000..1261f511d --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLogoutParameter.java @@ -0,0 +1,250 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp.parameter; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutRange; + +/** + * 在会话注销时的 配置参数对象,决定注销时的一些细节行为
+ * + *
+ *     	// 例如:
+ *     	StpUtil.logout(10001, new SaLogoutParameter());
+ * 
+ * + * @author click33 + * @since 1.41.0 + */ +public class SaLogoutParameter { + + // --------- 单独参数 + + /** + * 需要注销的设备类型 (为 null 代表不限制,为具体值代表只注销此设备类型的会话) + *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) + */ + private String deviceType; + + /** + * 需要注销的设备ID (为 null 代表不限制,为具体值代表只注销此设备ID的会话) + *
(此参数只在调用 StpUtil.logout(id, param) 时有效) + */ + private String deviceId; + + /** + * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线,REPLACED=顶人下线) + */ + private SaLogoutMode mode = SaLogoutMode.LOGOUT; + + + // --------- 覆盖性参数 + + /** + * 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) + *
(此参数只在调用 StpUtil.logout(param) 时有效) + */ + private SaLogoutRange range; + + /** + * 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) + *
(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token", param) 时有效) + */ + private Boolean isKeepFreezeOps; + + /** + * 在注销 token 后,是否保留其对应的 Token-Session + */ + private Boolean isKeepTokenSession; + + + // ------ 附加方法 + + public SaLogoutParameter() { + this(SaManager.getConfig()); + } + public SaLogoutParameter(SaTokenConfig config) { + setDefaultValues(config); + } + + /** + * 根据 SaTokenConfig 对象初始化默认值 + * + * @param config 使用的配置对象 + * @return 对象自身 + */ + public SaLogoutParameter setDefaultValues(SaTokenConfig config) { + this.range = config.getLogoutRange(); + this.isKeepFreezeOps = config.getIsLogoutKeepFreezeOps(); + this.isKeepTokenSession = config.getIsLogoutKeepTokenSession(); + return this; + } + + /** + * 静态方法获取一个 SaLoginParameter 对象 + * @return SaLoginParameter 对象 + */ + public static SaLogoutParameter create() { + return new SaLogoutParameter(SaManager.getConfig()); + } + + + + // ---------------- get set + + /** + * @return 在注销 token 后,是否保留其对应的 Token-Session + */ + public Boolean getIsKeepTokenSession() { + return isKeepTokenSession; + } + + /** + * @param isKeepTokenSession 在注销 token 后,是否保留其对应的 Token-Session + * + * @return 对象自身 + */ + public SaLogoutParameter setIsKeepTokenSession(Boolean isKeepTokenSession) { + this.isKeepTokenSession = isKeepTokenSession; + return this; + } + + /** + * 获取 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) + *
(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token", param) 时有效) + * + * @return / + */ + public Boolean getIsKeepFreezeOps() { + return this.isKeepFreezeOps; + } + + /** + * 设置 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) + *
(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token", param) 时有效) + * + * @param isKeepFreezeOps / + * @return 对象自身 + */ + public SaLogoutParameter setIsKeepFreezeOps(Boolean isKeepFreezeOps) { + this.isKeepFreezeOps = isKeepFreezeOps; + return this; + } + + /** + * 需要注销的设备类型 (为 null 代表不限制,为具体值代表只注销此设备类型的会话) + *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) + * + * @return deviceType / + */ + public String getDeviceType() { + return this.deviceType; + } + + /** + * 需要注销的设备类型 (为 null 代表不限制,为具体值代表只注销此设备类型的会话) + *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) + * + * @param deviceType / + * @return / + */ + public SaLogoutParameter setDeviceType(String deviceType) { + this.deviceType = deviceType; + return this; + } + + /** + * 需要注销的设备ID (为 null 代表不限制,为具体值代表只注销此设备 ID 的会话) + *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) + * + * @return / + */ + public String getDeviceId() { + return this.deviceId; + } + + /** + * 需要注销的设备类型 (为 null 代表不限制,为具体值代表只注销此设备 ID 的会话) + *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) + * + * @param deviceId / + * @return / + */ + public SaLogoutParameter setDeviceId(String deviceId) { + this.deviceId = deviceId; + return this; + } + + /** + * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线,REPLACED=顶人下线) + * + * @return logoutMode 注销类型 + */ + public SaLogoutMode getMode() { + return this.mode; + } + + /** + * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线,REPLACED=顶人下线) + * + * @param mode 注销类型 + * @return / + */ + public SaLogoutParameter setMode(SaLogoutMode mode) { + this.mode = mode; + return this; + } + + /** + * 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) + *
(此参数只在调用 StpUtil.logout(param) 时有效) + * + * @return / + */ + public SaLogoutRange getRange() { + return this.range; + } + + /** + * 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) + *
(此参数只在调用 StpUtil.logout(param) 时有效) + * + * @param range / + * @return / + */ + public SaLogoutParameter setRange(SaLogoutRange range) { + this.range = range; + return this; + } + + /* + * toString + */ + @Override + public String toString() { + return "SaLoginParameter [" + + "deviceType=" + deviceType + + ", deviceId=" + deviceId + + ", isKeepTokenSession=" + isKeepTokenSession + + ", isKeepFreezeOps=" + isKeepFreezeOps + + ", mode=" + mode + + ", range=" + range + + "]"; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutMode.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutMode.java new file mode 100644 index 000000000..64d33c1aa --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutMode.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp.parameter.enums; + +/** + * SaLogoutMode: 注销模式 + * + * @author click33 + * @since 1.41.0 + */ +public enum SaLogoutMode { + + /** + * 注销下线 + */ + LOGOUT, + + /** + * 踢人下线 + */ + KICKOUT, + + /** + * 顶人下线 + */ + REPLACED; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutRange.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutRange.java new file mode 100644 index 000000000..18286724d --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutRange.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp.parameter.enums; + +/** + * SaLogoutMode: 注销范围 + * + * @author click33 + * @since 1.41.0 + */ +public enum SaLogoutRange { + + /** + * token 范围:只注销提供的 token 指向的会话 + */ + TOKEN, + + /** + * 账号范围:注销 token 指向的 loginId 会话 + */ + ACCOUNT + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedLoginExitMode.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedLoginExitMode.java new file mode 100644 index 000000000..0e33064b7 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedLoginExitMode.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp.parameter.enums; + +/** + * 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 + * @author 石泽旭 + * @since 1.44.0 + */ +public enum SaReplacedLoginExitMode { + + /** + * 旧设备下线,新设备登录成功 + */ + OLD_DEVICE, + + /** + * 新设备登录失败,旧设备维持在线 + */ + NEW_DEVICE + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedRange.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedRange.java new file mode 100644 index 000000000..137739515 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedRange.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.stp.parameter.enums; + +/** + * 顶人下线的范围 + * + * @author click33 + * @since 1.41.0 + */ +public enum SaReplacedRange { + + /** + * 当前指定的设备类型端 + */ + CURR_DEVICE_TYPE, + + /** + * 所有设备类型端 + */ + ALL_DEVICE_TYPE + +} 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 000000000..8126cdfab --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaAnnotationStrategy.java @@ -0,0 +1,159 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.*; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.router.SaRouter; + +import java.lang.annotation.Annotation; +import java.util.*; + +/** + * 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(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] 对象进行注解校验 (注解鉴权内部实现) + */ + public SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> { + + // 如果 Method 或其所属 Class 上有 @SaIgnore 注解,则直接跳过整个校验过程 + if(instance.isAnnotationPresent.apply(method, SaIgnore.class)) { + SaRouter.stop(); + } + + // 先校验 Method 所属 Class 上的注解 + instance.checkElementAnnotation.accept(method.getDeclaringClass()); + + // 再校验 Method 上的注解 + instance.checkElementAnnotation.accept(method); + }; + + /** + * 对一个 [Element] 对象进行注解校验 (注解鉴权内部实现) + */ + @SuppressWarnings("unchecked") + public SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> { + // 如果此元素上标注了 @SaCheckOr,则必须在后续判断中忽略掉其指定的 append() 类型注解判断 + List> ignoreClassList = new ArrayList<>(); + SaCheckOr checkOr = (SaCheckOr)instance.getAnnotation.apply(element, SaCheckOr.class); + if(checkOr != null) { + ignoreClassList = Arrays.asList(checkOr.append()); + } + + // 遍历所有的注解处理器,检查此 element 是否具有这些指定的注解 + for (Map.Entry, SaAnnotationHandlerInterface> entry: annotationHandlerMap.entrySet()) { + // 忽略掉在 @SaCheckOr 中 append 字段指定的注解 + Class atClass = (Class)entry.getKey(); + if(ignoreClassList.contains(atClass)) { + continue; + } + Annotation annotation = instance.getAnnotation.apply(element, atClass); + if(annotation != null) { + entry.getValue().check(annotation, element); + } + } + }; + + /** + * 从元素上获取注解 + */ + public SaGetAnnotationFunction getAnnotation = (element, annotationClass)->{ + 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; + }; + + /** + * SaCheckELRootMap 扩展函数 + */ + public SaCheckELRootMapExtendFunction checkELRootMapExtendFunction = rootMap -> { + // 默认不做任何处理 + }; + + + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaFirewallStrategy.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaFirewallStrategy.java new file mode 100644 index 000000000..ce171ad42 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaFirewallStrategy.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.SaManager; +import cn.dev33.satoken.fun.strategy.SaFirewallCheckFailHandleFunction; +import cn.dev33.satoken.fun.strategy.SaFirewallCheckFunction; +import cn.dev33.satoken.strategy.hooks.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * Sa-Token 防火墙策略 + * + * @author click33 + * @since 1.40.0 + */ +public final class SaFirewallStrategy { + + /** + * 全局单例引用 + */ + public static final SaFirewallStrategy instance = new SaFirewallStrategy(); + + /** + * 防火墙校验钩子函数集合 + */ + public List checkHooks = new ArrayList<>(); + + private SaFirewallStrategy() { + // 初始化默认的防火墙校验钩子函数集合 + checkHooks.add(SaFirewallCheckHookForWhitePath.instance); + checkHooks.add(SaFirewallCheckHookForBlackPath.instance); + checkHooks.add(SaFirewallCheckHookForPathDangerCharacter.instance); + checkHooks.add(SaFirewallCheckHookForPathBannedCharacter.instance); + checkHooks.add(SaFirewallCheckHookForDirectoryTraversal.instance); + checkHooks.add(SaFirewallCheckHookForHost.instance); + checkHooks.add(SaFirewallCheckHookForHttpMethod.instance); + checkHooks.add(SaFirewallCheckHookForHeader.instance); + checkHooks.add(SaFirewallCheckHookForParameter.instance); + } + + /** + * 注册一个防火墙校验 hook + * @param checkHook / + */ + public void registerHook(SaFirewallCheckHook checkHook) { + SaManager.getLog().info("防火墙校验 hook 注册成功: " + checkHook.getClass()); + checkHooks.add(checkHook); + } + + /** + * 注册一个防火墙校验 hook 到第一位, + * 请注意将 hook 注册到第一位将会优先于白名单的判断,如果您依然希望白名单 hook 保持最高优先级,请调用 registerHookToSecond + * @param checkHook / + */ + public void registerHookToFirst(SaFirewallCheckHook checkHook) { + SaManager.getLog().info("防火墙校验 hook 注册成功: " + checkHook.getClass()); + checkHooks.add(0, checkHook); + } + + /** + * 注册一个防火墙校验 hook 到第二位 + * @param checkHook / + */ + public void registerHookToSecond(SaFirewallCheckHook checkHook) { + SaManager.getLog().info("防火墙校验 hook 注册成功: " + checkHook.getClass()); + checkHooks.add(1, checkHook); + } + + /** + * 移除指定类型的防火墙校验 hook + * @param hookClass / + */ + public void removeHook(Class hookClass) { + for (SaFirewallCheckHook hook : checkHooks) { + if (hook.getClass().equals(hookClass)) { + checkHooks.remove(hook); + SaManager.getLog().info("防火墙校验 hook 移除成功: " + hookClass); + return; + } + } + } + + /** + * 防火墙校验函数 + */ + public SaFirewallCheckFunction check = (req, res, extArg) -> { + for (SaFirewallCheckHook checkHook : checkHooks) { + checkHook.execute(req, res, extArg); + } + }; + + /** + * 当请求 path 校验不通过时地处理方案,自定义示例: + *
+	 * 		SaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> {
+	 * 			// 自定义处理逻辑 ...
+	 *      };
+	 * 
+ */ + public SaFirewallCheckFailHandleFunction checkFailHandle = 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 new file mode 100644 index 000000000..65e2066bc --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java @@ -0,0 +1,271 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.SaManager; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.NotImplException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.fun.strategy.*; +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.UUID; + +/** + * Sa-Token 策略对象 + *

+ * 此类统一定义框架内的一些关键性逻辑算法,方便开发者进行按需重写,例: + *

+ *
+ // SaStrategy全局单例,所有方法都用以下形式重写
+ SaStrategy.instance.setCreateToken((loginId, loginType) -》 {
+ // 自定义Token生成的算法
+ return "xxxx";
+ });
+ * 
+ * + * @author click33 + * @since 1.27.0 + */ +public final class SaStrategy { + + private SaStrategy() { + } + + /** + * 获取 SaStrategy 对象的单例引用 + */ + public static final SaStrategy instance = new SaStrategy(); + + + // ----------------------- 所有策略 + + /** + * 创建 Token 的策略 + */ + public SaCreateTokenFunction createToken = (loginId, loginType) -> { + // 根据配置的tokenStyle生成不同风格的token + String tokenStyle = SaManager.getStpLogic(loginType).getConfigOrGlobal().getTokenStyle(); + + switch (tokenStyle) { + // uuid + case SaTokenConsts.TOKEN_STYLE_UUID: + return UUID.randomUUID().toString(); + + // 简单uuid (不带下划线) + case SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID: + return UUID.randomUUID().toString().replaceAll("-", ""); + + // 32位随机字符串 + case SaTokenConsts.TOKEN_STYLE_RANDOM_32: + return SaFoxUtil.getRandomString(32); + + // 64位随机字符串 + case SaTokenConsts.TOKEN_STYLE_RANDOM_64: + return SaFoxUtil.getRandomString(64); + + // 128位随机字符串 + case SaTokenConsts.TOKEN_STYLE_RANDOM_128: + return SaFoxUtil.getRandomString(128); + + // tik风格 (2_14_16) + case SaTokenConsts.TOKEN_STYLE_TIK: + return SaFoxUtil.getRandomString(2) + "_" + SaFoxUtil.getRandomString(14) + "_" + SaFoxUtil.getRandomString(16) + "__"; + + // 默认,还是uuid + default: + SaManager.getLog().warn("配置的 tokenStyle 值无效:{},仅允许以下取值: " + + "uuid、simple-uuid、random-32、random-64、random-128、tik", tokenStyle); + return UUID.randomUUID().toString(); + } + }; + + /** + * 创建 Session 的策略 + */ + public SaCreateSessionFunction createSession = (sessionId) -> { + return new SaSession(sessionId); + }; + + /** + * 反序列化 SaSession 时默认指定的类型 + */ + public volatile Class sessionClassType = SaSession.class; + + /** + * 判断:集合中是否包含指定元素(模糊匹配) + */ + public SaHasElementFunction hasElement = (list, element) -> { + + // 空集合直接返回false + if(list == null || list.size() == 0) { + return false; + } + + // 先尝试一下简单匹配,如果可以匹配成功则无需继续模糊匹配 + if (list.contains(element)) { + return true; + } + + // 开始模糊匹配 + for (String patt : list) { + if(SaFoxUtil.vagueMatch(patt, element)) { + return true; + } + } + + // 走出for循环说明没有一个元素可以匹配成功 + return false; + }; + + /** + * 生成唯一式 token 的算法 + */ + public SaGenerateUniqueTokenFunction generateUniqueToken = (elementName, maxTryTimes, createTokenFunction, checkTokenFunction) -> { + + // 为方便叙述,以下代码注释均假设在处理生成 token 的场景,但实际上本方法也可能被用于生成 code、ticket 等 + + // 循环生成 + for (int i = 1; ; i++) { + // 生成 token + String token = createTokenFunction.get(); + + // 如果 maxTryTimes == -1,表示不做唯一性验证,直接返回 + if (maxTryTimes == -1) { + return token; + } + + // 如果 token 在DB库查询不到数据,说明是个可用的全新 token,直接返回 + if (checkTokenFunction.apply(token)) { + return token; + } + + // 如果已经循环了 maxTryTimes 次,仍然没有创建出可用的 token,那么抛出异常 + if (i >= maxTryTimes) { + throw new SaTokenException(elementName + " 生成失败,已尝试" + i + "次,生成算法过于简单或资源池已耗尽"); + } + } + }; + + /** + * 是否自动续期 active-timeout + */ + public SaAutoRenewFunction autoRenew = (stpLogic) -> { + return stpLogic.getConfigOrGlobal().getAutoRenew(); + }; + + /** + * 创建 StpLogic 的算法 + */ + public SaCreateStpLogicFunction createStpLogic = (loginType) -> { + return new StpLogic(loginType); + }; + + /** + * 路由匹配策略 + */ + public SaRouteMatchFunction routeMatcher = (pattern, path) -> { + throw new NotImplException("未实现具体路由匹配策略").setCode(SaErrorCode.CODE_12401); + }; + + /** + * CORS 策略处理函数 + */ + public SaCorsHandleFunction corsHandle = (req, res, sto) -> { + + }; + + + // ----------------------- 重写策略 set连缀风格 + + /** + * 重写创建 Token 的策略 + * + * @param createToken / + * @return / + */ + public SaStrategy setCreateToken(SaCreateTokenFunction createToken) { + this.createToken = createToken; + return this; + } + + /** + * 重写创建 Session 的策略 + * + * @param createSession / + * @return / + */ + public SaStrategy setCreateSession(SaCreateSessionFunction createSession) { + this.createSession = createSession; + return this; + } + + /** + * 判断:集合中是否包含指定元素(模糊匹配) + * + * @param hasElement / + * @return / + */ + public SaStrategy setHasElement(SaHasElementFunction hasElement) { + this.hasElement = hasElement; + return this; + } + + /** + * 生成唯一式 token 的算法 + * + * @param generateUniqueToken / + * @return / + */ + public SaStrategy setGenerateUniqueToken(SaGenerateUniqueTokenFunction generateUniqueToken) { + this.generateUniqueToken = generateUniqueToken; + return this; + } + + /** + * 创建 StpLogic 的算法 + * + * @param createStpLogic / + * @return / + */ + public SaStrategy setCreateStpLogic(SaCreateStpLogicFunction createStpLogic) { + this.createStpLogic = createStpLogic; + return this; + } + + /** + * 是否自动续期 + * + * @param autoRenew / + * @return / + */ + public SaStrategy setAutoRenew(SaAutoRenewFunction autoRenew) { + this.autoRenew = autoRenew; + return this; + } + + // + + /** + * 请更换为 instance + */ + @Deprecated + public static final SaStrategy me = instance; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHook.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHook.java new file mode 100644 index 000000000..a7d0624d7 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHook.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; + +/** + * 防火墙策略校验钩子函数 - 接口 + * + * @author click33 + * @since 1.41.0 + */ +@FunctionalInterface +public interface SaFirewallCheckHook { + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + void execute(SaRequest req, SaResponse res, Object extArg); + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForBlackPath.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForBlackPath.java new file mode 100644 index 000000000..bec8baf4d --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForBlackPath.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.RequestPathInvalidException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 防火墙策略校验钩子函数:请求 path 黑名单校验 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForBlackPath implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForBlackPath instance = new SaFirewallCheckHookForBlackPath(); + + /** + * 请求 path 黑名单 + */ + public List blackPaths = new ArrayList<>(); + + /** + * 重载配置 + * @param paths 黑名单 path 列表 + */ + public void resetConfig(String... paths) { + this.blackPaths.clear(); + this.blackPaths.addAll(Arrays.asList(paths)); + } + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 扩展预留参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + String requestPath = req.getRequestPath(); + for (String item : blackPaths) { + if (requestPath.equals(item)) { + throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath); + } + } + + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForDirectoryTraversal.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForDirectoryTraversal.java new file mode 100644 index 000000000..9e41f44d0 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForDirectoryTraversal.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.RequestPathInvalidException; + +/** + * 防火墙策略校验钩子函数:请求 path 目录遍历符检测 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForDirectoryTraversal implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForDirectoryTraversal instance = new SaFirewallCheckHookForDirectoryTraversal(); + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + String requestPath = req.getRequestPath(); + if(!isPathValid(requestPath)) { + throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath); + } + } + + /** + * 检查路径是否有效 + * @param path / + * @return / + */ + public static boolean isPathValid(String path) { + if (path == null || path.isEmpty()) { + return false; + } + + // 必须以 '/' 开头 + if (path.charAt(0) != '/') { + return false; + } + + // 特殊处理根路径 "/" + if (path.equals("/")) { + return true; + } + + String[] components = path.split("/"); + for (int i = 0; i < components.length; i++) { + String component = components[i]; + + // 处理空组件 + if (component.isEmpty()) { + if (i == 0) { + // 允许路径以 "/" 开头(第一个组件为空) + continue; + } else { + // 其他位置的空组件(如中间或末尾的 "//")非法 + return false; + } + } + + // 检查是否包含 "." 或 ".." 组件 + if (component.equals(".") || component.equals("..")) { + return false; + } + } + return true; + } + + // 测试 +// public static void main(String[] args) { +// test("/user/info", true); // 合法 +// test("/user/info/.", false); // 末尾包含 /. +// test("/user/info/..", false); // 末尾包含 /.. +// test("/user/info/./get", false); // 中间包含 /./ +// test("/user/info/../get", false); // 中间包含 /../ +// test("/user/info/.js", true); // 合法后缀 +// test("/.abcdef", true); // 合法隐藏文件 +// test("//user", false); // 多余斜杠 +// test("/user//info", false); // 中间多余斜杠 +// test("/", true); // 根目录合法 +// test("user/../info", false); // 不以 / 开头 +// test("a/b/c/..", false); // 不以 / 开头 +// test("test/.", false); // 不以 / 开头 +// test("", true); // 空路径非法 +// } +// +// private static void test(String path, boolean expected) { +// boolean result = isPathValid(path); +// System.out.printf("Path: %-20s Expected: %-5s Actual: %-5s %s%n", +// path, expected, result, (result == expected) ? "✓" : "✗"); +// } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHeader.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHeader.java new file mode 100644 index 000000000..8b85ce94d --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHeader.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.FirewallCheckException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 防火墙策略校验钩子函数:请求头检测 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForHeader implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForHeader instance = new SaFirewallCheckHookForHeader(); + + /** + * 不允许的请求头列表 + */ + public List notAllowHeaderNames = new ArrayList<>(); + + public SaFirewallCheckHookForHeader() { + } + + /** + * 配置 + * @param notAllowHeaderNames 不允许的请求头列表 (先清空原来的,再添加上新的) + */ + public void resetConfig(String... notAllowHeaderNames) { + this.notAllowHeaderNames.clear(); + this.notAllowHeaderNames.addAll(Arrays.asList(notAllowHeaderNames)); + } + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + for (String headerName : notAllowHeaderNames) { + if(req.getHeader(headerName) != null) { + throw new FirewallCheckException("非法请求头:" + headerName); + } + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHost.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHost.java new file mode 100644 index 000000000..597aa995c --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHost.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.FirewallCheckException; +import cn.dev33.satoken.strategy.SaStrategy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 防火墙策略校验钩子函数:Host 检测 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForHost implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForHost instance = new SaFirewallCheckHookForHost(); + + /** + * 是否校验 host,默认关闭 + */ + public boolean isCheckHost = false; + + /** + * 允许的 host 列表,允许通配符 + */ + public List allowHosts = new ArrayList<>(); + + /** + * 重载配置 + * @param isCheckHost 是否校验 host + * @param allowHosts 允许的 host 列表 (先清空原来的,再添加上新的) + */ + public void resetConfig(boolean isCheckHost, String... allowHosts) { + this.isCheckHost = isCheckHost; + this.allowHosts.clear(); + this.allowHosts.addAll(Arrays.asList(allowHosts)); + } + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + if(isCheckHost) { + String host = req.getHost(); + if( ! SaStrategy.instance.hasElement.apply(allowHosts, host) ) { + throw new FirewallCheckException("非法请求 host:" + host); + } + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHttpMethod.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHttpMethod.java new file mode 100644 index 000000000..2b74246d5 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHttpMethod.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.FirewallCheckException; +import cn.dev33.satoken.router.SaHttpMethod; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 防火墙策略校验钩子函数:请求 Method 检测 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForHttpMethod implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForHttpMethod instance = new SaFirewallCheckHookForHttpMethod(); + + /** + * 是否校验 请求Method,默认开启 + */ + public boolean isCheckMethod = true; + + /** + * 允许的 HttpMethod 列表 + */ + public List allowMethods = new ArrayList<>(); + + public SaFirewallCheckHookForHttpMethod() { + // 默认允许的 HttpMethod 列表 + allowMethods.add(SaHttpMethod.GET.name()); + allowMethods.add(SaHttpMethod.POST.name()); + allowMethods.add(SaHttpMethod.PUT.name()); + allowMethods.add(SaHttpMethod.DELETE.name()); + allowMethods.add(SaHttpMethod.HEAD.name()); + allowMethods.add(SaHttpMethod.OPTIONS.name()); + allowMethods.add(SaHttpMethod.PATCH.name()); + allowMethods.add(SaHttpMethod.TRACE.name()); + allowMethods.add(SaHttpMethod.CONNECT.name()); + } + + /** + * 配置 + * @param isCheckMethod 是否校验 Method + * @param methods 允许的 HttpMethod 列表 (先清空原来的,再添加上新的) + */ + public void resetConfig(boolean isCheckMethod, String... methods) { + this.isCheckMethod = isCheckMethod; + this.allowMethods.clear(); + this.allowMethods.addAll(Arrays.asList(methods)); + } + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + if(isCheckMethod) { + String method = req.getMethod(); + if( ! allowMethods.contains(method) ) { + throw new FirewallCheckException("非法请求 Method:" + method); + } + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForParameter.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForParameter.java new file mode 100644 index 000000000..27803e698 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForParameter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.FirewallCheckException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 防火墙策略校验钩子函数:请求参数检测 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForParameter implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForParameter instance = new SaFirewallCheckHookForParameter(); + + /** + * 不允许的请求参数列表 + */ + public List notAllowParameterNames = new ArrayList<>(); + + public SaFirewallCheckHookForParameter() { + } + + /** + * 配置 + * @param notAllowParameterNames 不允许的请求参数列表 (先清空原来的,再添加上新的) + */ + public void resetConfig(String... notAllowParameterNames) { + this.notAllowParameterNames.clear(); + this.notAllowParameterNames.addAll(Arrays.asList(notAllowParameterNames)); + } + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + for (String parameterName : notAllowParameterNames) { + if(req.getParam(parameterName) != null) { + throw new FirewallCheckException("非法请求参数:" + parameterName); + } + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathBannedCharacter.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathBannedCharacter.java new file mode 100644 index 000000000..0dfc8b261 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathBannedCharacter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.RequestPathInvalidException; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 防火墙策略校验钩子函数:请求 path 禁止字符校验 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForPathBannedCharacter implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForPathBannedCharacter instance = new SaFirewallCheckHookForPathBannedCharacter(); + + /** + * 是否严格禁止出现百分号字符 % (默认:否) + */ + public boolean bannedPercentage = false; + + /** + * 重载配置 + * @param bannedPercentage 是否严格禁止出现百分号字符 % (默认:否) + */ + public void resetConfig(boolean bannedPercentage) { + this.bannedPercentage = bannedPercentage; + } + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + // 非可打印 ASCII 字符检查 + String requestPath = req.getRequestPath(); + if(SaFoxUtil.hasNonPrintableASCII(requestPath)) { + throw new RequestPathInvalidException("请求 path 包含禁止字符:" + requestPath, requestPath); + } + if(bannedPercentage && requestPath.contains("%")) { + throw new RequestPathInvalidException("请求 path 包含禁止字符 %:" + requestPath, requestPath); + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathDangerCharacter.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathDangerCharacter.java new file mode 100644 index 000000000..7444e66d6 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathDangerCharacter.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.RequestPathInvalidException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 防火墙策略校验钩子函数:请求 path 危险字符校验 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForPathDangerCharacter implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForPathDangerCharacter instance = new SaFirewallCheckHookForPathDangerCharacter(); + + /** + * 请求 path 不允许出现的危险字符 + */ + public List dangerCharacter = new ArrayList<>(Arrays.asList( + "//", // // + "\\", // \ + "%2e", "%2E", // . + "%2f", "%2F", // / + "%5c", "%5C", // \ + ";", "%3b", "%3B", // ; // 参考资料:https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA + "%25", // 空格 + "\0", "%00", // 空字符 + "\n", "%0a", "%0A", // 换行符 + "\r", "%0d", "%0D", // 回车符 + "\u2028", // 行分隔符 + "\u2029" // 段分隔符 + )); + + /** + * 重载配置 + * @param character 危险字符列表 + */ + public void resetConfig(String... character) { + this.dangerCharacter = Arrays.asList(character); + } + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + String requestPath = req.getRequestPath(); + for (String item : dangerCharacter) { + if (requestPath.contains(item)) { + throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath); + } + } + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForWhitePath.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForWhitePath.java new file mode 100644 index 000000000..c848e31e1 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForWhitePath.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.hooks; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.StopMatchException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 防火墙策略校验钩子函数:请求 path 白名单放行 + * + * @author click33 + * @since 1.41.0 + */ +public class SaFirewallCheckHookForWhitePath implements SaFirewallCheckHook { + + /** + * 默认实例 + */ + public static SaFirewallCheckHookForWhitePath instance = new SaFirewallCheckHookForWhitePath(); + + /** + * 请求 path 白名单 + */ + public List whitePaths = new ArrayList<>(); + + /** + * 重载配置 + * @param paths 白名单 path 列表 + */ + public void resetConfig(String... paths) { + this.whitePaths.clear(); + this.whitePaths.addAll(Arrays.asList(paths)); + } + + /** + * 执行的方法 + * + * @param req 请求对象 + * @param res 响应对象 + * @param extArg 预留扩展参数 + */ + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + String requestPath = req.getRequestPath(); + for (String item : whitePaths) { + if (requestPath.equals(item)) { + throw new StopMatchException(); + } + } + + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempDefaultImpl.java deleted file mode 100644 index e0ec0cbb1..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempDefaultImpl.java +++ /dev/null @@ -1,10 +0,0 @@ -package cn.dev33.satoken.temp; - -/** - * Sa-Token 临时令牌验证模块 默认实现类 - * @author kong - * - */ -public class SaTempDefaultImpl implements SaTempInterface { - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempInterface.java b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempInterface.java deleted file mode 100644 index 8c3c93738..000000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempInterface.java +++ /dev/null @@ -1,80 +0,0 @@ -package cn.dev33.satoken.temp; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * Sa-Token 临时令牌验证模块接口 - * @author kong - * - */ -public interface SaTempInterface { - - /** - * 根据value创建一个token - * @param value 指定值 - * @param timeout 有效期,单位:秒 - * @return 生成的token - */ - public default String createToken(Object value, long timeout) { - - // 生成 token - String token = SaManager.getSaTokenAction().createToken(null, null); - - // 持久化映射关系 - String key = splicingKeyTempToken(token); - SaManager.getSaTokenDao().setObject(key, value, timeout); - - // 返回 - return token; - } - - /** - * 解析token获取value - * @param token 指定token - * @return See Note - */ - public default Object parseToken(String token) { - String key = splicingKeyTempToken(token); - return SaManager.getSaTokenDao().getObject(key); - } - - /** - * 解析token获取value,并转换为指定类型 - * @param token 指定token - * @param cs 指定类型 - * @param 默认值的类型 - * @return See Note - */ - public default T parseToken(String token, Class cs) { - return SaFoxUtil.getValueByType(parseToken(token), cs); - } - - /** - * 获取指定 token 的剩余有效期,单位:秒 - *

返回值 -1 代表永久,-2 代表token无效 - * @param token see note - * @return see note - */ - public default long getTimeout(String token) { - String key = splicingKeyTempToken(token); - return SaManager.getSaTokenDao().getObjectTimeout(key); - } - - /** - * 获取映射关系的持久化key - * @param token token值 - * @return key - */ - public default String splicingKeyTempToken(String token) { - return SaManager.getConfig().getTokenName() + ":temp-token:" + token; - } - - /** - * @return jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) - */ - public default String getJwtSecretKey() { - return null; - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempTemplate.java new file mode 100644 index 000000000..3e6f6f4ec --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempTemplate.java @@ -0,0 +1,376 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.temp; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.raw.SaRawSessionDelegator; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaTtlMethods; + +import java.util.*; + +/** + * Sa-Token 临时 token 验证模块 + * + *

+ * 有效期很短的一种token,一般用于一次性接口防盗用、短时间资源访问等业务场景 + *

+ * + * @author click33 + * @since 1.42.0 + */ +public class SaTempTemplate implements SaTtlMethods { + + /** + *默认命名空间 + */ + public static final String DEFAULT_NAMESPACE = "temp-token"; + + /** + * 命名空间 + */ + public String namespace; + + /** + * Raw Session 读写委托 + */ + public SaRawSessionDelegator rawSessionDelegator; + + /** + * 在 raw-session 中的保存索引列表使用的 key + */ + public static final String TEMP_TOKEN_MAP = "__HD_TEMP_TOKEN_MAP"; + + public SaTempTemplate(){ + this(DEFAULT_NAMESPACE); + } + + /** + * 实例化 + * @param namespace 命名空间,用于多实例隔离 + */ + public SaTempTemplate(String namespace){ + if(SaFoxUtil.isEmpty(namespace)) { + throw new SaTokenException("namespace 不能为空"); + } + this.namespace = namespace; + this.rawSessionDelegator = new SaRawSessionDelegator(namespace); + } + + + // -------- 创建 + + /** + * 为指定 value 创建一个临时 token (如果多条业务线均需要创建临时 token,请自行在 value 拼接不同前缀) + * + * @param value 指定值 + * @param timeout 有效时间,单位:秒,-1 代表永久有效 + * @return 生成的 token + */ + public String createToken(Object value, long timeout) { + return createToken(value, timeout, false); + } + + /** + * 为指定 业务标识、指定 value 创建一个 Token + * @param value 指定值 + * @param timeout 有效期,单位:秒,-1 代表永久有效 + * @param isRecordIndex 是否记录索引,以便后续使用 value 反查 token + * @return 生成的token + */ + public String createToken(Object value, long timeout, boolean isRecordIndex) { + + // 生成 temp-token + String tempToken = createTempTokenValue(value); + + // 持久化映射关系 + saveToken(tempToken, value, timeout); + + // 记录索引 + if(isRecordIndex) { + SaSession session = rawSessionDelegator.getSessionById(value); + addTempTokenIndex(session, tempToken, timeout); + adjustIndex(value, session); + } + + // 返回 + return tempToken; + } + + /** + * 保存 token + * @param token / + * @param value / + * @param timeout / + */ + public void saveToken(String token, Object value, long timeout) { + String key = splicingTempTokenSaveKey(token); + SaManager.getSaTokenDao().setObject(key, value, timeout); + } + + /** + * 创建一个 temp-token 值 + * + * @return / + */ + public String createTempTokenValue(Object value) { + return SaStrategy.instance.generateUniqueToken.execute( + "Temp Token", + SaManager.getConfig().getMaxTryTimes(), + () -> randomTempToken(value), + _apiKey -> _getValue(_apiKey) == null + ); + } + + /** + * 随机一个 temp-token + * + * @return / + */ + public String randomTempToken(Object value) { + return UUID.randomUUID().toString().replace("-", ""); + } + + + // -------- 解析 + + /** + * 解析 Token 获取 value + * @param token 指定 Token + * @return / + */ + public Object parseToken(String token) { + return _getValue(token); + } + + /** + * 解析 Token 获取 value,并转换为指定类型 + * + * @param token 指定 Token + * @param cs 指定类型 + * @param 默认值的类型 + * @return / + */ + public T parseToken(String token, Class cs) { + return parseToken(token, null, cs); + } + + /** + * 解析 token 获取 value,并裁剪指定前缀,然后转换为指定类型 + *

+ * 请注意此方法在旧版本(<= v1.41.0) 时的三个参数为:service, token, class
+ * 新版本三个参数为:token, cutPrefix, class
+ * 请注意其中的逻辑变化 + *

+ * + * @param token 指定 Token + * @param cs 指定类型 + * @param 默认值的类型 + * @return / + */ + public T parseToken(String token, String cutPrefix, Class cs) { + // 解析值 + Object value = parseToken(token); + + // 如果未指定裁剪前缀,则直接返回 + if(SaFoxUtil.isEmpty(cutPrefix)) { + return SaFoxUtil.getValueByType(value, cs); + } + + // 如果符合前缀则裁剪并返回,如果不符合前缀则返回 null + checkCutPrefixLength(cutPrefix); + String str = SaFoxUtil.valueToString(value); + if(str.startsWith(cutPrefix)) { + return SaFoxUtil.getValueByType(str.substring(cutPrefix.length()), cs); + } else { + return null; + } + } + + /** + * 获取指定指定 Token 的剩余有效期,单位:秒 + *

返回值 -1 代表永久,-2 代表 token 无效 + * + * @param token / + * @return / + */ + public long getTimeout(String token) { + return _getTimeout(token); + } + + + // -------- 删除 + + /** + * 删除一个 token + * @param token 指定 Token + */ + public void deleteToken(String token) { + // 如果无此数据,则直接返回 + Object value = parseToken(token); + if(SaFoxUtil.isEmpty(value)) { + return; + } + + // 删除 token 本身 + _deleteToken(token); + + // 调整索引 + SaSession session = rawSessionDelegator.getSessionById(value, false); + if(session != null) { + deleteTempTokenIndex(session, token); + adjustIndex(value, null); + } + } + + + + // ------------------- 索引操作 + + /** + * 调整索引 + * + * @param value 值 + * @param session 可填写 null,代表使用 value 现场查询 + * @return 调整后的索引列表 + */ + public Map adjustIndex(Object value, SaSession session) { + + // 未提供则现场查询 + if(session == null) { + session = rawSessionDelegator.getSessionById(value, false); + if(session == null) { + return newTokenIndexMap(); + } + } + + // 重新整理索引列表 + Map tempTokenNewList = newTokenIndexMap(); + ArrayList tempTokenTtlList = new ArrayList<>(); + Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap); + for (Map.Entry entry : tempTokenMap.entrySet()) { + long ttl = expireTimeToTtl(entry.getValue()); + if(ttl != SaTokenDao.NOT_VALUE_EXPIRE) { + tempTokenNewList.put(entry.getKey(), entry.getValue()); + tempTokenTtlList.add(ttl); + } + } + + // 有则保存,无则删除 + if( ! tempTokenNewList.isEmpty()) { + session.set(TEMP_TOKEN_MAP, tempTokenNewList); + } else { + rawSessionDelegator.deleteSessionById(value); + return tempTokenNewList; + } + + // 调整 SaSession TTL + long maxTtl = getMaxTtl(tempTokenTtlList); + if(maxTtl != 0) { + session.updateTimeout(maxTtl); + } + return tempTokenNewList; + } + + /** + * 获取指定 value 的 temp-token 列表记录 + * @param value / + * @return / + */ + public List getTempTokenList(Object value) { + // 先调增索引再获取,否则有可能获取到的不是最新有效数据 + Map tempTokenMap = adjustIndex(value, null); + return new ArrayList<>(tempTokenMap.keySet()); + } + + /** + * 在 SaSession 上添加临时 temp-token 索引 + * @param session / + * @param token / + * @param timeout / + */ + protected void addTempTokenIndex(SaSession session, String token, long timeout) { + Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap); + if(! tempTokenMap.containsKey(token)) { + tempTokenMap.put(token, ttlToExpireTime(timeout)); + session.set(TEMP_TOKEN_MAP, tempTokenMap); + } + } + + /** + * 在 SaSession 上删除临时 temp-token 索引 + * @param session / + * @param token / + */ + protected void deleteTempTokenIndex(SaSession session, String token) { + Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap); + if(tempTokenMap.containsKey(token)) { + tempTokenMap.remove(token); + session.set(TEMP_TOKEN_MAP, tempTokenMap); + } + } + + + // -------- 元操作 + + protected Object _getValue(String token) { + String key = splicingTempTokenSaveKey(token); + return SaManager.getSaTokenDao().getObject(key); + } + protected void _deleteToken(String token) { + String key = splicingTempTokenSaveKey(token); + SaManager.getSaTokenDao().deleteObject(key); + } + protected long _getTimeout(String token) { + String key = splicingTempTokenSaveKey(token); + return SaManager.getSaTokenDao().getObjectTimeout(key); + } + + + + // -------- 其它 + + /** + * 检查裁剪前缀长度 + * @param cutPrefix / + */ + protected static void checkCutPrefixLength(String cutPrefix) { + if(cutPrefix.length() >= 32) { + throw new SaTokenException("裁剪前缀长度必须小于 32 位"); + } + } + + /** + * 获取:在存储临时 token 数据时,应该使用的 key + * @param token token值 + * @return key + */ + public String splicingTempTokenSaveKey(String token) { + return SaManager.getConfig().getTokenName() + ":" + namespace + ":" + token; + } + + /** + * @return jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) + */ + public String getJwtSecretKey() { + return null; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java index b3d98c991..3ff1f65b4 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java @@ -1,52 +1,145 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.temp; import cn.dev33.satoken.SaManager; +import java.util.List; + /** - * Sa-Token 临时验证令牌模块 - * @author kong + * Sa-Token 临时 token 验证模块 - 工具类 * + *

+ * 有效期很短的一种token,一般用于一次性接口防盗用、短时间资源访问等业务场景 + *

+ * + * @author click33 + * @since 1.20.0 */ public class SaTempUtil { + private SaTempUtil() { + } + + // -------- 创建 + /** - * 根据value创建一个token + * 为指定 value 创建一个临时 token (如果多条业务线均需要创建临时 token,请自行在 value 拼接不同前缀) + * * @param value 指定值 - * @param timeout 有效期,单位:秒 - * @return 生成的token + * @param timeout 有效时间,单位:秒,-1 代表永久有效 + * @return 生成的 token */ public static String createToken(Object value, long timeout) { - return SaManager.getSaTemp().createToken(value, timeout); + return SaManager.getSaTempTemplate().createToken(value, timeout); + } + + /** + * 为指定 业务标识、指定 value 创建一个 Token + * @param value 指定值 + * @param timeout 有效期,单位:秒,-1 代表永久有效 + * @param isRecordIndex 是否记录索引,以便后续使用 value 反查 token + * @return 生成的token + */ + public static String createToken(Object value, long timeout, boolean isRecordIndex) { + return SaManager.getSaTempTemplate().createToken(value, timeout, isRecordIndex); + } + + /** + * 保存 token + * @param token / + * @param value / + * @param timeout / + */ + public static void saveToken(String token, Object value, long timeout) { + SaManager.getSaTempTemplate().saveToken(token, value, timeout); } - + + // -------- 解析 + /** - * 解析token获取value - * @param token 指定token - * @return See Note + * 解析 Token 获取 value + * @param token 指定 Token + * @return / */ public static Object parseToken(String token) { - return SaManager.getSaTemp().parseToken(token); + return SaManager.getSaTempTemplate().parseToken(token); } /** - * 解析token获取value,并转换为指定类型 - * @param token 指定token - * @param cs 指定类型 - * @param 默认值的类型 - * @return See Note + * 解析 Token 获取 value,并转换为指定类型 + * + * @param token 指定 Token + * @param cs 指定类型 + * @param 默认值的类型 + * @return / */ public static T parseToken(String token, Class cs) { - return SaManager.getSaTemp().parseToken(token, cs); + return SaManager.getSaTempTemplate().parseToken(token, cs); + } + + /** + * 解析 token 获取 value,并裁剪指定前缀,然后转换为指定类型 + *

+ * 请注意此方法在旧版本(<= v1.41.0) 时的三个参数为:service, token, class
+ * 新版本三个参数为:token, cutPrefix, class
+ * 请注意其中的逻辑变化 + *

+ * + * @param token 指定 Token + * @param cs 指定类型 + * @param 默认值的类型 + * @return / + */ + public static T parseToken(String token, String cutPrefix, Class cs) { + return SaManager.getSaTempTemplate().parseToken(token, cutPrefix, cs); } - + /** - * 获取指定 token 的剩余有效期,单位:秒 - *

返回值 -1 代表永久,-2 代表token无效 - * @param token see note - * @return see note + * 获取指定指定 Token 的剩余有效期,单位:秒 + *

返回值 -1 代表永久,-2 代表 token 无效 + * + * @param token / + * @return / */ public static long getTimeout(String token) { - return SaManager.getSaTemp().getTimeout(token); + return SaManager.getSaTempTemplate().getTimeout(token); + } + + + // -------- 删除 + + /** + * 删除一个 token + * @param token 指定 Token + */ + public static void deleteToken(String token) { + SaManager.getSaTempTemplate().deleteToken(token); + } + + + // ------------------- 索引操作 + + /** + * 获取指定 value 的 temp-token 列表记录 + * @param value / + * @return / + */ + public static List getTempTokenList(Object value) { + return SaManager.getSaTempTemplate().getTempTokenList(value); } - + } 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 7a7a03247..c69c1ceb8 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 @@ -1,61 +1,95 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaTokenException; + +import java.io.Console; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; import java.net.URLDecoder; import java.net.URLEncoder; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Random; -import java.util.regex.Pattern; - -import cn.dev33.satoken.exception.SaTokenException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; /** - * Sa-Token 内部工具类 - * - * @author kong + * Sa-Token 内部工具类 * + * @author click33 + * @since 1.18.0 */ public class SaFoxUtil { + private SaFoxUtil() { + } + /** * 打印 Sa-Token 版本字符画 */ public static void printSaToken() { - String str = "____ ____ ___ ____ _ _ ____ _ _ \r\n" + "[__ |__| __ | | | |_/ |___ |\\ | \r\n" - + "___] | | | |__| | \\_ |___ | \\| " -// + "sa-token:" - + "\r\n" + "DevDoc:" + SaTokenConsts.DEV_DOC_URL // + "\r\n"; - + " (" + SaTokenConsts.VERSION_NO + ")" - + "\r\n" + "GitHub:" + SaTokenConsts.GITHUB_URL // + "\r\n"; + String str = "" + + "____ ____ ___ ____ _ _ ____ _ _ \r\n" + + "[__ |__| __ | | | |_/ |___ |\\ | \r\n" + + "___] | | | |__| | \\_ |___ | \\| " +// + SaTokenConsts.VERSION_NO +// + "sa-token:" +// + "\r\n" + "DevDoc:" + SaTokenConsts.DEV_DOC_URL // + "\r\n"; + + "\r\n" + SaTokenConsts.DEV_DOC_URL // + "\r\n"; + + " (" + SaTokenConsts.VERSION_NO + ")" +// + "\r\n" + "GitHub:" + SaTokenConsts.GITHUB_URL // + "\r\n"; ; System.out.println(str); } /** * 生成指定长度的随机字符串 - * + * * @param length 字符串的长度 * @return 一个随机字符串 */ public static String getRandomString(int length) { String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - Random random = new Random(); - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { - int number = random.nextInt(62); + int number = ThreadLocalRandom.current().nextInt(62); sb.append(str.charAt(number)); } 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 指定元素 + * @param str 指定元素 * @return 是否为null或者空字符串 */ public static boolean isEmpty(Object str) { @@ -64,43 +98,114 @@ public static boolean isEmpty(Object str) { /** * 指定元素是否不为 (null或者空字符串) - * @param str 指定元素 + * @param str 指定元素 * @return 是否为null或者空字符串 */ public static boolean isNotEmpty(Object str) { - return isEmpty(str) == false; + return ! isEmpty(str); + } + + /** + * 指定数组是否为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 第一个对象 + * @param b 第二个对象 + * @return 两个对象是否相等 + */ + public static boolean equals(Object a, Object b) { + return (a == b) || (a != null && a.equals(b)); + } /** + * 比较两个对象是否不相等 + * @param a 第一个对象 + * @param b 第二个对象 + * @return 两个对象是否不相等 + */ + public static boolean notEquals(Object a, Object b) { + return !equals(a, b); + } + /** * 以当前时间戳和随机int数字拼接一个随机字符串 - * + * * @return 随机字符串 */ public static String getMarking28() { - return System.currentTimeMillis() + "" + new Random().nextInt(Integer.MAX_VALUE); + return System.currentTimeMillis() + "" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE); } /** * 将日期格式化 (yyyy-MM-dd HH:mm:ss) * @param date 日期 - * @return 格式化后的时间 + * @return 格式化后的时间 */ public static String formatDate(Date date){ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); } - + + /** + * 将日期格式化 (yyyy-MM-dd HH:mm:ss) + * @param zonedDateTime 日期 + * @return 格式化后的时间 + */ + public static String formatDate(ZonedDateTime zonedDateTime) { + return zonedDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + /** + * 指定毫秒后的时间(格式化 :yyyy-MM-dd HH:mm:ss) + * @param ms 指定毫秒后 + * @return 格式化后的时间 + */ + public static String formatAfterDate(long ms) { + Instant instant = Instant.ofEpochMilli(System.currentTimeMillis() + ms); + ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()); + return formatDate(zonedDateTime); + } + /** * 从集合里查询数据 - * + * * @param dataList 数据集合 * @param prefix 前缀 * @param keyword 关键字 * @param start 起始位置 (-1代表查询所有) * @param size 获取条数 + * @param sortType 排序类型(true=正序,false=反序) + * * @return 符合条件的新数据集合 */ - public static List searchList(Collection dataList, String prefix, String keyword, int start, - int size) { + public static List searchList(Collection dataList, String prefix, String keyword, int start, int size, boolean sortType) { if (prefix == null) { prefix = ""; } @@ -108,34 +213,46 @@ public static List searchList(Collection dataList, String prefix keyword = ""; } // 挑选出所有符合条件的 - List list = new ArrayList(); - Iterator keys = dataList.iterator(); - while (keys.hasNext()) { - String key = keys.next(); - if (key.startsWith(prefix) && key.indexOf(keyword) > -1) { + List list = new ArrayList<>(); + for (String key : dataList) { + if (key.startsWith(prefix) && key.contains(keyword)) { list.add(key); } } // 取指定段数据 - return searchList(list, start, size); + return searchList(list, start, size, sortType); } /** * 从集合里查询数据 - * + * * @param list 数据集合 - * @param start 起始位置 (-1代表查询所有) - * @param size 获取条数 + * @param start 起始位置 + * @param size 获取条数 (-1代表从start处一直取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * * @return 符合条件的新数据集合 */ - public static List searchList(List list, int start, int size) { - // 取指定段数据 + public static List searchList(List list, int start, int size, boolean sortType) { + // 如果是反序的话 + if( ! sortType) { + Collections.reverse(list); + } + // start 至少为0 if (start < 0) { - return list; + start = 0; } - int end = start + size; - List list2 = new ArrayList(); + // size为-1时,代表一直取到末尾,否则取到 start + size + int end; + if(size == -1) { + end = list.size(); + } else { + end = start + size; + } + // 取出的数据放到新集合中 + List list2 = new ArrayList<>(); for (int i = start; i < end; i++) { + // 如果已经取到list的末尾,则直接退出 if (i >= list.size()) { return list2; } @@ -143,22 +260,81 @@ public static List searchList(List list, int start, int size) { } return list2; } - + /** * 字符串模糊匹配 *

example: - *

user* user-add -- true - *

user* art-add -- false - * @param patt 表达式 - * @param str 待匹配的字符串 - * @return 是否可以匹配 + *

user* user-add -- true + *

user* art-add -- false + * @param patt 表达式 + * @param str 待匹配的字符串 + * @return 是否可以匹配 */ public static boolean vagueMatch(String patt, String str) { - // 如果表达式不带有*号,则只需简单equals即可 (速度提升200倍) - if(patt.indexOf("*") == -1) { + // 两者均为 null 时,直接返回 true + if(patt == null && str == null) { + return true; + } + // 两者其一为 null 时,直接返回 false + if(patt == null || str == null) { + return false; + } + // 如果表达式不带有*号,则只需简单equals即可 (这样可以使速度提升200倍左右) + if( ! patt.contains("*")) { return patt.equals(str); } - return Pattern.matches(patt.replaceAll("\\*", ".*"), str); + // 深入匹配 + return vagueMatchMethod(patt, str); + } + + /** + * 字符串模糊匹配 + * + * @param pattern / + * @param str / + * @return / + */ + private static boolean vagueMatchMethod( String pattern, String str) { + int m = str.length(); + int n = pattern.length(); + boolean[][] dp = new boolean[m + 1][n + 1]; + dp[0][0] = true; + for (int i = 1; i <= n; ++i) { + if (pattern.charAt(i - 1) == '*') { + dp[0][i] = true; + } else { + break; + } + } + for (int i = 1; i <= m; ++i) { + for (int j = 1; j <= n; ++j) { + if (pattern.charAt(j - 1) == '*') { + dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; + } else if (str.charAt(i - 1) == pattern.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } + } + } + return dp[m][n]; + } + + /** + * 判断类型是否为8大包装类型 + * @param cs / + * @return / + */ + public static boolean isWrapperType(Class cs) { + return cs == Integer.class || cs == Short.class || cs == Long.class || cs == Byte.class + || cs == Float.class || cs == Double.class || cs == Boolean.class || cs == Character.class; + } + + /** + * 判断类型是否为基础类型:8大基本数据类型、8大包装类、String + * @param cs / + * @return / + */ + public static boolean isBasicType(Class cs) { + return cs.isPrimitive() || isWrapperType(cs) || cs == String.class; } /** @@ -166,17 +342,17 @@ public static boolean vagueMatch(String patt, String str) { * @param 泛型 * @param obj 值 * @param cs 类型 - * @return 转换后的值 + * @return 转换后的值 */ @SuppressWarnings("unchecked") public static T getValueByType(Object obj, Class cs) { - // 如果 obj 为 null 或者本来就是 cs 类型 + // 如果 obj 为 null 或者本来就是 cs 类型 if(obj == null || obj.getClass().equals(cs)) { return (T)obj; } // 开始转换 String obj2 = String.valueOf(obj); - Object obj3 = null; + Object obj3; if (cs.equals(String.class)) { obj3 = obj2; } else if (cs.equals(int.class) || cs.equals(Integer.class)) { @@ -193,21 +369,53 @@ public static T getValueByType(Object obj, Class cs) { obj3 = Double.valueOf(obj2); } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) { obj3 = Boolean.valueOf(obj2); + } else if (cs.equals(char.class) || cs.equals(Character.class)) { + obj3 = obj2.charAt(0); } else { - obj3 = (T)obj; + obj3 = obj; } return (T)obj3; } - + /** - * 在url上拼接上kv参数并返回 + * 将 Map 转化为 Object + * @param map / + * @param clazz / + * @return / + * @param / + */ + public static T mapToObject(Map map, Class clazz) { + if(map == null) { + return null; + } + if(clazz == Map.class) { + return (T) map; + } + try { + T obj = clazz.getDeclaredConstructor().newInstance(); + for (Field field : clazz.getDeclaredFields()) { + String fieldName = field.getName(); + if (map.containsKey(fieldName)) { + field.setAccessible(true); + field.set(obj, map.get(fieldName)); + } + } + return obj; + } catch (Exception e) { + throw new RuntimeException("转换失败: " + e.getMessage(), e); + } + } + + + /** + * 在url上拼接上kv参数并返回 * @param url url - * @param parameStr 参数, 例如 id=1001 - * @return 拼接后的url字符串 + * @param paramStr 参数, 例如 id=1001 + * @return 拼接后的url字符串 */ - public static String joinParam(String url, String parameStr) { - // 如果参数为空, 直接返回 - if(parameStr == null || parameStr.length() == 0) { + public static String joinParam(String url, String paramStr) { + // 如果参数为空, 直接返回 + if(paramStr == null || paramStr.length() == 0) { return url; } if(url == null) { @@ -216,93 +424,119 @@ public static String joinParam(String url, String parameStr) { int index = url.lastIndexOf('?'); // ? 不存在 if(index == -1) { - return url + '?' + parameStr; + return url + '?' + paramStr; } // ? 是最后一位 if(index == url.length() - 1) { - return url + parameStr; + return url + paramStr; } // ? 是其中一位 - if(index > -1 && index < url.length() - 1) { + if(index < url.length() - 1) { String separatorChar = "&"; - // 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 & - if(url.lastIndexOf(separatorChar) != url.length() - 1 && parameStr.indexOf(separatorChar) != 0) { - return url + separatorChar + parameStr; + // 如果最后一位是 不是&, 且 paramStr 第一位不是 &, 就赠送一个 & + if(url.lastIndexOf(separatorChar) != url.length() - 1 && paramStr.indexOf(separatorChar) != 0) { + return url + separatorChar + paramStr; } else { - return url + parameStr; + return url + paramStr; } } - // 正常情况下, 代码不可能执行到此 + // 正常情况下, 代码不可能执行到此 return url; } /** - * 在url上拼接上kv参数并返回 + * 在url上拼接上kv参数并返回 * @param url url * @param key 参数名称 - * @param value 参数值 - * @return 拼接后的url字符串 + * @param value 参数值 + * @return 拼接后的url字符串 */ public static String joinParam(String url, String key, Object value) { - // 如果参数为空, 直接返回 - if(isEmpty(url) || isEmpty(key) || isEmpty(value)) { + // 如果url或者key为空, 直接返回 + if(isEmpty(url) || isEmpty(key)) { return url; } return joinParam(url, key + "=" + value); } /** - * 在url上拼接锚参数 + * 在url上拼接锚参数 * @param url url - * @param parameStr 参数, 例如 id=1001 - * @return 拼接后的url字符串 + * @param paramStr 参数, 例如 id=1001 + * @return 拼接后的url字符串 */ - public static String joinSharpParam(String url, String parameStr) { - // 如果参数为空, 直接返回 - if(parameStr == null || parameStr.length() == 0) { + public static String joinSharpParam(String url, String paramStr) { + // 如果参数为空, 直接返回 + if(paramStr == null || paramStr.length() == 0) { return url; } if(url == null) { url = ""; } int index = url.lastIndexOf('#'); - // ? 不存在 + // # 不存在 if(index == -1) { - return url + '#' + parameStr; + return url + '#' + paramStr; } - // ? 是最后一位 + // # 是最后一位 if(index == url.length() - 1) { - return url + parameStr; + return url + paramStr; } - // ? 是其中一位 - if(index > -1 && index < url.length() - 1) { + // # 是其中一位 + if(index < url.length() - 1) { String separatorChar = "&"; - // 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 & - if(url.lastIndexOf(separatorChar) != url.length() - 1 && parameStr.indexOf(separatorChar) != 0) { - return url + separatorChar + parameStr; + // 如果最后一位是 不是&, 且 paramStr 第一位不是 &, 就赠送一个 & + if(url.lastIndexOf(separatorChar) != url.length() - 1 && paramStr.indexOf(separatorChar) != 0) { + return url + separatorChar + paramStr; } else { - return url + parameStr; + return url + paramStr; } } - // 正常情况下, 代码不可能执行到此 + // 正常情况下, 代码不可能执行到此 return url; } /** - * 在url上拼接锚参数 + * 在url上拼接锚参数 * @param url url * @param key 参数名称 - * @param value 参数值 - * @return 拼接后的url字符串 + * @param value 参数值 + * @return 拼接后的url字符串 */ public static String joinSharpParam(String url, String key, Object value) { - // 如果参数为空, 直接返回 - if(isEmpty(url) || isEmpty(key) || isEmpty(value)) { + // 如果url或者key为空, 直接返回 + if(isEmpty(url) || isEmpty(key)) { return url; } return joinSharpParam(url, key + "=" + value); } - + + /** + * 拼接两个url + *

例如:url1=http://domain.cn,url2=/sso/auth,则返回:http://domain.cn/sso/auth

+ * + * @param url1 第一个url + * @param url2 第二个url + * @return 拼接完成的url + */ + public static String spliceTwoUrl(String url1, String url2) { + // q1、任意一个为空,则直接返回另一个 + if(url1 == null) { + return url2; + } + if(url2 == null) { + return url1; + } + + // q2、如果 url2 以 http 开头,将其视为一个完整地址 + if(url2.startsWith("http")) { + return url2; + } + + // q3、将两个地址拼接在一起 + return url1 + url2; + } + /** * 将数组的所有元素使用逗号拼接在一起 * @param arr 数组 @@ -312,73 +546,73 @@ public static String arrayJoin(String[] arr) { if(arr == null) { return ""; } - String str = ""; + StringBuilder str = new StringBuilder(); for (int i = 0; i < arr.length; i++) { - str += arr[i]; + str.append(arr[i]); if(i != arr.length - 1) { - str += ","; + str.append(","); } } - return str; + return str.toString(); } - + /** - * 验证URL的正则表达式 + * 验证URL的正则表达式 */ - public static final String URL_REGEX = "(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]"; - + public static String URL_REGEX = "(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]"; + /** * 使用正则表达式判断一个字符串是否为URL - * @param str 字符串 - * @return 拼接后的url字符串 + * @param str 字符串 + * @return 拼接后的url字符串 */ public static boolean isUrl(String str) { - if(str == null) { + if(isEmpty(str)) { return false; } return str.toLowerCase().matches(URL_REGEX); } - + /** - * URL编码 - * @param url see note - * @return see note + * URL编码 + * @param url see note + * @return see note */ public static String encodeUrl(String url) { try { return URLEncoder.encode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12103); } } /** - * URL解码 - * @param url see note - * @return see note + * URL解码 + * @param url see note + * @return see note */ public static String decoderUrl(String url) { try { return URLDecoder.decode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaErrorCode.CODE_12104); } } - + /** - * 将指定字符串按照逗号分隔符转化为字符串集合 + * 将指定字符串按照逗号分隔符转化为字符串集合 * @param str 字符串 - * @return 分割后的字符串集合 + * @return 分割后的字符串集合 */ public static List convertStringToList(String str) { - List list = new ArrayList(); + List list = new ArrayList<>(); if(isEmpty(str)) { return list; } String[] arr = str.split(","); for (String s : arr) { s = s.trim(); - if(isEmpty(s) == false) { + if(!isEmpty(s)) { list.add(s); } } @@ -386,22 +620,225 @@ public static List convertStringToList(String str) { } /** - * 将指定集合按照逗号连接成一个字符串 - * @param list 集合 - * @return 字符串 + * 将指定集合按照逗号连接成一个字符串 + * @param list 集合 + * @return 字符串 */ public static String convertListToString(List list) { - if(list == null || list.size() == 0) { + if(list == null || list.isEmpty()) { return ""; } - String str = ""; + StringBuilder str = new StringBuilder(); for (int i = 0; i < list.size(); i++) { - str += list.get(i); + str.append(list.get(i)); if(i != list.size() - 1) { - str += ","; + str.append(","); } } - return str; + return str.toString(); } - + + /** + * String 转 Array,按照逗号切割 + * @param str 字符串 + * @return 数组 + */ + public static String[] convertStringToArray(String str) { + List list = convertStringToList(str); + return list.toArray(new String[0]); + } + + /** + * Array 转 String,按照逗号连接 + * @param arr 数组 + * @return 字符串 + */ + public static String convertArrayToString(String[] arr) { + if(arr == null || arr.length == 0) { + return ""; + } + return String.join(",", arr); + } + + /** + * 返回一个空集合 + * @param 集合类型 + * @return 空集合 + */ + public static List emptyList() { + return new ArrayList<>(); + } + + /** + * String数组转集合 + * @param str String数组 + * @return 集合 + */ + public static List toList(String... str) { + 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"); + + /** + * 将日志等级从 String 格式转化为 int 格式 + * @param level / + * @return / + */ + public static int translateLogLevelToInt(String level) { + int levelInt = logLevelList.indexOf(level); + if(levelInt <= 0 || levelInt >= logLevelList.size()) { + levelInt = 1; + } + return levelInt; + } + + /** + * 将日志等级从 String 格式转化为 int 格式 + * @param level / + * @return / + */ + public static String translateLogLevelToString(int level) { + if(level <= 0 || level >= logLevelList.size()) { + level = 1; + } + return logLevelList.get(level); + } + + /** + * 判断当前系统是否可以打印彩色日志,判断准确率并非100%,但基本可以满足大部分场景 + * @return / + */ + @SuppressWarnings("all") + public static boolean isCanColorLog() { + + // 获取当前环境相关信息 + Console console = System.console(); + String term = System.getenv().get("TERM"); + + // 两者均为 null,一般是在 eclipse、idea 等 IDE 环境下运行的,此时可以打印彩色日志 + if(console == null && term == null) { + return true; + } + + // 两者均不为 null,一般是在 linux 环境下控制台运行的,此时可以打印彩色日志 + if(console != null && term != null) { + return true; + } + + // console 有值,term 为 null,一般是在 windows 的 cmd 控制台运行的,此时不可以打印彩色日志 + if(console != null && term == null) { + return false; + } + + // console 为 null,term 有值,一般是在 linux 的 nohup 命令运行的,此时不可以打印彩色日志 + // 此时也有可能是在 windows 的 git bash 环境下运行的,此时可以打印彩色日志,但此场景无法和上述场景区分,所以统一不打印彩色日志 + if(console == null && term != null) { + return false; + } + + // 正常情况下,代码不会走到这里,但是方法又必须要有返回值,所以随便返回一个 + 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; + } + + /** + * 检查字符串是否包含非可打印 ASCII 字符 + * @param str / + * @return / + */ + public static boolean hasNonPrintableASCII(String str) { + if (str == null) { + return false; + } + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + // ASCII 范围检查:0-31 或 127 + if ((c <= 31) || (c == 127)) { + return true; + } + } + return false; + } + + /** + * 将 value 转化为 String,如果 value 为 null,则返回空字符串 + * @param value / + * @return / + */ + public static String valueToString(Object value) { + if (value == null) { + return ""; + } + return value.toString(); + } + + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaHexUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaHexUtil.java new file mode 100644 index 000000000..fda2cfa5c --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaHexUtil.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; + +/** + * 十六进制工具类 + * + * @author deepseek + * @since 2025/2/24 + */ +public class SaHexUtil { + + // 十六进制字符表(大写) + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + /** + * 将字节数组转换为十六进制字符串(JDK8兼容) + * @param bytes 要转换的字节数组 + * @return 十六进制字符串(大写) + */ + public static String bytesToHex(byte[] bytes) { + if (bytes == null) return null; + char[] hexChars = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; i++) { + int v = bytes[i] & 0xFF; + hexChars[i * 2] = HEX_ARRAY[v >>> 4]; + hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + /** + * 将十六进制字符串转换为字节数组(JDK8兼容) + * @param hexString 有效的十六进制字符串(不区分大小写) + * @return 对应的字节数组 + * @throws IllegalArgumentException 输入字符串格式错误时抛出异常 + */ + public static byte[] hexToBytes(String hexString) { + if (hexString == null) return null; + int len = hexString.length(); + if (len % 2 != 0) { + throw new IllegalArgumentException("Hex string must have even length"); + } + + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + int high = Character.digit(hexString.charAt(i), 16); + int low = Character.digit(hexString.charAt(i+1), 16); + + if (high == -1 || low == -1) { + throw new IllegalArgumentException( + "Invalid hex character at position " + i + " or " + (i+1) + ); + } + + data[i/2] = (byte) ((high << 4) + low); + } + return data; + } + +} \ No newline at end of file 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 512220774..b0faa3c1c 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 @@ -1,30 +1,75 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; +import cn.dev33.satoken.SaManager; + import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; /** - * 对Ajax请求返回Json格式数据的简易封装
- * 所有预留字段:
- * code=状态码
- * msg=描述信息
- * data=携带对象
- * @author kong + * 对请求接口返回 Json 格式数据的简易封装。 + * + *

+ * 所有预留字段:
+ * code = 状态码
+ * msg = 描述信息
+ * data = 携带对象
+ *

* + * @author click33 + * @since 1.22.0 */ public class SaResult extends LinkedHashMap implements Serializable{ - private static final long serialVersionUID = 1L; // 序列化版本号 - + // 序列化版本号 + private static final long serialVersionUID = 1L; + + // 预定的状态码 public static final int CODE_SUCCESS = 200; - public static final int CODE_ERROR = 500; + public static final int CODE_ERROR = 500; + public static final int CODE_NOT_PERMISSION = 403; + public static final int CODE_NOT_LOGIN = 401; + /** + * 构建 + */ + public SaResult() { + } + + /** + * 构建 + * @param code 状态码 + * @param msg 信息 + * @param data 数据 + */ public SaResult(int code, String msg, Object data) { this.setCode(code); this.setMsg(msg); this.setData(data); } + + /** + * 根据 Map 快速构建 + * @param map / + */ + public SaResult(Map map) { + this.setMap(map); + } /** * 获取code @@ -45,7 +90,7 @@ public String getMsg() { * @return data */ public Object getData() { - return (Object)this.get("data"); + return this.get("data"); } /** @@ -87,20 +132,68 @@ public SaResult set(String key, Object data) { return this; } + /** + * 获取一个值 根据自定义key + * @param 要转换为的类型 + * @param key key + * @param cs 要转换为的类型 + * @return 值 + */ + public T get(String key, Class cs) { + return SaFoxUtil.getValueByType(get(key), cs); + } + /** * 写入一个Map, 连缀风格 * @param map map * @return 对象自身 */ public SaResult setMap(Map map) { - for (String key : map.keySet()) { - this.put(key, map.get(key)); + if(map != null) { + for (String key : map.keySet()) { + this.put(key, map.get(key)); + } } return this; } + + /** + * 写入一个 json 字符串, 连缀风格 + * @param jsonString json 字符串 + * @return 对象自身 + */ + public SaResult setJsonString(String jsonString) { + Map map = SaManager.getSaJsonTemplate().jsonToMap(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; + } + - - // ============================ 构建 ================================== + // ============================ 静态方法快速构建 ================================== // 构建成功 public static SaResult ok() { @@ -109,6 +202,9 @@ public static SaResult ok() { public static SaResult ok(String msg) { return new SaResult(CODE_SUCCESS, msg, null); } + public static SaResult code(int code) { + return new SaResult(code, null, null); + } public static SaResult data(Object data) { return new SaResult(CODE_SUCCESS, "ok", data); } @@ -121,11 +217,27 @@ public static SaResult error(String msg) { return new SaResult(CODE_ERROR, msg, null); } + // 构建未登录 + public static SaResult notLogin() { + return new SaResult(CODE_NOT_LOGIN, "not login", null); + } + + // 构建无权限 + public static SaResult notPermission() { + return new SaResult(CODE_NOT_PERMISSION, "not permission", null); + } + + + // 构建指定状态码 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() @@ -134,9 +246,27 @@ public static SaResult get(int code, String msg, Object data) { public String toString() { return "{" + "\"code\": " + this.getCode() - + ", \"msg\": \"" + this.getMsg() + "\"" - + ", \"data\": \"" + this.getData() + "\"" + + ", \"msg\": " + transValue(this.getMsg()) + + ", \"data\": " + transValue(this.getData()) + "}"; } + + /** + * 转换 value 值: + * 如果 value 值属于 String 类型,则在前后补上引号 + * 如果 value 值属于其它类型,则原样返回 + * + * @param value 具体要操作的值 + * @return 转换后的值 + */ + private String transValue(Object value) { + if(value == null) { + return null; + } + if(value instanceof String) { + return "\"" + value + "\""; + } + return String.valueOf(value); + } } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaSugar.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaSugar.java new file mode 100644 index 000000000..811f8af09 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaSugar.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; + +import cn.dev33.satoken.fun.SaFunction; + +import java.util.function.Supplier; + +/** + * 代码语法糖封装 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSugar { + + /** + * 执行一个 Lambda 表达式,返回这个 Lambda 表达式的结果值, + *
方便组织代码,例如: + *
 
+	 	int value = Sugar.get(() -> {
+			int a = 1;
+			int b = 2;
+			return a + b;
+		});
+		
+ * @param 返回值类型 + * @param lambda lambda 表达式 + * @return lambda 的执行结果 + */ + public static R get(Supplier lambda) { + return lambda.get(); + } + + /** + * 执行一个 Lambda 表达式 + *
方便组织代码,例如: + *
 
+	 	Sugar.exe(() -> {
+			int a = 1;
+			int b = 2;
+			return a + b;
+		});
+		
+ * @param lambda lambda 表达式 + */ + public static void exe(SaFunction lambda) { + lambda.run(); + } + +} 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 5ec6c6204..6f654b948 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 @@ -1,59 +1,130 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; /** - * Sa-Token常量类 - * @author kong + * Sa-Token 常量类 + * + *

+ * 一般的常量采用就近原则,定义在各自相应的模块中。 + * 但有一些常量没有明确的归属模块,会在很多模块中使用到,比如版本号、开源地址等,属于全局性的础性常量,这些常量统一定义在此类中。 + *

* + * @author click33 + * @since 1.8.0 */ public class SaTokenConsts { + private SaTokenConsts() { + } - // =================== sa-token版本信息 =================== + // ------------------ Sa-Token 版本信息 /** * Sa-Token 当前版本号 */ - public static final String VERSION_NO = "v1.24.1"; + public static final String VERSION_NO = "v1.45.0"; /** - * Sa-Token 开源地址 + * Sa-Token 开源地址 Gitee + */ + public static final String GITEE_URL = "https://gitee.com/dromara/sa-token"; + + /** + * Sa-Token 开源地址 GitHub */ public static final String GITHUB_URL = "https://github.com/dromara/sa-token"; /** * Sa-Token 开发文档地址 */ - public static final String DEV_DOC_URL = "http://sa-token.dev33.cn"; + public static final String DEV_DOC_URL = "https://sa-token.com"; + - // =================== 常量key标记 =================== + // ------------------ 常量 key 标记 /** - * 常量key标记: 如果token为本次请求新创建的,则以此字符串为key存储在当前request中 + * 常量 key 标记: 如果 token 为本次请求新创建的,则以此字符串为 key 存储在当前请求 str 中 + */ + public static final String JUST_CREATED = "JUST_CREATED_"; + + /** + * 常量 key 标记: 如果 token 为本次请求新创建的,则以此字符串为 key 存储在当前 request 中(不拼接前缀,纯Token) + */ + public static final String JUST_CREATED_NOT_PREFIX = "JUST_CREATED_NOT_PREFIX_"; + + /** + * 常量 key 标记: 如果本次请求已经验证过 activeTimeout, 则以此 key 在 storage 中做一个标记 + */ + public static final String TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY = "TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY_"; + + /** + * 常量 key 标记: 在登录时,默认使用的设备类型 + */ + public static final String DEFAULT_LOGIN_DEVICE_TYPE = "DEF"; + + /** + * 常量 key 标记: 在封禁账号时,默认封禁的服务类型 */ - public static final String JUST_CREATED_SAVE_KEY = "JUST_CREATED_SAVE_KEY_"; + public static final String DEFAULT_DISABLE_SERVICE = "login"; /** - * 常量key标记: 如果本次请求已经验证过[无操作过期], 则以此值存储在当前request中 + * 常量 key 标记: 在封禁账号时,默认封禁的等级 */ - public static final String TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY = "TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY_"; + public static final int DEFAULT_DISABLE_LEVEL = 1; /** - * 常量key标记: 在登录时,默认使用的设备名称 + * 常量 key 标记: 在封禁账号时,可使用的最小封禁级别 */ - public static final String DEFAULT_LOGIN_DEVICE = "default-device"; + public static final int MIN_DISABLE_LEVEL = 1; + + /** + * 常量 key 标记: 账号封禁级别,表示未被封禁 + */ + public static final int NOT_DISABLE_LEVEL = -2; /** - * 常量key标记: 在进行临时身份切换时使用的key + * 常量 key 标记: 在进行临时身份切换时使用的 key */ public static final String SWITCH_TO_SAVE_KEY = "SWITCH_TO_SAVE_KEY_"; /** - * 常量key标记: 在进行Token二级验证时使用的key + * 常量 key 标记: 在进行 Token 二级验证时,使用的 key */ + @Deprecated public static final String SAFE_AUTH_SAVE_KEY = "SAFE_AUTH_SAVE_KEY_"; + /** + * 常量 key 标记: 在进行 Token 二级认证时,写入的 value 值 + */ + public static final String SAFE_AUTH_SAVE_VALUE = "SAFE_AUTH_SAVE_VALUE"; + + /** + * 常量 key 标记: 在进行 Token 二级验证时,默认的业务类型 + */ + public static final String DEFAULT_SAFE_AUTH_SERVICE = "important"; + + /** + * 常量 key 标记: 临时 Token 认证模块,默认的业务类型 + */ + @Deprecated + public static final String DEFAULT_TEMP_TOKEN_SERVICE = "record"; - // =================== token-style 相关 =================== + + // ------------------ token-style 相关 /** * Token风格: uuid @@ -83,13 +154,36 @@ public class SaTokenConsts { /** * Token风格: tik风格 (2_14_16) */ - public static final String TOKEN_STYLE_TIK = "tik"; + public static final String TOKEN_STYLE_TIK = "tik"; - - // =================== 其它 =================== + + // ------------------ SaSession 的类型 + + /** + * SaSession 的类型: Account-Session + */ + public static final String SESSION_TYPE__ACCOUNT = "Account-Session"; + + /** + * SaSession 的类型: Token-Session + */ + public static final String SESSION_TYPE__TOKEN = "Token-Session"; + + /** + * SaSession 的类型: Anon-Token-Session + */ + public static final String SESSION_TYPE__ANON = "Anon-Token-Session"; /** - * 连接Token前缀和Token值的字符 + * SaSession 的类型: Custom-Session + */ + public static final String SESSION_TYPE__CUSTOM = "Custom-Session"; + + + // ------------------ 其它 + + /** + * 连接 Token 前缀和 Token 值的字符 */ public static final String TOKEN_CONNECTOR_CHAT = " "; @@ -97,5 +191,63 @@ public class SaTokenConsts { * 切面、拦截器、过滤器等各种组件的注册优先级顺序 */ public static final int ASSEMBLY_ORDER = -100; + + /** + * 防火墙校验过滤器的注册顺序 + */ + public static final int FIREWALL_CHECK_FILTER_ORDER = -102; + + /** + * 跨域处理过滤器的注册顺序 + */ + public static final int CORS_FILTER_ORDER = -103; + + /** + * 上下文过滤器的注册顺序 + */ + public static final int SA_TOKEN_CONTEXT_FILTER_ORDER = -104; + + /** + * RPC 框架权限过滤器的注册顺序 + */ + public static final int RPC_PERMISSION_FILTER_ORDER = -30000; + + /** + * RPC 框架上下文过滤器的注册顺序 + */ + public static final int RPC_CONTEXT_FILTER_ORDER = -30005; + + /** + * Content-Type key + */ + public static final String CONTENT_TYPE_KEY = "Content-Type"; + + /** + * Content-Type text/plain; charset=utf-8 + */ + public static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain; charset=utf-8"; + + /** + * Content-Type application/json;charset=UTF-8 + */ + public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json;charset=UTF-8"; + + + + // =================== 废弃 =================== + + /** + * 请更换为 JUST_CREATED + */ + @Deprecated + public static final String JUST_CREATED_SAVE_KEY = JUST_CREATED; + + /** + * 请更换为 TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY + */ + @Deprecated + public static final String TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY = TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY; + + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTtlMethods.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTtlMethods.java new file mode 100644 index 000000000..9bf3a698f --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTtlMethods.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; + +import cn.dev33.satoken.dao.SaTokenDao; + +import java.util.*; + +/** + * TTL 操作工具方法 + * + * @author click33 + * @since 1.43.0 + */ +public interface SaTtlMethods { + + /** + * 获取一个新的 Token 集合 + * @return / + */ + default List newTokenValueList() { + return new ArrayList<>(); + } + + /** + * 获取一个新的 TokenIndexMap 集合 + * @return / + */ + default Map newTokenIndexMap() { + return new LinkedHashMap<>(); + } + + /** + * 获取最大 ttl 值 + * @param ttlList / + * @return / + */ + default long getMaxTtl(ArrayList ttlList) { + long maxTtl = 0; + for (long ttl : ttlList) { + if(ttl == SaTokenDao.NEVER_EXPIRE) { + maxTtl = SaTokenDao.NEVER_EXPIRE; + break; + } + if(ttl > maxTtl) { + maxTtl = ttl; + } + } + return maxTtl; + } + + /** + * 获取最大 ttl 值:过期时间 (13位时间戳) 转 ttl (秒) + * @param expireTimeList / + * @return / + */ + default long getMaxTtlByExpireTime(Collection expireTimeList) { + long maxTtl = 0; + for (long expireTime : expireTimeList) { + long ttl = expireTimeToTtl(expireTime); + if(ttl == SaTokenDao.NEVER_EXPIRE) { + maxTtl = SaTokenDao.NEVER_EXPIRE; + break; + } + if(ttl > maxTtl) { + maxTtl = ttl; + } + } + return maxTtl; + } + + /** + * 过期时间 (13位时间戳) 转 (13位时间戳) ttl (秒) + * @param expireTime / + * @return / + */ + default long expireTimeToTtl(long expireTime) { + if(expireTime == SaTokenDao.NEVER_EXPIRE) { + return SaTokenDao.NEVER_EXPIRE; + } + if(expireTime == SaTokenDao.NOT_VALUE_EXPIRE) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + long currentTime = System.currentTimeMillis(); + if(expireTime < currentTime) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + return (expireTime - currentTime) / 1000; + } + + /** + * ttl (秒) 转 过期时间 (13位时间戳) + * @param ttl / + * @return / + */ + default long ttlToExpireTime(long ttl) { + if(ttl == SaTokenDao.NEVER_EXPIRE) { + return SaTokenDao.NEVER_EXPIRE; + } + if(ttl < 0) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + return ttl * 1000 + System.currentTimeMillis(); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaValue2Box.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaValue2Box.java new file mode 100644 index 000000000..71eb4760a --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaValue2Box.java @@ -0,0 +1,219 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; + + +/** + * 封装两个值的容器,方便取值、写值等操作,value1 和 value2 用逗号隔开,形如:123,abc + * + * @author click33 + * @since 1.35.0 + */ +public class SaValue2Box { + + /** + * 第一个值 + */ + private Object value1; + + /** + * 第二个值 + */ + private Object value2; + + /** + * 直接提供两个值构建 + * @param value1 第一个值 + * @param value2 第二个值 + */ + public SaValue2Box(Object value1, Object value2) { + this.value1 = value1; + this.value2 = value2; + } + + /** + * 根据字符串构建,字符串形如:123,abc + * @param valueString 形如:123,abc + */ + public SaValue2Box(String valueString) { + if(valueString == null){ + return; + } + String[] split = valueString.split(","); + if(split.length == 0){ + // do nothing + } + else if(split.length == 1){ + this.value1 = split[0]; + } + else { + this.value1 = split[0]; + this.value2 = split[1]; + } + } + + /** + * 获取第一个值 + * @return 第一个值 + */ + public Object getValue1() { + return value1; + } + + /** + * 获取第二个值 + * @return 第二个值 + */ + public Object getValue2() { + return value2; + } + + /** + * 设置第一个值 + * @param value1 第一个值 + */ + public void setValue1(Object value1) { + this.value1 = value1; + } + + /** + * 设置第二个值 + * @param value2 第二个值 + */ + public void setValue2(Object value2) { + this.value2 = value2; + } + + /** + * 判断第一个值是否为 null 或者空字符串 + * @return / + */ + public boolean value1IsEmpty() { + return SaFoxUtil.isEmpty(value1); + } + + /** + * 判断第二个值是否为 null 或者空字符串 + * @return / + */ + public boolean value2IsEmpty() { + return SaFoxUtil.isEmpty(value2); + } + + /** + * 获取第一个值,并转化为 String 类型 + * @return / + */ + public String getValue1AsString() { + return value1 == null ? null : value1.toString(); + } + + /** + * 获取第二个值,并转化为 String 类型 + * @return / + */ + public String getValue2AsString() { + return value2 == null ? null : value2.toString(); + } + + /** + * 获取第一个值,并转化为 long 类型 + * @return / + */ + public long getValue1AsLong() { + return Long.parseLong(value1.toString()); + } + + /** + * 获取第二个值,并转化为 long 类型 + * @return / + */ + public long getValue2AsLong() { + return Long.parseLong(value2.toString()); + } + + /** + * 获取第一个值,并转化为 long 类型,值不存在则返回默认值 + * @return / + */ + public Long getValue1AsLong(Long defaultValue) { + // 这里如果改成三元表达式,会导致自动拆箱造成空指针异常,所以只能用 if-else + if(value1 == null){ + return defaultValue; + } + return Long.parseLong(value1.toString()); + } + + /** + * 获取第二个值,并转化为 long 类型,值不存在则返回默认值 + * @return / + */ + public Long getValue2AsLong(Long defaultValue) { + // 这里如果改成三元表达式,会导致自动拆箱造成空指针异常,所以只能用 if-else + if(value2 == null){ + return defaultValue; + } + return Long.parseLong(value2.toString()); + } + + /** + * 该容器是否为无值状态,即:value1 无值、value2 无值 + * @return / + */ + public boolean isNotValueState() { + return value1IsEmpty() && value2IsEmpty(); + } + + /** + * 该容器是否为单值状态,即:value1 有值、value2 == 无值 + * @return / + */ + public boolean isSingleValueState() { + return ! value1IsEmpty() && value2IsEmpty(); + } + + /** + * 该容器是否为双值状态,即:value2 有值 (在 value2 有值的情况下,即使 value1 无值,也视为双值状态) + * @return / + */ + public boolean isDoubleValueState() { + return ! value2IsEmpty(); + } + + /** + * 获取两个值的字符串形式,形如:123,abc + * + *

+ *
+     *     System.out.println(new SaValue2Box(1, 2));     // 1,2
+     *     System.out.println(new SaValue2Box(null, null));   // null
+     *     System.out.println(new SaValue2Box(1, null));   // 1
+     *     System.out.println(new SaValue2Box(null, 2));  // ,2
+     * 
+ * @return / + */ + @Override + public String toString() { + if(value1 == null && value2 == null) { + return null; + } + if(value1 != null && value2 == null) { + return value1.toString(); + } + return (value1 == null ? "" : value1.toString()) + "," + (value2 == null ? "" : value2.toString()); + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/StrFormatter.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/StrFormatter.java new file mode 100644 index 000000000..a50b9ae2c --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/StrFormatter.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; + +/** + * 字符串格式化工具,将字符串中的 {} 按序替换为参数 + *

+ * 本工具类 copy 自 Hutool: + * https://github.com/dromara/hutool/blob/v5-master/hutool-core/src/main/java/cn/hutool/core/text/StrFormatter.java + *

+ * + * @author Looly + * @since 1.33.0 + */ +public class StrFormatter { + + /** + * 占位符(保留原有 public 访问权限,避免破坏外部依赖) + * @deprecated 语义不明确,建议内部使用 {@link #DEFAULT_PLACEHOLDER} 替代 + */ + @Deprecated + public static String EMPTY_JSON = "{}"; + + /** + * 反斜杠转义字符(保留原有 public 访问权限,避免破坏外部依赖) + * @deprecated 命名不规范,建议内部使用 {@link #BACKSLASH_CHAR} 替代 + */ + @Deprecated + public static char C_BACKSLASH = '\\'; + + /** + * 新增内部规范常量(private,仅内部使用) + * 默认占位符 */ + private static final String DEFAULT_PLACEHOLDER = "{}"; + + /** + * 反斜杠转义字符 + * */ + private static final char BACKSLASH_CHAR = '\\'; + + /** + * 字符串构建器初始扩容长度 + * */ + private static final int BUFFER_INIT_CAPACITY = 50; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") =》 this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") =》 this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") =》 this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 格式化后的结果 + */ + public static String format(String strPattern, Object... argArray) { + return formatWith(strPattern, DEFAULT_PLACEHOLDER, argArray); + } + + /** + * 格式化字符串
+ * 此方法只是简单将指定占位符 按照顺序替换为参数
+ * 如果想输出占位符使用 \\转义即可,如果想输出占位符之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "{}", "a", "b") =》 this is a for b
+ * 转义{}: format("this is \\{} for {}", "{}", "a", "b") =》 this is {} for a
+ * 转义\: format("this is \\\\{} for {}", "{}", "a", "b") =》 this is \a for b
+ * + * @param strPattern 字符串模板 + * @param placeHolder 占位符,例如{} + * @param argArray 参数列表 + * @return 格式化后的结果 + * @since 1.33.0 + */ + public static String formatWith(String strPattern, String placeHolder, Object... argArray) { + if (SaFoxUtil.isEmpty(strPattern) || SaFoxUtil.isEmpty(placeHolder) || SaFoxUtil.isEmpty(argArray)) { + return strPattern; + } + final int strPatternLength = strPattern.length(); + final int placeHolderLength = placeHolder.length(); + + // 初始化定义好的长度以获得更好的性能 + final StringBuilder sbu = new StringBuilder(strPatternLength + BUFFER_INIT_CAPACITY); + + int handledPosition = 0;// 记录已经处理到的位置 + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) { + delimIndex = strPattern.indexOf(placeHolder, handledPosition); + if (delimIndex == -1) {// 剩余部分无占位符 + if (handledPosition == 0) { // 不带占位符的模板直接返回 + return strPattern; + } + // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbu.append(strPattern, handledPosition, strPatternLength); + return sbu.toString(); + } + + // 转义符 + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == BACKSLASH_CHAR) {// 转义符 + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == BACKSLASH_CHAR) {// 双转义符 + // 转义符之前还有一个转义符,占位符依旧有效 + sbu.append(strPattern, handledPosition, delimIndex - 1); + sbu.append(argArray[argIndex]); + handledPosition = delimIndex + placeHolderLength; + } else { + // 占位符被转义 + argIndex--; + sbu.append(strPattern, handledPosition, delimIndex - 1); + sbu.append(placeHolder.charAt(0)); + handledPosition = delimIndex + 1; + } + } else {// 正常占位符 + sbu.append(strPattern, handledPosition, delimIndex); + sbu.append(argArray[argIndex]); + handledPosition = delimIndex + placeHolderLength; + } + } + + // 加入最后一个占位符后所有的字符 + sbu.append(strPattern, handledPosition, strPatternLength); + + return sbu.toString(); + } + +} \ No newline at end of file diff --git a/sa-token-demo/pom.xml b/sa-token-demo/pom.xml new file mode 100644 index 000000000..522dbeb38 --- /dev/null +++ b/sa-token-demo/pom.xml @@ -0,0 +1,97 @@ + + 4.0.0 + cn.dev33 + sa-token-demo + 0.0.1-SNAPSHOT + pom + + + + sa-token-demo-alone-redis + sa-token-demo-alone-redis-cluster + sa-token-demo-alone-redis-sb4 + sa-token-demo-alone-redisson + sa-token-demo-alone-redisson-cluster + sa-token-demo-alone-redisson-sb3 + sa-token-demo-alone-redisson-sb4 + sa-token-demo-apikey + sa-token-demo-async + sa-token-demo-beetl + sa-token-demo-bom-import + sa-token-demo-caffeine + sa-token-demo-case + sa-token-demo-device-lock + sa-token-demo-dubbo/sa-token-demo-dubbo-provider + sa-token-demo-dubbo/sa-token-demo-dubbo-consumer + sa-token-demo-dubbo/sa-token-demo-dubbo3-provider + sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer + sa-token-demo-freemarker + sa-token-demo-first-run + + sa-token-demo-hutool-timed-cache + sa-token-demo-jwt + sa-token-demo-oauth2/sa-token-demo-oauth2-server + sa-token-demo-oauth2/sa-token-demo-oauth2-client + sa-token-demo-quick-login + sa-token-demo-quick-login-sb3 + sa-token-demo-remember-me/sa-token-demo-remember-me-server + sa-token-demo-solon + sa-token-demo-solon-redisson + sa-token-demo-springboot + sa-token-demo-springboot3-redis + sa-token-demo-springboot4-redis + sa-token-demo-springboot-low-version + sa-token-demo-springboot-redis + sa-token-demo-springboot-redisson + sa-token-demo-sse + sa-token-demo-ssm + sa-token-demo-sso/sa-token-demo-sso-server + sa-token-demo-sso/sa-token-demo-sso1-client + sa-token-demo-sso/sa-token-demo-sso2-client + sa-token-demo-sso/sa-token-demo-sso3-client + sa-token-demo-sso/sa-token-demo-sso3-client-nosdk + sa-token-demo-sso/sa-token-demo-sso3-client-resdk + sa-token-demo-sso/sa-token-demo-sso3-client-anon + sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon + sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon + sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon + sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon + sa-token-demo-test + sa-token-demo-thymeleaf + sa-token-demo-webflux + sa-token-demo-webflux-springboot3 + sa-token-demo-webflux-springboot4 + sa-token-demo-bug-reproduce + sa-token-demo-websocket + sa-token-demo-websocket-spring + sa-token-demo-loveqq-boot + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..3b0023dcc --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-cluster/pom.xml @@ -0,0 +1,71 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-alone-redis-cluster + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-alone-redis + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/SaTokenAloneRedisClusterApplication.java b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/SaTokenAloneRedisClusterApplication.java new file mode 100644 index 000000000..616b5cc8c --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/SaTokenAloneRedisClusterApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +/** + * Sa-Token整合SpringBoot 示例 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenAloneRedisClusterApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenAloneRedisClusterApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/AjaxJson.java b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/AjaxJson.java new file mode 100644 index 000000000..1581888d2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.test; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..1b123e717 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,39 @@ +package com.pj.test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + StringRedisTemplate stringRedisTemplate; + + // 测试Sa-Token缓存, 浏览器访问: http://localhost:8081/test/login + @RequestMapping("login") + public AjaxJson login(@RequestParam(defaultValue="10001") String id) { + System.out.println("--------------- 测试Sa-Token缓存"); + StpUtil.login(id); + return AjaxJson.getSuccess(); + } + + // 测试业务缓存 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public AjaxJson test() { + System.out.println("--------------- 测试业务缓存"); + stringRedisTemplate.opsForValue().set("hello", "Hello World"); + return AjaxJson.getSuccess(); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application.yml new file mode 100644 index 000000000..458053081 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application.yml @@ -0,0 +1,66 @@ +# 端口 +server: + port: 8081 + +# Sa-Token配置 +sa-token: + # Token名称 (同时也是cookie名称) + token-name: satoken + # Token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # Token风格 + token-style: uuid + # 配置Sa-Token单独使用的Redis连接 + alone-redis: + # 普通集群 + pattern: cluster + # Redis服务器连接用户名(默认为空) + username: + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10s + cluster: + # Redis集群服务器节点地址 + nodes: 127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 + # 最大重定向次数 + maxRedirects: 2 + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + +spring: + # 配置业务使用的Redis连接 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application_sentinel.yml b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application_sentinel.yml new file mode 100644 index 000000000..fc8b73abf --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application_sentinel.yml @@ -0,0 +1,68 @@ +# 端口 +server: + port: 8081 + +# Sa-Token配置 +sa-token: + # Token名称 (同时也是cookie名称) + token-name: satoken + # Token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # Token风格 + token-style: uuid + # 配置Sa-Token单独使用的Redis连接 + alone-redis: + # 哨兵模式 + pattern: sentinel + # Redis数据库索引(默认为0) + database: 2 + # Redis服务器连接用户名(默认为空) + username: + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10s + sentinel: + #哨兵的名字 + master: master_name + # Redis集群服务器节点地址 + nodes: 127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + +spring: + # 配置业务使用的Redis连接 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-alone-redis-sb4/pom.xml b/sa-token-demo/sa-token-demo-alone-redis-sb4/pom.xml new file mode 100644 index 000000000..83c10a086 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-sb4/pom.xml @@ -0,0 +1,78 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-alone-redis-sb4 + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-aspectj + + + + + cn.dev33 + sa-token-spring-boot4-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template-jdk-serializer + ${sa-token.version} + test + + + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-alone-redis-by-spring-boot4 + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + diff --git a/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/SaTokenAloneRedisSb4Application.java b/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/SaTokenAloneRedisSb4Application.java new file mode 100644 index 000000000..eedf1503f --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/SaTokenAloneRedisSb4Application.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 SpringBoot4 示例,整合 alone-redis 插件(权限缓存与业务缓存分离) + * + * @author click33 + */ +@SpringBootApplication +public class SaTokenAloneRedisSb4Application { + + public static void main(String[] args) { + SpringApplication.run(SaTokenAloneRedisSb4Application.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..240cd2eb9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,40 @@ +package com.pj.test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 测试专用Controller,演示 alone-redis 权限缓存与业务缓存分离 + * + * @author click33 + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + StringRedisTemplate stringRedisTemplate; + + // 测试Sa-Token缓存,浏览器访问: http://localhost:8083/test/login + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue = "10001") String id) { + System.out.println("--------------- 测试Sa-Token缓存"); + StpUtil.login(id); + return SaResult.ok(); + } + + // 测试业务缓存,浏览器访问: http://localhost:8083/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("--------------- 测试业务缓存"); + stringRedisTemplate.opsForValue().set("hello", "Hello World"); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/resources/application.yml new file mode 100644 index 000000000..d700d19d4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/resources/application.yml @@ -0,0 +1,59 @@ +# 端口 +server: + port: 8083 + +# Sa-Token配置 +sa-token: + # Token名称 (同时也是cookie名称) + token-name: satoken + # Token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # Token风格 + token-style: uuid + # 配置Sa-Token单独使用的Redis连接 + alone-redis: + # Redis数据库索引(默认为0) + database: 2 + # 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 + +# 配置业务使用的Redis连接 +spring: + data: + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 diff --git a/sa-token-demo/sa-token-demo-alone-redis/.gitignore b/sa-token-demo/sa-token-demo-alone-redis/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-alone-redis/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file 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 4add5011c..c7bb19543 100644 --- a/sa-token-demo/sa-token-demo-alone-redis/pom.xml +++ b/sa-token-demo/sa-token-demo-alone-redis/pom.xml @@ -10,13 +10,14 @@ org.springframework.boot spring-boot-starter-parent - 2.0.0.RELEASE + 2.5.14 + - + - 1.24.0 + 1.45.0 @@ -31,18 +32,18 @@ spring-boot-starter-aop - + cn.dev33 sa-token-spring-boot-starter - ${sa-token-version} + ${sa-token.version} cn.dev33 - sa-token-dao-redis-jackson - ${sa-token-version} + sa-token-redis-jackson + ${sa-token.version} @@ -55,7 +56,7 @@ cn.dev33 sa-token-alone-redis - ${sa-token-version} + ${sa-token.version} diff --git a/sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/SaTokenAloneRedisApplication.java b/sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/SaTokenAloneRedisApplication.java index f78e850e7..20eccd578 100644 --- a/sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/SaTokenAloneRedisApplication.java +++ b/sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/SaTokenAloneRedisApplication.java @@ -7,13 +7,13 @@ /** * Sa-Token整合SpringBoot 示例 - * @author kong + * @author click33 * */ @SpringBootApplication public class SaTokenAloneRedisApplication { - public static void main(String[] args) throws ClassNotFoundException { + public static void main(String[] args) { SpringApplication.run(SaTokenAloneRedisApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } diff --git a/sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/test/TestController.java index 634faeedf..1b123e717 100644 --- a/sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/test/TestController.java +++ b/sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/test/TestController.java @@ -10,7 +10,7 @@ /** * 测试专用Controller - * @author kong + * @author click33 * */ @RestController diff --git a/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml index 65ebe15ed..aeb5f94e8 100644 --- a/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml @@ -11,7 +11,9 @@ sa-token: # Token风格 token-style: uuid # 配置Sa-Token单独使用的Redis连接 - alone-redis: + alone-redis: + # Redis模式(默认单体) + # pattern: single # Redis数据库索引(默认为0) database: 2 # Redis服务器地址 @@ -19,9 +21,9 @@ sa-token: # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) - password: + # password: # 连接超时时间(毫秒) - timeout: 10ms + timeout: 10s lettuce: pool: # 连接池最大连接数 @@ -45,7 +47,7 @@ spring: # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) - timeout: 10ms + timeout: 10s lettuce: pool: # 连接池最大连接数 diff --git a/sa-token-demo/sa-token-demo-alone-redisson-cluster/pom.xml b/sa-token-demo/sa-token-demo-alone-redisson-cluster/pom.xml new file mode 100644 index 000000000..892b070e1 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-cluster/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-alone-redisson-cluster + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + org.redisson + redisson-spring-boot-starter + 3.45.0 + + + + io.netty + netty-transport-native-unix-common + + + + + cn.dev33 + sa-token-alone-redisson + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + diff --git a/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/java/com/pj/SaTokenAloneRedissonClusterApplication.java b/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/java/com/pj/SaTokenAloneRedissonClusterApplication.java new file mode 100644 index 000000000..69d2bd883 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/java/com/pj/SaTokenAloneRedissonClusterApplication.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 SpringBoot 示例,整合 alone-redisson 插件(集群模式) + * + * @author click33 + */ +@SpringBootApplication +public class SaTokenAloneRedissonClusterApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenAloneRedissonClusterApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..f4ad0a449 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,39 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试专用Controller,演示 alone-redisson 集群模式下权限缓存与业务缓存分离 + * + * @author click33 + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + RedissonClient redissonClient; + + // 测试 Sa-Token 缓存,浏览器访问: http://localhost:8085/test/login + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue = "10001") String id) { + System.out.println("--------------- 测试Sa-Token缓存"); + StpUtil.login(id); + return SaResult.ok(); + } + + // 测试业务缓存,浏览器访问: http://localhost:8085/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("--------------- 测试业务缓存"); + redissonClient.getBucket("hello").set("Hello World"); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/resources/application.yml new file mode 100644 index 000000000..14a4979be --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/resources/application.yml @@ -0,0 +1,29 @@ +# 端口 +server: + port: 8085 + +# Sa-Token配置 +sa-token: + # Token名称 (同时也是cookie名称) + token-name: satoken + # Token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # Token风格 + token-style: uuid + # Sa-Token 单独使用的 Redisson 集群连接(Redisson 原生 yaml) + alone-redisson: + config: | + clusterServersConfig: + nodeAddresses: + - "redis://127.0.0.1:3000" + - "redis://127.0.0.1:3001" + - "redis://127.0.0.1:3002" + +# 业务仍使用单机 Redis +spring: + redis: + redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 0 diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb3/pom.xml b/sa-token-demo/sa-token-demo-alone-redisson-sb3/pom.xml new file mode 100644 index 000000000..5b323c4d7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb3/pom.xml @@ -0,0 +1,61 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-alone-redisson-sb3 + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 3.5.11 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + + org.redisson + redisson-spring-boot-starter + 3.45.0 + + + + + cn.dev33 + sa-token-alone-redisson + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/java/com/pj/SaTokenAloneRedissonSb3Application.java b/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/java/com/pj/SaTokenAloneRedissonSb3Application.java new file mode 100644 index 000000000..79a2fb719 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/java/com/pj/SaTokenAloneRedissonSb3Application.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 SpringBoot3 示例,整合 alone-redisson 插件(权限缓存与业务缓存分离) + * + * @author click33 + */ +@SpringBootApplication +public class SaTokenAloneRedissonSb3Application { + + public static void main(String[] args) { + SpringApplication.run(SaTokenAloneRedissonSb3Application.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..5da7091dd --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,39 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试专用Controller,演示 alone-redisson 在 Spring Boot 3 下权限缓存与业务缓存分离 + * + * @author click33 + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + RedissonClient redissonClient; + + // 测试 Sa-Token 缓存,浏览器访问: http://localhost:8087/test/login + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue = "10001") String id) { + System.out.println("--------------- 测试Sa-Token缓存"); + StpUtil.login(id); + return SaResult.ok(); + } + + // 测试业务缓存,浏览器访问: http://localhost:8087/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("--------------- 测试业务缓存"); + redissonClient.getBucket("hello").set("Hello World"); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/resources/application.yml new file mode 100644 index 000000000..9fe6bc566 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/resources/application.yml @@ -0,0 +1,28 @@ +# 端口 +server: + port: 8087 + +# Sa-Token配置 +sa-token: + # Token名称 (同时也是cookie名称) + token-name: satoken + # Token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # Token风格 + token-style: uuid + # Sa-Token 单独使用的 Redisson 连接(Redisson 原生 yaml) + alone-redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 2 + +# 业务使用的 Redisson 连接(Boot 3 前缀为 spring.data.redis) +spring: + data: + redis: + redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 0 diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb4/pom.xml b/sa-token-demo/sa-token-demo-alone-redisson-sb4/pom.xml new file mode 100644 index 000000000..a79110d18 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb4/pom.xml @@ -0,0 +1,54 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-alone-redisson-sb4 + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + + + cn.dev33 + sa-token-spring-boot4-starter + ${sa-token.version} + + + + + + cn.dev33 + sa-token-alone-redisson + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/SaTokenAloneRedissonSb4Application.java b/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/SaTokenAloneRedissonSb4Application.java new file mode 100644 index 000000000..dd90406b6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/SaTokenAloneRedissonSb4Application.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 SpringBoot4 示例,整合 alone-redisson 插件(权限缓存与业务缓存分离) + * + * @author click33 + */ +@SpringBootApplication +public class SaTokenAloneRedissonSb4Application { + + public static void main(String[] args) { + SpringApplication.run(SaTokenAloneRedissonSb4Application.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/redisson/BusinessRedissonConfig.java b/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/redisson/BusinessRedissonConfig.java new file mode 100644 index 000000000..021fd0b81 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/redisson/BusinessRedissonConfig.java @@ -0,0 +1,25 @@ +package com.pj.redisson; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +/** + * 业务 RedissonClient。Boot 4 下不使用 redisson-spring-boot-starter 3.45,改为按 yaml 自行创建。 + * + * @author click33 + */ +@Configuration +public class BusinessRedissonConfig { + + @Bean(destroyMethod = "shutdown") + public RedissonClient redissonClient(@Value("${spring.redis.redisson.config}") String yaml) throws IOException { + return Redisson.create(Config.fromYAML(yaml)); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..4d4995c96 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,39 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试专用Controller,演示 alone-redisson 在 Spring Boot 4 下权限缓存与业务缓存分离 + * + * @author click33 + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + RedissonClient redissonClient; + + // 测试 Sa-Token 缓存,浏览器访问: http://localhost:8086/test/login + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue = "10001") String id) { + System.out.println("--------------- 测试Sa-Token缓存"); + StpUtil.login(id); + return SaResult.ok(); + } + + // 测试业务缓存,浏览器访问: http://localhost:8086/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("--------------- 测试业务缓存"); + redissonClient.getBucket("hello").set("Hello World"); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/resources/application.yml new file mode 100644 index 000000000..ff9807677 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/resources/application.yml @@ -0,0 +1,27 @@ +# 端口 +server: + port: 8086 + +# Sa-Token配置 +sa-token: + # Token名称 (同时也是cookie名称) + token-name: satoken + # Token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # Token风格 + token-style: uuid + # Sa-Token 单独使用的 Redisson 连接(Redisson 原生 yaml) + alone-redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 2 + +# 业务使用的 Redisson 连接 +spring: + redis: + redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 0 diff --git a/sa-token-demo/sa-token-demo-alone-redisson/pom.xml b/sa-token-demo/sa-token-demo-alone-redisson/pom.xml new file mode 100644 index 000000000..99c345465 --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-alone-redisson + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + org.redisson + redisson-spring-boot-starter + 3.45.0 + + + + io.netty + netty-transport-native-unix-common + + + + + cn.dev33 + sa-token-alone-redisson + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + diff --git a/sa-token-demo/sa-token-demo-alone-redisson/src/main/java/com/pj/SaTokenAloneRedissonApplication.java b/sa-token-demo/sa-token-demo-alone-redisson/src/main/java/com/pj/SaTokenAloneRedissonApplication.java new file mode 100644 index 000000000..c198db28d --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson/src/main/java/com/pj/SaTokenAloneRedissonApplication.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 SpringBoot 示例,整合 alone-redisson 插件(权限缓存与业务缓存分离) + * + * @author click33 + */ +@SpringBootApplication +public class SaTokenAloneRedissonApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenAloneRedissonApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-alone-redisson/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..db0bf97ce --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,39 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试专用Controller,演示 alone-redisson 权限缓存与业务缓存分离 + * + * @author click33 + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + RedissonClient redissonClient; + + // 测试 Sa-Token 缓存,浏览器访问: http://localhost:8084/test/login + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue = "10001") String id) { + System.out.println("--------------- 测试Sa-Token缓存"); + StpUtil.login(id); + return SaResult.ok(); + } + + // 测试业务缓存,浏览器访问: http://localhost:8084/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("--------------- 测试业务缓存"); + redissonClient.getBucket("hello").set("Hello World"); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-alone-redisson/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-alone-redisson/src/main/resources/application.yml new file mode 100644 index 000000000..f2830901e --- /dev/null +++ b/sa-token-demo/sa-token-demo-alone-redisson/src/main/resources/application.yml @@ -0,0 +1,27 @@ +# 端口 +server: + port: 8084 + +# Sa-Token配置 +sa-token: + # Token名称 (同时也是cookie名称) + token-name: satoken + # Token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # Token风格 + token-style: uuid + # Sa-Token 单独使用的 Redisson 连接(Redisson 原生 yaml) + alone-redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 2 + +# 业务使用的 Redisson 连接 +spring: + redis: + redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 0 diff --git a/sa-token-demo/sa-token-demo-apikey/pom.xml b/sa-token-demo/sa-token-demo-apikey/pom.xml new file mode 100644 index 000000000..9e54192ca --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/pom.xml @@ -0,0 +1,74 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-apikey + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-apikey + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-devtools + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/SaTokenApiKeyApplication.java b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/SaTokenApiKeyApplication.java new file mode 100644 index 000000000..8884c00e9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/SaTokenApiKeyApplication.java @@ -0,0 +1,18 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.apikey.SaApiKeyManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaTokenApiKeyApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenApiKeyApplication.class, args); + System.out.println("启动成功:Sa-Token 配置如下:" + SaManager.getConfig()); + System.out.println("启动成功:API Key 配置如下:" + SaApiKeyManager.getConfig()); + System.out.println("测试访问:http://localhost:8081/index.html"); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyDataLoaderImpl.java b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyDataLoaderImpl.java new file mode 100644 index 000000000..6e56341fc --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyDataLoaderImpl.java @@ -0,0 +1,31 @@ +package com.pj.mock; + +import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; +import cn.dev33.satoken.apikey.model.ApiKeyModel; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * API Key 数据加载器实现类 (从数据库查询) + * + * @author click33 + * @since 2025/4/4 + */ +//@Component // 打开此注解后,springboot 会自动注入此组件,打开 Sa-Token API Key 模块的数据库模式 +public class SaApiKeyDataLoaderImpl implements SaApiKeyDataLoader { + + @Autowired + SaApiKeyMockMapper apiKeyMockMapper; + + // 指定框架不再维护 API Key 索引信息,而是由我们手动从数据库维护 + @Override + public Boolean getIsRecordIndex() { + return false; + } + + // 根据 apiKey 从数据库获取 ApiKeyModel 信息 (实现此方法无需为数据做缓存处理,框架内部已包含缓存逻辑) + @Override + public ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) { + return apiKeyMockMapper.getApiKeyModel(apiKey); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyMockMapper.java b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyMockMapper.java new file mode 100644 index 000000000..6d37f78df --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyMockMapper.java @@ -0,0 +1,42 @@ +package com.pj.mock; + +import cn.dev33.satoken.apikey.model.ApiKeyModel; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 模拟数据库操作类 + * + * @author click33 + * @since 2025/4/4 + */ +@Component +public class SaApiKeyMockMapper { + + // 添加模拟测试数据 + public static final Map map = new HashMap<>(); + static { + ApiKeyModel ak1 = new ApiKeyModel(); + ak1.setLoginId(10001); // 设置绑定的用户 id + ak1.setApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp"); // 设置 API Key 值 + ak1.setTitle("test"); // 设置名称 + ak1.setExpiresTime(System.currentTimeMillis() + 2592000); // 设置失效时间,13位时间戳,-1=永不失效 + map.put(ak1.getApiKey(), ak1); + + ApiKeyModel ak2 = new ApiKeyModel(); + ak2.setLoginId(10001); // 设置绑定的用户 id + ak2.setApiKey("AK-NxcO63u57zbOWCmLaiVQuVWXssRwAxFcAxcFF"); // 设置 API Key 值 + ak2.setTitle("commit2"); // 设置名称 + ak1.addScope("commit", "pull"); // 设置权限范围 + ak2.setExpiresTime(System.currentTimeMillis() + 2592000); // 设置失效时间,13位时间戳,-1=永不失效 + map.put(ak2.getApiKey(), ak2); + } + + // 返回指定 API Key 对应的 ApiKeyModel + public ApiKeyModel getApiKeyModel(String apiKey) { + return map.get(apiKey); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/GlobalException.java b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/GlobalException.java new file mode 100644 index 000000000..c38ebed3a --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/GlobalException.java @@ -0,0 +1,24 @@ +package com.pj.satoken; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import cn.dev33.satoken.util.SaResult; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..38756c7ac --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,76 @@ +package com.pj.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +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) { + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // 输出 API 请求日志,方便调试代码 + // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行 + .setBeforeAuth(obj -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 允许指定域访问跨域资源 + .setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyController.java b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyController.java new file mode 100644 index 000000000..a3f6d371b --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyController.java @@ -0,0 +1,65 @@ +package com.pj.test; + +import cn.dev33.satoken.apikey.model.ApiKeyModel; +import cn.dev33.satoken.apikey.template.SaApiKeyUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * API Key 相关接口 + * + * @author click33 + */ +@RestController +public class ApiKeyController { + + // 返回当前登录用户拥有的 ApiKey 列表 + @RequestMapping("/myApiKeyList") + public SaResult myApiKeyList() { + List apiKeyList = SaApiKeyUtil.getApiKeyList(StpUtil.getLoginId()); + return SaResult.data(apiKeyList); + } + + // 创建一个新的 ApiKey,并返回 + @RequestMapping("/createApiKey") + public SaResult createApiKey() { + ApiKeyModel akModel = SaApiKeyUtil.createApiKeyModel(StpUtil.getLoginId()).setTitle("test"); + SaApiKeyUtil.saveApiKey(akModel); + return SaResult.data(akModel); + } + + // 修改 ApiKey + @RequestMapping("/updateApiKey") + public SaResult updateApiKey(ApiKeyModel akModel) { + // 先验证一下是否为本人的 ApiKey + SaApiKeyUtil.checkApiKeyLoginId(akModel.getApiKey(), StpUtil.getLoginId()); + // 修改 + ApiKeyModel akModel2 = SaApiKeyUtil.getApiKey(akModel.getApiKey()); + akModel2.setTitle(akModel.getTitle()); + akModel2.setExpiresTime(akModel.getExpiresTime()); + akModel2.setIsValid(akModel.getIsValid()); + akModel2.setScopes(akModel.getScopes()); + SaApiKeyUtil.saveApiKey(akModel2); + return SaResult.ok(); + } + + // 删除 ApiKey + @RequestMapping("/deleteApiKey") + public SaResult deleteApiKey(String apiKey) { + SaApiKeyUtil.checkApiKeyLoginId(apiKey, StpUtil.getLoginId()); + SaApiKeyUtil.deleteApiKey(apiKey); + return SaResult.ok(); + } + + // 删除当前用户所有 ApiKey + @RequestMapping("/deleteMyAllApiKey") + public SaResult deleteMyAllApiKey() { + SaApiKeyUtil.deleteApiKeyByLoginId(StpUtil.getLoginId()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyResourcesController.java b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyResourcesController.java new file mode 100644 index 000000000..a5d01cb64 --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyResourcesController.java @@ -0,0 +1,55 @@ +package com.pj.test; + +import cn.dev33.satoken.annotation.SaMode; +import cn.dev33.satoken.apikey.annotation.SaCheckApiKey; +import cn.dev33.satoken.apikey.model.ApiKeyModel; +import cn.dev33.satoken.apikey.template.SaApiKeyUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * API Key 资源 相关接口 + * + * @author click33 + */ +@RestController +public class ApiKeyResourcesController { + + // 必须携带有效的 ApiKey 才能访问 + @SaCheckApiKey + @RequestMapping("/akRes1") + public SaResult akRes1() { + ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); + System.out.println("当前 ApiKey: " + akModel); + return SaResult.ok("调用成功"); + } + + // 必须携带有效的 ApiKey ,且具有 userinfo 权限 + @SaCheckApiKey(scope = "userinfo") + @RequestMapping("/akRes2") + public SaResult akRes2() { + ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); + System.out.println("当前 ApiKey: " + akModel); + return SaResult.ok("调用成功"); + } + + // 必须携带有效的 ApiKey ,且同时具有 userinfo、chat 权限 + @SaCheckApiKey(scope = {"userinfo", "chat"}) + @RequestMapping("/akRes3") + public SaResult akRes3() { + ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); + System.out.println("当前 ApiKey: " + akModel); + return SaResult.ok("调用成功"); + } + + // 必须携带有效的 ApiKey ,且具有 userinfo、chat 其中之一权限 + @SaCheckApiKey(scope = {"userinfo", "chat"}, mode = SaMode.OR) + @RequestMapping("/akRes4") + public SaResult akRes4() { + ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); + System.out.println("当前 ApiKey: " + akModel); + return SaResult.ok("调用成功"); + } + +} diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..7ec224a56 --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,37 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 登录 Controller + * + * @author click33 + */ +@RestController +public class LoginController { + + // 登录 + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue="10001") String id) { + StpUtil.login(id); + return SaResult.ok().set("satoken", StpUtil.getTokenValue()); + } + + // 查询当前登录人 + @RequestMapping("getLoginId") + public SaResult getLoginId() { + return SaResult.data(StpUtil.getLoginId()); + } + + // 注销 + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-apikey/src/main/resources/application.yml new file mode 100644 index 000000000..26dff4ff5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/resources/application.yml @@ -0,0 +1,43 @@ +# 端口 +server: + port: 8081 + + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # 开启日志信息 + is-log: true + # API Key 相关配置 + api-key: + # API Key 前缀 + prefix: AK- + # API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) + timeout: 2592000 + # 框架是否记录索引信息 + is-record-index: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/resources/static/common.js b/sa-token-demo/sa-token-demo-apikey/src/main/resources/static/common.js new file mode 100644 index 000000000..a0690cb04 --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/resources/static/common.js @@ -0,0 +1,137 @@ +// 服务器接口主机地址 +var baseUrl = "http://localhost:8081"; + +// 封装一下Ajax +function ajax(path, data, successFn, errorFn) { + console.log(baseUrl + path); + fetch(baseUrl + path, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'satoken': localStorage.getItem('satoken') + }, + body: serializeToQueryString(data), + }) + .then(response => response.json()) + .then(res => { + console.log('返回数据:', res); + if(res.code == 200) { + successFn(res); + } else { + if(errorFn) { + errorFn(res); + } else { + showMsg('错误:' + res.msg); + } + } + }) + .catch(error => { + console.error('请求失败:', error); + return alert("异常:" + JSON.stringify(error)); + }); +} + + +// ------------ 工具方法 --------------- + +// 从url中查询到指定名称的参数值 +function getParam(name, defaultValue) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + if (pair[0] == name) { + return pair[1]; + } + } + return (defaultValue == undefined ? null : defaultValue); +} + +// 将 json 对象序列化为kv字符串,形如:name=Joh&age=30&active=true +function serializeToQueryString(obj) { + return Object.entries(obj) + .filter(([_, value]) => value != null) // 过滤 null 和 undefined + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); +} + +// 随机生成字符串 +function randomString(len) { + len = len || 32; + var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'; + var maxPos = $chars.length; + var str = ''; + for (i = 0; i < len; i++) { + str += $chars.charAt(Math.floor(Math.random() * maxPos)); + } + return str; +} + +// 带动画的弹出提示 +function showMsg(message) { + const alertBox = document.createElement('div'); + + // 初始样式(包含隐藏状态) + Object.assign(alertBox.style, { + position: 'fixed', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%) scale(0.8) translateY(-30px)', // 初始缩放+位移 + opacity: '0', + background: 'rgba(0, 0, 0, 0.85)', + color: 'white', + padding: '16px 32px', + borderRadius: '8px', + transition: 'all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55)', // 弹性动画曲线 + pointerEvents: 'none', + whiteSpace: 'nowrap', + fontSize: '16px', + boxShadow: '0 4px 12px rgba(0,0,0,0.25)' // 添加投影增强立体感 + }); + alertBox.textContent = message; + + document.body.appendChild(alertBox); + + // 强制重绘确保动画触发 + void alertBox.offsetHeight; + + // 应用入场动画 + Object.assign(alertBox.style, { + opacity: '1', + transform: 'translate(-50%, -50%) scale(1) translateY(-20px)' + }); + + // 自动消失逻辑 + setTimeout(() => { + Object.assign(alertBox.style, { + opacity: '0', + transform: 'translate(-50%, -50%) scale(0.9) translateY(-20px)' + }); + + alertBox.addEventListener('transitionend', () => { + alertBox.remove(); + }, { + once: true + }); + }, 3000); +} + +// 将日期格式化 yyyy-MM-dd HH:mm:ss +function formatDateTime(date) { + date = new Date(date); + // 补零函数 + const pad = (n, len) => n.toString().padStart(len, '0'); + + // 分解时间组件 + const year = date.getFullYear(); + const month = pad(date.getMonth() + 1, 2); // 0-11 → 1-12 + const day = pad(date.getDate(), 2); + const hours = pad(date.getHours(), 2); // 24小时制 + const minutes = pad(date.getMinutes(), 2); + const seconds = pad(date.getSeconds(), 2); + const milliseconds = pad(date.getMilliseconds(), 3); + + // 拼接格式 + // return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-apikey/src/main/resources/static/index.html b/sa-token-demo/sa-token-demo-apikey/src/main/resources/static/index.html new file mode 100644 index 000000000..313f7e97a --- /dev/null +++ b/sa-token-demo/sa-token-demo-apikey/src/main/resources/static/index.html @@ -0,0 +1,197 @@ + + + + + Title + + + +
+

Sa-Token - API Key 测试页

+

登录

+
当前登录人:
+ 输入账号 id 登录: + + + + +

API Key 列表

+ + + + + + + + + + + + + +
名称API Key权限(多个用逗号隔开)过期时间是否生效操作
+ +

调用 API

+
+ 使用的 API Key: +
+ 需要正确的 API Key
+ 需要具备 Scope: userinfo
+ 需要具备 Scope: userinfo,chat (需要全部具备)
+ 需要具备 Scope: userinfo,chat (具备其一即可)
+
+ +
+ +
+ + + + + + diff --git a/sa-token-demo/sa-token-demo-async/pom.xml b/sa-token-demo/sa-token-demo-async/pom.xml new file mode 100644 index 000000000..64bbfaeee --- /dev/null +++ b/sa-token-demo/sa-token-demo-async/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-async + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + + 1.45.0 + com.pj.SaTokenAsyncApplication + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/SaTokenAsyncApplication.java b/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/SaTokenAsyncApplication.java new file mode 100644 index 000000000..d0b6e1b8c --- /dev/null +++ b/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/SaTokenAsyncApplication.java @@ -0,0 +1,25 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + + +/** + * Sa-Token 异步方案 测试 + * @author click33 + * + */ +@EnableAsync // 启用异步 +@EnableScheduling // 启动定时任务 +@SpringBootApplication +public class SaTokenAsyncApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenAsyncApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..750fc780d --- /dev/null +++ b/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,20 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..17b2f8cdd --- /dev/null +++ b/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,56 @@ +package com.pj.satoken; + +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // 输出 API 请求日志,方便调试代码 + // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }) + + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..2b1cf1732 --- /dev/null +++ b/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,139 @@ +package com.pj.test; + +import cn.dev33.satoken.context.mock.SaTokenContextMockUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; + +/** + * 测试几种场景的异步场景 Controller + * + * @author click33 + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + public ThreadPoolTaskExecutor taskExecutor; + + // 【同步】登录 ---- http://localhost:8081/test/login?id=10001 + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue = "10001") long id) { + StpUtil.login(id); + return SaResult.ok("登录成功"); + } + + // 【同步】判断是否登录 --- http://localhost:8081/test/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + System.out.println("是否登录:" + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenValue()); + } + + // 【同步】注销 浏览器访问: http://localhost:8081/test/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.data(null); + } + + // 【异步】new Thread --- http://localhost:8081/test/isLogin2 + @RequestMapping("isLogin2") + public SaResult isLogin2() { + System.out.println("是否登录:" + StpUtil.isLogin()); + String tokenValue = StpUtil.getTokenValue(); + new Thread(() -> { + SaTokenContextMockUtil.setMockContext(()->{ + StpUtil.setTokenValueToStorage(tokenValue); + System.out.println("是否登录:" + StpUtil.isLogin()); + }); + }).start(); + return SaResult.data(StpUtil.getTokenValue()); + } + + // 【异步】线程池 ThreadPoolTaskExecutor --- http://localhost:8081/test/isLogin3 + @RequestMapping("isLogin3") + public SaResult isLogin3(HttpServletRequest request, HttpServletResponse response) { + System.out.println("是否登录:" + StpUtil.isLogin()); + String tokenValue = StpUtil.getTokenValue(); + taskExecutor.execute(() -> { + SaTokenContextMockUtil.setMockContext(()->{ + StpUtil.setTokenValueToStorage(tokenValue); + System.out.println("是否登录:" + StpUtil.isLogin()); + }); + }); + return SaResult.data(StpUtil.getTokenValue()); + } + + // 【异步】@Async --- http://localhost:8081/test/isLogin4 + @Async + @RequestMapping("isLogin4") + public SaResult isLogin4(@CookieValue("satoken") String satoken) { + SaTokenContextMockUtil.setMockContext(()->{ + StpUtil.setTokenValueToStorage(satoken); + System.out.println("是否登录:" + StpUtil.isLogin()); + }); + return SaResult.ok(); + } + + // 【异步】定时任务 + @Scheduled(cron = "0 * * * * ?") // 一分钟执行一次 +// @Scheduled(cron = "0/10 * * * * ?") // 十秒执行一次 + public void scheduledMethod(){ + // 错误写法:直接调用 Sa-Token API 会报错 + // System.out.println("定时任务,Mock 范围外:是否登录:" + StpUtil.isLogin()); + System.out.println(SaFoxUtil.formatDate(new Date())); + + // 需要先设置模拟上下文 + SaTokenContextMockUtil.setMockContext(() -> { + // StpUtil.setTokenValueToStorage("f452571f-bfdb-413d-aba9-e26992cf07be"); // 模拟 Token + System.out.println("定时任务,Mock 范围内:是否登录:" + StpUtil.isLogin()); + // 模拟登录 +// StpUtil.login(10066); // 模拟 登录 +// System.out.println("定时任务,Mock 范围内:登录账号:" + StpUtil.getLoginId()); + }); + + } + +} + + /* + 使用 InheritableThreadLocal 存储上下文带来的坑: + + @RequestMapping("isLogin2") + public SaResult isLogin2() { + System.out.println("是否登录:" + StpUtil.isLogin()); + + new Thread(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + System.out.println("是否登录:" + StpUtil.isLogin()); + }).start(); + + return SaResult.data(null); + } + + 如果不 Thread.sleep(1000),外面 true,里面 true + 如果 Thread.sleep(1000),则外面 true,里面false + + 因为 SpringBoot 会在请求结束后清除 request 里的数据, + 此时子线程内部可以读取到 request,但是 request 无值,导致代码既能成功运行,又逻辑错误,是一种难以排查的隐形 bug + 应该避免使用 InheritableThreadLocal 来存储上下文数据 + + */ diff --git a/sa-token-demo/sa-token-demo-async/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-async/src/main/resources/application.yml new file mode 100644 index 000000000..95e98258f --- /dev/null +++ b/sa-token-demo/sa-token-demo-async/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-beetl/pom.xml b/sa-token-demo/sa-token-demo-beetl/pom.xml new file mode 100644 index 000000000..b8343db27 --- /dev/null +++ b/sa-token-demo/sa-token-demo-beetl/pom.xml @@ -0,0 +1,61 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-beetl + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.ibeetl + beetl-framework-starter + 1.2.40.Beetl.RELEASE + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-devtools + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + diff --git a/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/SaTokenBeetlDemoApplication.java b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/SaTokenBeetlDemoApplication.java new file mode 100644 index 000000000..116b9f092 --- /dev/null +++ b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/SaTokenBeetlDemoApplication.java @@ -0,0 +1,17 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +@SpringBootApplication +public class SaTokenBeetlDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenBeetlDemoApplication.class, args); + System.out.println("\n启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); + System.out.println("\n测试访问:http://localhost:8081/"); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..5425fcf37 --- /dev/null +++ b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,25 @@ +package com.pj.satoken; + +import cn.dev33.satoken.stp.StpUtil; +import com.ibeetl.starter.BeetlTemplateCustomize; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + + +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + * + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + + // 为 Beetl 视图引擎注册自定义函数: + // 通过 stp.xxx() 调用 StpUtil.stpLogic 对象上所有 public 方法 + @Bean + public BeetlTemplateCustomize beetlTemplateCustomize(){ + return groupTemplate -> groupTemplate.registerFunctionPackage("stp", StpUtil.stpLogic); + } + +} diff --git a/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..b6cc79f71 --- /dev/null +++ b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/GlobalException.java new file mode 100644 index 000000000..16b295e72 --- /dev/null +++ b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/GlobalException.java @@ -0,0 +1,23 @@ +package com.pj.test; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..f1b253706 --- /dev/null +++ b/sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,39 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +/** + * 测试 Controller + * @author click33 + * + */ +@RestController +public class TestController { + + // 首页 + @RequestMapping("/") + public Object index() { + return new ModelAndView("index.btl"); + } + + // 登录 + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue="10001") String id) { + StpUtil.login(id); + StpUtil.getSession().set("name", "zhangsan"); + return SaResult.ok(); + } + + // 注销 + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-beetl/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-beetl/src/main/resources/application.yml new file mode 100644 index 000000000..aac39af7f --- /dev/null +++ b/sa-token-demo/sa-token-demo-beetl/src/main/resources/application.yml @@ -0,0 +1,5 @@ +# 端口 +server: + port: 8081 + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-beetl/src/main/resources/templates/index.btl b/sa-token-demo/sa-token-demo-beetl/src/main/resources/templates/index.btl new file mode 100644 index 000000000..d0e58aafe --- /dev/null +++ b/sa-token-demo/sa-token-demo-beetl/src/main/resources/templates/index.btl @@ -0,0 +1,50 @@ + + + + Sa-Token 集成 Beetl 示例 + + + + +
+

Sa-Token 集成 Beetl —— 测试页面

+

当前是否登录:${stp.isLogin()}

+

+ 操作: + 登录 + 注销 +

+ +

+ 登录之后才能显示: + <% if(stp.isLogin()){ %> + value + <%}%> +

+

+ 不登录才能显示: + <% if(!stp.isLogin()){ %> + value + <%}%> +

+ +

具有角色 admin 才能显示:<% if(stp.hasRole("admin")){ %> value <% } %>

+

同时具备多个角色才能显示:<% if(stp.hasRoleAnd("admin", "ceo", "cto")){ %> value <% } %>

+

只要具有其中一个角色就能显示:<% if(stp.hasRoleOr("admin", "ceo", "cto")){ %> value <% } %>

+

不具有角色 admin 才能显示:<% if(!stp.hasRole("admin")){ %> value <% } %>

+ +

具有权限 user-add 才能显示:<% if(stp.hasPermission("user-add")){ %> value <% } %>

+

同时具备多个权限才能显示:<% if(stp.hasPermissionAnd("user-add", "user-delete", "user-get")){ %> value <% } %>

+

只要具有其中一个权限就能显示:<% if(stp.hasPermissionOr("user-add", "user-delete", "user-get")){ %> value <% } %>

+

不具有权限 user-add 才能显示:<% if(!stp.hasPermission("user-add")){ %> value <% } %>

+ + <% if(stp.isLogin()){ %> +

+ 从SaSession中取值:${stp.getSession()["name"]}
+ 或:${stp.getSession().name} +

+ <%}%> + +
+ + diff --git a/sa-token-demo/sa-token-demo-bom-import/pom.xml b/sa-token-demo/sa-token-demo-bom-import/pom.xml new file mode 100644 index 000000000..1daf47d2b --- /dev/null +++ b/sa-token-demo/sa-token-demo-bom-import/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-bom-import + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + + + + + cn.dev33 + sa-token-redis-jackson + + + + + cn.dev33 + sa-token-jwt + + + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + + + + + + cn.dev33 + sa-token-bom + 1.45.0 + pom + import + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/SaTokenDemoApplication.java b/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/SaTokenDemoApplication.java new file mode 100644 index 000000000..9ae761b97 --- /dev/null +++ b/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/SaTokenDemoApplication.java @@ -0,0 +1,27 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.jwt.StpLogicJwtForSimple; +import cn.dev33.satoken.stp.StpLogic; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +/** + * Sa-Token 使用 bom 包引入框架 + */ +@SpringBootApplication +public class SaTokenDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenDemoApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + + // Sa-Token 整合 jwt (Simple 简单模式) + @Bean + public StpLogic getStpLogicJwt() { + return new StpLogicJwtForSimple(); + } + +} diff --git a/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..6b8047612 --- /dev/null +++ b/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,20 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e){ + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..26fc75fbd --- /dev/null +++ b/sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,32 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + // 测试 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("------------进来了"); + StpUtil.login(10001); + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8081/test/test2 + @RequestMapping("test2") + public SaResult test2() { + StpUtil.checkLogin(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-bom-import/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-bom-import/src/main/resources/application.yml new file mode 100644 index 000000000..9be95cfb9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-bom-import/src/main/resources/application.yml @@ -0,0 +1,52 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + # jwt秘钥 + jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk + + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-bug-reproduce/README.md b/sa-token-demo/sa-token-demo-bug-reproduce/README.md new file mode 100644 index 000000000..a9c52cf61 --- /dev/null +++ b/sa-token-demo/sa-token-demo-bug-reproduce/README.md @@ -0,0 +1,45 @@ +# sa-token-demo-bug-reproduce + +专门用来**复现 Issue / Bug** 的沙盒 Demo。 +社区报了问题、本地要验证修复或回归时,在这个模块里改代码跑,避免污染正式示例。 + +## 使用约定 + +1. **一个时期通常只保留当前正在查的用例**(旧用例可注释掉或写在 README「历史用例」里) +2. 按目标环境改 `pom.xml` 的 Boot 版本 / starter(Web / WebFlux / Gateway 等) +3. 入口类:`BugReproduceApplication` +4. 默认端口:`8092` + +## 当前用例:setStatus NoSuchMethodError(已修) + +关联: + +- Gitee [IIAW1A](https://gitee.com/dromara/sa-token/issues/IIAW1A) +- GitHub [#916](https://github.com/dromara/Sa-Token/issues/916) +- 提案 `#92` / `#221` + +现象(修复前):Boot 3.5 + WebFlux 下 `SaHolder.getResponse().setStatus(401)` → +`NoSuchMethodError: ServerHttpResponse.setStatusCode(HttpStatus)` + +根因:Reactor common 按 Spring 5 编译,高版本运行签名不兼容。 +修复:Boot2 实现收在 `sa-token-reactor-spring-boot-starter`,Boot3/4 共用 `reactor-v3v4-common`(`HttpStatusCode`)。 + +### 怎么跑当前用例 + +```bash +# IDEA 运行 BugReproduceApplication,或: +mvn -DskipTests spring-boot:run +``` + +```bash +curl -i http://127.0.0.1:8092/repro/direct-set-status +curl -i http://127.0.0.1:8092/basic/ping +``` + +期望(修复后):返回 HTTP 401,进程不崩、无 `NoSuchMethodError`。 + +## 历史用例 + +| 日期 | Issue | 摘要 | 状态 | +|------|-------|------|------| +| 2026-08 | IIAW1A / #916 | WebFlux setStatus 二进制不兼容 | 已修 | diff --git a/sa-token-demo/sa-token-demo-bug-reproduce/pom.xml b/sa-token-demo/sa-token-demo-bug-reproduce/pom.xml new file mode 100644 index 000000000..f6d70e9d8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-bug-reproduce/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + cn.dev33 + sa-token-demo-bug-reproduce + 0.0.1-SNAPSHOT + Sa-Token Issue / Bug 复现专用 Demo(按需改代码验证) + + + + org.springframework.boot + spring-boot-starter-parent + 3.5.11 + + + + + 1.45.0 + 17 + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + cn.dev33 + sa-token-reactor-spring-boot3-starter + ${sa-token.version} + + + + diff --git a/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/BugReproduceApplication.java b/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/BugReproduceApplication.java new file mode 100644 index 000000000..ee7b2449f --- /dev/null +++ b/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/BugReproduceApplication.java @@ -0,0 +1,19 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Issue / Bug 复现入口。 + *

+ * 本模块专门用来复现社区 Issue 上提到的问题,便于本地验证与回归。 + */ +@SpringBootApplication +public class BugReproduceApplication { + + public static void main(String[] args) { + SpringApplication.run(BugReproduceApplication.class, args); + System.out.println("\n启动成功。当前复现用例见 README / TestController。"); + } + +} diff --git a/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..6fe2d836f --- /dev/null +++ b/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,32 @@ +package com.pj.satoken; + +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.reactor.filter.SaReactorFilter; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 当前用例(IIAW1A / #916):SaReactorFilter + SaHttpBasicUtil.check 失败时会调 setStatus(401) + */ +@Configuration +public class SaTokenConfigure { + + @Bean + public SaReactorFilter getSaReactorFilter() { + return new SaReactorFilter() + .addInclude("/**") + .addExclude("/favicon.ico", "/ok") + .setAuth(obj -> { + // 访问 /basic/** 时走 Http Basic;无凭证 → setStatus(401) + SaRouter.match("/basic/**", () -> SaHttpBasicUtil.check("sa:123456")); + }) + .setError(e -> { + System.out.println("---------- sa全局异常: " + e); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }); + } + +} diff --git a/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..7104ef089 --- /dev/null +++ b/sa-token-demo/sa-token-demo-bug-reproduce/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,43 @@ +package com.pj.test; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.reactor.context.SaReactorHolder; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +/** + * 当前复现用例的入口(随 Issue 更换可改这里) + */ +@RestController +public class TestController { + + /** 健康检查(过滤器放行) --- http://localhost:8092/ok */ + @RequestMapping("/ok") + public SaResult ok() { + return SaResult.ok("alive"); + } + + /** + * 用例 A:直接调 setStatus + * --- http://localhost:8092/repro/direct-set-status + */ + @RequestMapping("/repro/direct-set-status") + public Mono directSetStatus() { + return SaReactorHolder.sync(() -> { + SaHolder.getResponse().setStatus(401); + return SaResult.ok("setStatus 已执行"); + }); + } + + /** + * 用例 B:Http Basic 校验失败内部会 setStatus(401) + * --- curl -i http://localhost:8092/basic/ping + */ + @RequestMapping("/basic/ping") + public SaResult basicPing() { + return SaResult.ok("basic ok"); + } + +} diff --git a/sa-token-demo/sa-token-demo-bug-reproduce/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-bug-reproduce/src/main/resources/application.yml new file mode 100644 index 000000000..9a82580e3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-bug-reproduce/src/main/resources/application.yml @@ -0,0 +1,6 @@ +server: + port: 8092 + +sa-token: + token-name: satoken + is-log: true diff --git a/sa-token-demo/sa-token-demo-caffeine/pom.xml b/sa-token-demo/sa-token-demo-caffeine/pom.xml new file mode 100644 index 000000000..e82bb44be --- /dev/null +++ b/sa-token-demo/sa-token-demo-caffeine/pom.xml @@ -0,0 +1,59 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-caffeine + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-caffeine + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/SaTokenDemoApplication.java b/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/SaTokenDemoApplication.java new file mode 100644 index 000000000..105a522d8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/SaTokenDemoApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 Caffeine 示例 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenDemoApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + System.out.println(SaManager.getSaTokenDao()); + } + +} diff --git a/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..750fc780d --- /dev/null +++ b/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,20 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..34eea9a19 --- /dev/null +++ b/sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-caffeine/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-caffeine/src/main/resources/application.yml new file mode 100644 index 000000000..6ae97b86a --- /dev/null +++ b/sa-token-demo/sa-token-demo-caffeine/src/main/resources/application.yml @@ -0,0 +1,23 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-case/pom.xml b/sa-token-demo/sa-token-demo-case/pom.xml new file mode 100644 index 000000000..71277928e --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/pom.xml @@ -0,0 +1,79 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-case + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + + + cn.dev33 + sa-token-spring-el + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..9f7d50500 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/SaTokenCaseApplication.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 示例 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenCaseApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenCaseApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/more/SaCheckELController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/more/SaCheckELController.java new file mode 100644 index 000000000..b0aa130f7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/more/SaCheckELController.java @@ -0,0 +1,102 @@ +package com.pj.cases.more; + +import cn.dev33.satoken.annotation.SaCheckEL; +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * SaCheckEL EL表达式注解鉴权示例 + * + * @author click33 + * @since 2022-10-13 + */ +@RestController +@RequestMapping("/check-el/") +public class SaCheckELController { + + // 登录校验 ---- http://localhost:8081/check-el/test1 + @SaCheckEL("stp.checkLogin()") + @RequestMapping("test1") + public SaResult test1() { + return SaResult.ok(); + } + + // 角色校验 ---- http://localhost:8081/check-el/test2 + @SaCheckEL("stp.checkRole('dev-admin')") + @RequestMapping("test2") + public SaResult test2() { + return SaResult.ok(); + } + + // 权限校验 ---- http://localhost:8081/check-el/test3 + @SaCheckEL("stp.checkPermission('user:edit')") + @RequestMapping("test3") + public SaResult test3() { + return SaResult.ok(); + } + + // 二级认证 ---- http://localhost:8081/check-el/test4 + @SaCheckEL("stp.checkSafe()") + @RequestMapping("test4") + public SaResult test4() { + return SaResult.ok(); + } + + // 参数长度校验 ---- http://localhost:8081/check-el/test5?name=zhangsan + @SaCheckEL("NEED( #name.length() > 3 )") + @RequestMapping("test5") + public SaResult test5(@RequestParam(defaultValue = "") String name) { + return SaResult.ok().set("name", name); + } + + // 参数长度校验,并自定义异常描述信息 ---- http://localhost:8081/check-el/test6?name=z + @SaCheckEL("NEED( #name !=null && #name.length() > 3, 'name长度不够' )") + @RequestMapping("test6") + public SaResult test6(String name) { + return SaResult.ok().set("name", name); + } + + // 已登录, 或者查询数据在公开范围内 ---- http://localhost:8081/check-el/test7?id=10044 + @SaCheckEL("NEED( stp.isLogin() or (#id != null and #id > 10010) )") + @RequestMapping("test7") + public SaResult test7(long id) { + return SaResult.ok().set("id", id); + } + + // SaSession 里取值校验 ---- http://localhost:8081/check-el/test8 + @SaCheckEL("NEED( stp.getSession().get('name') == 'zhangsan' )") + @RequestMapping("test8") + public SaResult test8() { + return SaResult.ok(); + } + + // 多账号体系鉴权测试 ---- http://localhost:8081/check-el/test9 + @SaCheckEL("stpUser.checkLogin()") + @RequestMapping("test9") + public SaResult test9() { + return SaResult.ok(); + } + + // 本模块需要鉴权的权限码 + public String permissionCode = "article:add"; + + // 调用本类的成员变量 ---- http://localhost:8081/check-el/test10 + @SaCheckEL("stp.checkPermission( this.permissionCode )") + @RequestMapping("test10") + public SaResult test10() { + return SaResult.ok(); + } + + // 忽略鉴权测试 ---- http://localhost:8081/check-el/test11 + @SaIgnore + @SaCheckEL("stp.checkPermission( 'abc' )") + @RequestMapping("test11") + public SaResult test11() { + return SaResult.ok(); + } + + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/plugin/TempTokenController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/plugin/TempTokenController.java new file mode 100644 index 000000000..73e43e74f --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/plugin/TempTokenController.java @@ -0,0 +1,69 @@ +package com.pj.cases.plugin; + +import cn.dev33.satoken.temp.SaTempUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 测试专用 Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/temp-token/") +public class TempTokenController { + + // 创建 浏览器访问: http://localhost:8081/temp-token/create + @RequestMapping("create") + public SaResult create() { + String token = SaTempUtil.createToken(10001, 1200); + System.out.println("创建成功:" + token); + return SaResult.data(token); + } + + // 创建,获取时裁剪前缀 浏览器访问: http://localhost:8081/temp-token/create2 + @RequestMapping("create2") + public SaResult create2() { + String token = SaTempUtil.createToken("shop_" + 1001, 1200); + System.out.println("创建成功:" + token); + System.out.println("获取对应值:" + SaTempUtil.parseToken(token)); + System.out.println("获取对应值,并裁剪前缀:" + SaTempUtil.parseToken(token, "shop_", Long.class)); + System.out.println("指定错误前缀来获取:" + SaTempUtil.parseToken(token, "art_", Long.class)); + return SaResult.data(token); + } + + // 创建,回收 浏览器访问: http://localhost:8081/temp-token/create3 + @RequestMapping("create3") + public SaResult create3() { + String token = SaTempUtil.createToken(10003, 1200); + System.out.println("创建成功:" + token); + System.out.println("获取对应值:" + SaTempUtil.parseToken(token)); + SaTempUtil.deleteToken(token); + System.out.println("回收后再获取:" + SaTempUtil.parseToken(token)); + return SaResult.data(token); + } + + // 创建,记录索引 浏览器访问: http://localhost:8081/temp-token/create4 + @RequestMapping("create4") + public SaResult create4() { + String token1 = SaTempUtil.createToken(10004, 1200, true); + String token2 = SaTempUtil.createToken(10004, 1300, true); + String token3 = SaTempUtil.createToken(10004, -1, true); + + System.out.println("token1 剩余有效期:" + SaTempUtil.getTimeout(token1)); + System.out.println("token2 剩余有效期:" + SaTempUtil.getTimeout(token2)); + System.out.println("token3 剩余有效期:" + SaTempUtil.getTimeout(token3)); + + SaTempUtil.deleteToken(token3); + + // 获取已创建的 token 列表 + System.out.println("获取已创建的 token 列表 "); + List tempTokenList = SaTempUtil.getTempTokenList(10004); + System.out.println(tempTokenList); + return SaResult.data(token1); + } + +} 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 new file mode 100644 index 000000000..da5aa8eb1 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/test/TestController.java @@ -0,0 +1,30 @@ +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; + +/** + * 测试专用 Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + // 测试 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("------------进来了"); + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8081/test/test2 + @RequestMapping("test2") + public SaResult test2() { + System.out.println("------------进来了"); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/DisableController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/DisableController.java new file mode 100644 index 000000000..f73666472 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/DisableController.java @@ -0,0 +1,65 @@ +package com.pj.cases.up; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 账号封禁示例 + * + * @author click33 + * @since 2022-10-17 + */ +@RestController +@RequestMapping("/disable/") +public class DisableController { + + /* + * 测试步骤: + 1、访问登录接口,可以正常登录 ---- http://localhost:8081/disable/login?userId=10001 + 2、注销登录 ---- http://localhost:8081/disable/logout + 3、禁用账号 ---- http://localhost:8081/disable/disable?userId=10001 + 4、再次访问登录接口,登录失败 ---- http://localhost:8081/disable/login?userId=10001 + 5、解封账号 ---- http://localhost:8081/disable/untieDisable?userId=10001 + 6、再次访问登录接口,登录成功 ---- http://localhost:8081/disable/login?userId=10001 + */ + + // 会话登录接口 ---- http://localhost:8081/disable/login?userId=10001 + @RequestMapping("login") + public SaResult login(long userId) { + // 1、先检查此账号是否已被封禁 + StpUtil.checkDisable(userId); + // 2、检查通过后,再登录 + StpUtil.login(userId); + return SaResult.ok("账号登录成功"); + } + + // 会话注销接口 ---- http://localhost:8081/disable/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok("账号退出成功"); + } + + // 封禁指定账号 ---- http://localhost:8081/disable/disable?userId=10001 + @RequestMapping("disable") + public SaResult disable(long userId) { + /* + * 账号封禁: + * 参数1:要封禁的账号id + * 参数2:要封禁的时间,单位:秒,86400秒=1天 + */ + StpUtil.disable(userId, 86400); + return SaResult.ok("账号 " + userId + " 封禁成功"); + } + + // 解封指定账号 ---- http://localhost:8081/disable/untieDisable?userId=10001 + @RequestMapping("untieDisable") + public SaResult untieDisable(long userId) { + StpUtil.untieDisable(userId); + return SaResult.ok("账号 " + userId + " 解封成功"); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/HttpBasicController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/HttpBasicController.java new file mode 100644 index 000000000..6be116998 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/HttpBasicController.java @@ -0,0 +1,37 @@ +package com.pj.cases.up; + +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Sa-Token Http Basic 认证 + * + * @author click33 + * @since 2022-10-17 + */ +@RestController +@RequestMapping("/basic/") +public class HttpBasicController { + + /* + * 测试步骤: + 1、访问资源接口,被拦截,无法返回数据信息 ---- http://localhost:8081/basic/getInfo + 2、浏览器弹出窗口,要求输入账号密码,输入:账号=sa,密码=123456,确认 + 3、后端返回数据信息 + 4、后续再次访问接口时,无需重复输入账号密码 + */ + + // 资源接口 ---- http://localhost:8081/basic/getInfo + @RequestMapping("getInfo") + public SaResult login() { + // 1、Http Basic 认证校验,账号=sa,密码=123456 + SaHttpBasicUtil.check("sa:123456"); + + // 2、返回数据 + String data = "这是通过 Http Basic 校验后才返回的数据"; + return SaResult.data(data); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java new file mode 100644 index 000000000..28322e2ba --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java @@ -0,0 +1,67 @@ +package com.pj.cases.up; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Sa-Token 同端互斥登录示例 + * + * @author click33 + * @since 2022-10-17 + */ +@RestController +@RequestMapping("/mutex/") +public class MutexLoginController { + + /* + * 前提1:准备至少四个浏览器:A、B、C、D + * 前提2:配置文件需要把 sa-token.is-concurrent 的值改为 false + * + * 测试步骤: + 1、在浏览器A上登录账号10001,设备为PC ---- http://localhost:8081/mutex/login?userId=10001&device=PC + 检查是否登录成功,返回true: ---- http://localhost:8081/mutex/isLogin + + 2、在浏览器B上登录账号10001,设备为APP ---- http://localhost:8081/mutex/login?userId=10001&device=APP + 检查是否登录成功,返回true: ---- http://localhost:8081/mutex/isLogin + + 3、复查一下览器A上的账号是否登录,发现并没有顶替下去,原因是两个浏览器指定的登录设备不同,允许同时在线 + ---- http://localhost:8081/mutex/isLogin + + 4、在浏览器C上登录账号10001,设备为PC ---- http://localhost:8081/mutex/login?userId=10001&device=PC + 检查是否登录成功,返回true: ---- http://localhost:8081/mutex/isLogin + + 5、复查一下浏览器A上的账号是否登录,发现账号已被顶替下线 + ---- http://localhost:8081/mutex/isLogin + + 6、再复查一下浏览器B上的账号是否登录,发现账号未被顶替下线,因为浏览器B上登录的设备是APP,而浏览器C顶替的设备是PC + ---- http://localhost:8081/mutex/isLogin + + 7、此时再从浏览器D上登录账号10001,设备为APP ---- http://localhost:8081/mutex/login?userId=10001&device=APP + 检查是否登录成功,返回true: ---- http://localhost:8081/mutex/isLogin + + 8、此时再复查一下浏览器B上的账号是否登录,发现账号已被顶替下线 + ---- http://localhost:8081/mutex/isLogin + */ + + // 会话登录接口 ---- http://localhost:8081/mutex/doLogin?userId=10001&device=PC + @RequestMapping("login") + public SaResult login(long userId, String device) { + /* + * 参数1:要登录的账号 + * 参数2:此账号在什么设备上登录的 + */ + StpUtil.login(userId, device); + return SaResult.ok("登录成功"); + } + + // 查询当前登录状态 ---- http://localhost:8081/mutex/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + // StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false + boolean isLogin = StpUtil.isLogin(); + return SaResult.ok("当前客户端是否登录:" + isLogin + ",登录的设备是:" + StpUtil.getLoginDeviceType()); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/NotCookieController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/NotCookieController.java new file mode 100644 index 000000000..242d3a9f9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/NotCookieController.java @@ -0,0 +1,48 @@ +package com.pj.cases.up; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 前后端分离模式示例 + * + * @author click33 + * @since 2022-10-17 + */ +@RestController +@RequestMapping("/NotCookie/") +public class NotCookieController { + + // 前后端一体模式的登录样例 ---- http://localhost:8081/NotCookie/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + if("zhang".equals(name) && "123456".equals(pwd)) { + // 会话登录 + StpUtil.login(10001); + return SaResult.ok(); + } + return SaResult.error("登录失败"); + } + + // 前后端分离模式的登录样例 ---- http://localhost:8081/NotCookie/doLogin2?name=zhang&pwd=123456 + @RequestMapping("doLogin2") + public SaResult doLogin2(String name, String pwd) { + + if("zhang".equals(name) && "123456".equals(pwd)) { + + // 会话登录 + StpUtil.login(10001); + + // 与常规登录不同点之处:这里需要把 Token 信息从响应体中返回到前端 + SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); + return SaResult.data(tokenInfo); + } + return SaResult.error("登录失败"); + } + +} + diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/RememberMeController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/RememberMeController.java new file mode 100644 index 000000000..677a7fa86 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/RememberMeController.java @@ -0,0 +1,49 @@ +package com.pj.cases.up; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 记住我模式登录 + * + * @author click33 + * @since 2022-10-17 + */ +@RestController +@RequestMapping("/RememberMe/") +public class RememberMeController { + + // 记住我登录 ---- http://localhost:8081/RememberMe/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001, true); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 不记住我登录 ---- http://localhost:8081/RememberMe/doLogin2?name=zhang&pwd=123456 + @RequestMapping("doLogin2") + public SaResult doLogin2(String name, String pwd) { + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001, false); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 七天免登录 ---- http://localhost:8081/RememberMe/doLogin3?name=zhang&pwd=123456 + @RequestMapping("doLogin3") + public SaResult doLogin3(String name, String pwd) { + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001, 60 * 60 * 24 * 7); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SafeAuthController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SafeAuthController.java new file mode 100644 index 000000000..cfbedc654 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SafeAuthController.java @@ -0,0 +1,107 @@ +package com.pj.cases.up; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 二级认证示例 + * + * @author click33 + * @since 2022-10-16 + */ +@RestController +@RequestMapping("/safe/") +public class SafeAuthController { + + /* + * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 + * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + * + * 测试步骤: + 1、前端调用 deleteProject 接口,尝试删除仓库。 ---- http://localhost:8081/safe/deleteProject + 2、后端校验会话尚未完成二级认证,返回: 仓库删除失败,请完成二级认证后再次访问接口。 + 3、前端将信息提示给用户,用户输入密码,调用 openSafe 接口。 ---- http://localhost:8081/safe/openSafe?password=123456 + 4、后端比对用户输入的密码,完成二级认证,有效期为:120秒。 + 5、前端在 120 秒内再次调用 deleteProject 接口,尝试删除仓库。 ---- http://localhost:8081/safe/deleteProject + 6、后端校验会话已完成二级认证,返回:仓库删除成功。 + */ + + // 删除仓库 ---- http://localhost:8081/safe/deleteProject + @RequestMapping("deleteProject") + public SaResult deleteProject(String projectId) { + // 第1步,先检查当前会话是否已完成二级认证 + // 这个地方既可以通过 StpUtil.isSafe() 手动判断, + // 也可以通过 StpUtil.checkSafe() 或者 @SaCheckSafe 来校验(校验不通过时将抛出 NotSafeException 异常) + if(!StpUtil.isSafe()) { + return SaResult.error("仓库删除失败,请完成二级认证后再次访问接口"); + } + + // 第2步,如果已完成二级认证,则开始执行业务逻辑 + // ... + + // 第3步,返回结果 + return SaResult.ok("仓库删除成功"); + } + + // 提供密码进行二级认证 ---- http://localhost:8081/safe/openSafe?password=123456 + @RequestMapping("openSafe") + public SaResult openSafe(String password) { + // 比对密码(此处只是举例,真实项目时可拿其它参数进行校验) + if("123456".equals(password)) { + + // 比对成功,为当前会话打开二级认证,有效期为120秒,意为在120秒内再调用 deleteProject 接口都无需提供密码 + StpUtil.openSafe(120); + return SaResult.ok("二级认证成功"); + } + + // 如果密码校验失败,则二级认证也会失败 + return SaResult.error("二级认证失败"); + } + + // 手动关闭二级认证 ---- http://localhost:8081/safe/closeSafe + @RequestMapping("closeSafe") + public SaResult closeSafe() { + StpUtil.closeSafe(); + return SaResult.ok(); + } + + + // ------------------ 指定业务类型进行二级认证 + + // 获取应用秘钥 ---- http://localhost:8081/safe/getClientSecret + @RequestMapping("getClientSecret") + public SaResult getClientSecret() { + // 第1步,先检查当前会话是否已完成 client业务 的二级认证 + StpUtil.checkSafe("client"); + + // 第2步,如果已完成二级认证,则返回数据 + return SaResult.data("aaaa-bbbb-cccc-dddd-eeee"); + } + + // 提供手势密码进行二级认证 ---- http://localhost:8081/safe/openClientSafe?gesture=35789 + @RequestMapping("openClientSafe") + public SaResult openClientSafe(String gesture) { + // 比对手势密码(此处只是举例,真实项目时可拿其它参数进行校验) + if("35789".equals(gesture)) { + + // 比对成功,为当前会话打开二级认证: + // 业务类型为:client + // 有效期为600秒==10分钟,意为在10分钟内,调用 getClientSecret 时都无需再提供手势密码 + StpUtil.openSafe("client", 600); + return SaResult.ok("二级认证成功"); + } + + // 如果密码校验失败,则二级认证也会失败 + return SaResult.error("二级认证失败"); + } + + // 查询当前会话是否已完成指定的二级认证 ---- http://localhost:8081/safe/isClientSafe + @RequestMapping("isClientSafe") + public SaResult isClientSafe() { + return SaResult.ok("当前是否已完成 client 二级认证:" + StpUtil.isSafe("client")); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SearchSessionController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SearchSessionController.java new file mode 100644 index 000000000..bef5116f5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SearchSessionController.java @@ -0,0 +1,79 @@ +package com.pj.cases.up; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.pj.model.SysUser; + +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 会话查询示例 + * + * @author click33 + * @since 2022-10-17 + */ +@RestController +@RequestMapping("/search/") +public class SearchSessionController { + + /* + * 测试步骤: + 1、先登录5个账号 + ---- http://localhost:8081/search/login?userId=10001&张三&age=18 + ---- http://localhost:8081/search/login?userId=10002&李四&age=20 + ---- http://localhost:8081/search/login?userId=10003&王五&age=22 + ---- http://localhost:8081/search/login?userId=10004&赵六&age=24 + ---- http://localhost:8081/search/login?userId=10005&冯七&age=26 + + 2、根据分页参数获取会话列表 + http://localhost:8081/search/getList?start=0&size=10 + */ + + // 会话登录接口 ---- http://localhost:8081/search/login?userId=10001&张三&age=18 + @RequestMapping("login") + public SaResult login(long userId, String name, int age) { + // 先登录上 + StpUtil.login(userId); + + // 再把 User 对象存储在 SaSession 上 + SysUser user = new SysUser(); + user.setId(userId); + user.setName(name); + user.setAge(age); + StpUtil.getSession().set("user", user); + + // 返回 + return SaResult.ok("账号登录成功"); + } + + // 会话查询接口 ---- http://localhost:8081/search/getList?start=0&size=10 + @RequestMapping("getList") + public SaResult getList(int start, int size) { + // 创建集合 + List sessionList = new ArrayList<>(); + + // 分页查询数据 + List sessionIdList = StpUtil.searchSessionId("", start, size, false); + for (String sessionId: sessionIdList) { + SaSession session = StpUtil.getSessionBySessionId(sessionId); + sessionList.add(session); + } + + // 返回 + return SaResult.data(sessionList); + } + + // 会话查询接口 ---- http://localhost:8081/disable/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + 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 new file mode 100644 index 000000000..a4ec5761d --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SecureController.java @@ -0,0 +1,91 @@ +package com.pj.cases.up; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.secure.SaBase64Util; +import cn.dev33.satoken.secure.SaSecureUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 密码加密示例 + * + * @author click33 + * @since 2022-10-17 + */ +@RestController +@RequestMapping("/secure/") +public class SecureController { + + // 摘要加密 ---- http://localhost:8081/secure/digest + @RequestMapping("digest") + public SaResult digest() { + // md5加密 + System.out.println(SaSecureUtil.md5("123456")); + + // sha1加密 + System.out.println(SaSecureUtil.sha1("123456")); + + // sha256加密 + System.out.println(SaSecureUtil.sha256("123456")); + + return SaResult.ok(); + } + + // AES加密 ---- http://localhost:8081/secure/aes + @RequestMapping("aes") + public SaResult aes() { + // 定义秘钥和明文 + String key = "123456"; + String text = "Sa-Token 一个轻量级java权限认证框架"; + + // 加密 + String ciphertext = SaSecureUtil.aesEncrypt(key, text); + System.out.println("AES加密后:" + ciphertext); + + // 解密 + String text2 = SaSecureUtil.aesDecrypt(key, ciphertext); + System.out.println("AES解密后:" + text2); + + return SaResult.ok(); + } + + // 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(); +// } + + // Base64 编码 ---- http://localhost:8081/secure/base64 + @RequestMapping("base64") + public SaResult base64() { + // 文本 + String text = "Sa-Token 一个轻量级java权限认证框架"; + + // 使用Base64编码 + String base64Text = SaBase64Util.encode(text); + System.out.println("Base64编码后:" + base64Text); + + // 使用Base64解码 + String text2 = SaBase64Util.decode(base64Text); + System.out.println("Base64解码后:" + text2); + + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SwitchToController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SwitchToController.java new file mode 100644 index 000000000..1d7fab636 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SwitchToController.java @@ -0,0 +1,63 @@ +package com.pj.cases.up; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 身份切换 + * + * @author click33 + * @since 2022-10-17 + */ +@RestController +@RequestMapping("/SwitchTo/") +public class SwitchToController { + + /* + * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 + * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + */ + + // 身份切换 ---- http://localhost:8081/SwitchTo/switchTo?userId=10044 + @RequestMapping("switchTo") + public SaResult switchTo(long userId) { + // 将当前会话 [身份临时切换] 为其它账号 + // --- 切换的有效期为本次请求 + StpUtil.switchTo(userId); + + // 此时再调用此方法会返回 10044 (我们临时切换到的账号id) + System.out.println(StpUtil.getLoginId());; + + // 结束 [身份临时切换] + StpUtil.endSwitch(); + + // 此时再打印账号,就又回到了原来的值:10001 + System.out.println(StpUtil.getLoginId()); + + return SaResult.ok(); + } + + // 以 lambda 表达式的方式身份切换 ---- http://localhost:8081/SwitchTo/switchTo2?userId=10044 + @RequestMapping("switchTo2") + public SaResult switchTo2(long userId) { + + // 输出 10001 + System.out.println("------- [身份临时切换] 调用前,当前登录账号id是:" + StpUtil.getLoginId()); + + // 以 lambda 表达式的方式身份切换,作用范围只在这个 lambda 表达式内有效 + StpUtil.switchTo(userId, () -> { + System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); // 输出 true + System.out.println("获取当前登录账号id: " + StpUtil.getLoginId()); // 输出 10044 + }); + + // 结束后,再次获取当前登录账号,输出10001 + System.out.println("------- [身份临时切换] 调用结束,当前登录账号id是:" + StpUtil.getLoginId()); + + return SaResult.ok(); + } + +} + diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/AtCheckController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/AtCheckController.java new file mode 100644 index 000000000..d37ac801b --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/AtCheckController.java @@ -0,0 +1,95 @@ +package com.pj.cases.use; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.annotation.SaMode; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 注解鉴权示例 + * + * @author click33 + * @since 2022-10-13 + */ +@RestController +@RequestMapping("/at-check/") +public class AtCheckController { + + /* + * 前提1:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 + * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + * + * 前提2:项目在配置类中注册拦截器 SaInterceptor ,代码在 com.pj.satoken.SaTokenConfigure + * 此拦截器将打开注解鉴权功能 + * + * 然后我们就可以使用以下示例中的代码进行注解鉴权了 + */ + + // 登录鉴权 ---- http://localhost:8081/at-check/checkLogin + // 登录认证后才可以进入方法 + @SaCheckLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + // 通过注解鉴权后才能进入方法 ... + return SaResult.ok(); + } + + // 权限校验 ---- http://localhost:8081/at-check/checkPermission + // 只有具有 user.add 权限的账号才可以进入方法 + @SaCheckPermission("user.add") + @RequestMapping("checkPermission") + public SaResult checkPermission() { + // ... + return SaResult.ok(); + } + + // 权限校验2 ---- http://localhost:8081/at-check/checkPermission2 + // 一次性校验多个权限,必须全部拥有,才可以进入方法 + @SaCheckPermission(value = {"user.add", "user.delete", "user.update"}, mode = SaMode.AND) + @RequestMapping("checkPermission2") + public SaResult checkPermission2() { + // ... + return SaResult.ok(); + } + + // 权限校验3 ---- http://localhost:8081/at-check/checkPermission3 + // 一次性校验多个权限,只要拥有其中一个,就可以进入方法 + @SaCheckPermission(value = {"user.add", "user.delete", "user.update"}, mode = SaMode.OR) + @RequestMapping("checkPermission3") + public SaResult checkPermission3() { + // ... + return SaResult.ok(); + } + + // 角色校验 ---- http://localhost:8081/at-check/checkRole + // 只有具有 super-admin 角色的账号才可以进入方法 + @SaCheckRole("super-admin") + @RequestMapping("checkRole") + public SaResult checkRole() { + // ... + return SaResult.ok(); + } + + // 角色权限双重 “or校验” ---- http://localhost:8081/at-check/userAdd + // 具备 "user.add"权限 或者 "admin"角色 即可通过校验 + @RequestMapping("userAdd") + @SaCheckPermission(value = "user.add", orRole = "admin") + public SaResult userAdd() { + return SaResult.data("用户信息"); + } + + // 忽略校验 ---- http://localhost:8081/at-check/ignore + // 使用 @SaIgnore 修饰的方法,无需任何校验即可进入,具体使用示例可参照在线文档 + @SaIgnore + @SaCheckLogin + @RequestMapping("ignore") + public SaResult ignore() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/JurAuthController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/JurAuthController.java new file mode 100644 index 000000000..756649595 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/JurAuthController.java @@ -0,0 +1,112 @@ +package com.pj.cases.use; + +import java.util.List; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 权限认证示例 + * + * @author click33 + * @since 2022-10-13 + */ +@RestController +@RequestMapping("/jur/") +public class JurAuthController { + + /* + * 前提1:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 + * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + * + * 前提2:项目实现 StpInterface 接口,代码在 com.pj.satoken.StpInterfaceImpl + * Sa-Token 将从此实现类获取 每个账号拥有哪些权限。 + * + * 然后我们就可以使用以下示例中的代码进行鉴权了 + */ + + // 查询权限 ---- http://localhost:8081/jur/getPermission + @RequestMapping("getPermission") + public SaResult getPermission() { + // 查询权限信息 ,如果当前会话未登录,会返回一个空集合 + List permissionList = StpUtil.getPermissionList(); + System.out.println("当前登录账号拥有的所有权限:" + permissionList); + + // 查询角色信息 ,如果当前会话未登录,会返回一个空集合 + List roleList = StpUtil.getRoleList(); + System.out.println("当前登录账号拥有的所有角色:" + roleList); + + // 返回给前端 + return SaResult.ok() + .set("roleList", roleList) + .set("permissionList", permissionList); + } + + // 权限校验 ---- http://localhost:8081/jur/checkPermission + @RequestMapping("checkPermission") + public SaResult checkPermission() { + + // 判断:当前账号是否拥有一个权限,返回 true 或 false + // 如果当前账号未登录,则永远返回 false + StpUtil.hasPermission("user.add"); + StpUtil.hasPermissionAnd("user.add", "user.delete", "user.get"); // 指定多个,必须全部拥有才会返回 true + StpUtil.hasPermissionOr("user.add", "user.delete", "user.get"); // 指定多个,只要拥有一个就会返回 true + + // 校验:当前账号是否拥有一个权限,校验不通过时会抛出 `NotPermissionException` 异常 + // 如果当前账号未登录,则永远校验失败 + StpUtil.checkPermission("user.add"); + StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get"); // 指定多个,必须全部拥有才会校验通过 + StpUtil.checkPermissionOr("user.add", "user.delete", "user.get"); // 指定多个,只要拥有一个就会校验通过 + + return SaResult.ok(); + } + + // 角色校验 ---- http://localhost:8081/jur/checkRole + @RequestMapping("checkRole") + public SaResult checkRole() { + + // 判断:当前账号是否拥有一个角色,返回 true 或 false + // 如果当前账号未登录,则永远返回 false + StpUtil.hasRole("admin"); + StpUtil.hasRoleAnd("admin", "ceo", "cfo"); // 指定多个,必须全部拥有才会返回 true + StpUtil.hasRoleOr("admin", "ceo", "cfo"); // 指定多个,只要拥有一个就会返回 true + + // 校验:当前账号是否拥有一个角色,校验不通过时会抛出 `NotRoleException` 异常 + // 如果当前账号未登录,则永远校验失败 + StpUtil.checkRole("admin"); + StpUtil.checkRoleAnd("admin", "ceo", "cfo"); // 指定多个,必须全部拥有才会校验通过 + StpUtil.checkRoleOr("admin", "ceo", "cfo"); // 指定多个,只要拥有一个就会校验通过 + + return SaResult.ok(); + } + + // 权限通配符 ---- http://localhost:8081/jur/wildcardPermission + @RequestMapping("wildcardPermission") + public SaResult wildcardPermission() { + + // 前提条件:在 StpInterface 实现类中,为账号返回了 "art.*" 泛权限 + StpUtil.hasPermission("art.add"); // 返回 true + StpUtil.hasPermission("art.delete"); // 返回 true + StpUtil.hasPermission("goods.add"); // 返回 false,因为前缀不符合 + + // * 符合可以出现在任意位置,比如权限码的开头,当账号拥有 "*.delete" 时 + StpUtil.hasPermission("goods.add"); // false + StpUtil.hasPermission("goods.delete"); // true + StpUtil.hasPermission("art.delete"); // true + + // 也可以出现在权限码的中间,比如当账号拥有 "shop.*.user" 时 + StpUtil.hasPermission("shop.add.user"); // true + StpUtil.hasPermission("shop.delete.user"); // true + StpUtil.hasPermission("shop.delete.goods"); // false,因为后缀不符合 + + // 注意点: + // 1、上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码 + // 2、角色校验也可以加 * ,指定泛角色,例如: "*.admin",暂不赘述 + + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/KickoutController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/KickoutController.java new file mode 100644 index 000000000..a8f07e41e --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/KickoutController.java @@ -0,0 +1,61 @@ +package com.pj.cases.use; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 权限认证示例 + * + * @author click33 + * @since 2022-10-13 + */ +@RestController +@RequestMapping("/kickout/") +public class KickoutController { + + /* + * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 + * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + */ + + // 将指定账号强制注销 ---- http://localhost:8081/kickout/logout?userId=10001 + @RequestMapping("logout") + public SaResult logout(long userId) { + + // 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。 + StpUtil.logout(userId); + + // 返回 + return SaResult.ok(); + } + + // 将指定账号踢下线 ---- http://localhost:8081/kickout/kickout?userId=10001 + @RequestMapping("kickout") + public SaResult kickout(long userId) { + + // 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。 + StpUtil.kickout(userId); + + // 返回 + return SaResult.ok(); + } + + /* + * 你可以分别在强制注销和踢人下线后,再次访问一下登录校验接口,对比一下两者返回的提示信息有何不同 + * ---- http://localhost:8081/acc/checkLogin + */ + + // 根据 Token 值踢人 ---- http://localhost:8081/kickout/kickoutByTokenValue?tokenValue=xxxx-xxxx-xxxx-xxxx已登录账号的token值 + @RequestMapping("kickoutByTokenValue") + public SaResult kickoutByTokenValue(String tokenValue) { + + StpUtil.kickoutByTokenValue(tokenValue); + + // 返回 + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/LoginAuthController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/LoginAuthController.java new file mode 100644 index 000000000..3dbfb9f32 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/LoginAuthController.java @@ -0,0 +1,139 @@ +package com.pj.cases.use; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token 登录认证示例 + * + * @author click33 + * @since 2022-10-13 + */ +@RestController +@RequestMapping("/acc/") +public class LoginAuthController { + + // 会话登录接口 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + + // 第一步:比对前端提交的 账号名称 & 密码 是否正确,比对成功后开始登录 + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + + // 第二步:根据账号id,进行登录 + // 此处填入的参数应该保持用户表唯一,比如用户id,不可以直接填入整个 User 对象 + StpUtil.login(10001); + + // SaResult 是 Sa-Token 中对返回结果的简单封装,下面的示例将不再赘述 + return SaResult.ok("登录成功"); + } + + return SaResult.error("登录失败"); + } + + // 查询当前登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + // StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false + boolean isLogin = StpUtil.isLogin(); + return SaResult.ok("当前客户端是否登录:" + isLogin); + } + + // 校验当前登录状态 ---- http://localhost:8081/acc/checkLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException` + StpUtil.checkLogin(); + + // 抛出异常后,代码将走入全局异常处理(GlobalException.java),如果没有抛出异常,则代表通过了登录校验,返回下面信息 + return SaResult.ok("校验登录成功,这行字符串是只有登录后才会返回的信息"); + } + + // 获取当前登录的账号是谁 ---- http://localhost:8081/acc/getLoginId + @RequestMapping("getLoginId") + public SaResult getLoginId() { + // 需要注意的是,StpUtil.getLoginId() 自带登录校验效果 + // 也就是说如果在未登录的情况下调用这句代码,框架就会抛出 `NotLoginException` 异常,效果和 StpUtil.checkLogin() 是一样的 + Object userId = StpUtil.getLoginId(); + System.out.println("当前登录的账号id是:" + userId); + + // 如果不希望 StpUtil.getLoginId() 触发登录校验效果,可以填入一个默认值 + // 如果会话未登录,则返回这个默认值,如果会话已登录,将正常返回登录的账号id + Object userId2 = StpUtil.getLoginId(0); + System.out.println("当前登录的账号id是:" + userId2); + + // 或者使其在未登录的时候返回 null + Object userId3 = StpUtil.getLoginIdDefaultNull(); + System.out.println("当前登录的账号id是:" + userId3); + + // 类型转换: + // StpUtil.getLoginId() 返回的是 Object 类型,你可以使用以下方法指定其返回的类型 + int userId4 = StpUtil.getLoginIdAsInt(); // 将返回值转换为 int 类型 + long userId5 = StpUtil.getLoginIdAsLong(); // 将返回值转换为 long 类型 + String userId6 = StpUtil.getLoginIdAsString(); // 将返回值转换为 String 类型 + + // 疑问:数据基本类型不是有八个吗,为什么只封装以上三种类型的转换? + // 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的,实在没见过哪个项目用 double、float、boolean 之类来声明 UserId + System.out.println("当前登录的账号id是:" + userId4 + " --- " + userId5 + " --- " + userId6); + + // 返回给前端 + return SaResult.ok("当前客户端登录的账号id是:" + userId); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + // TokenName 是 Token 名称的意思,此值也决定了前端提交 Token 时应该使用的参数名称 + String tokenName = StpUtil.getTokenName(); + System.out.println("前端提交 Token 时应该使用的参数名称:" + tokenName); + + // 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值 + // 框架默认前端可以从以下三个途径中提交 Token: + // Cookie (浏览器自动提交) + // Header头 (代码手动提交) + // Query 参数 (代码手动提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx + // 读取顺序为: Query 参数 --> Header头 -- > Cookie + // 以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token + String tokenValue = StpUtil.getTokenValue(); + System.out.println("前端提交的Token值为:" + tokenValue); + + // TokenInfo 包含了此 Token 的大多数信息 + SaTokenInfo info = StpUtil.getTokenInfo(); + System.out.println("Token 名称:" + info.getTokenName()); + System.out.println("Token 值:" + info.getTokenValue()); + System.out.println("当前是否登录:" + info.getIsLogin()); + System.out.println("当前登录的账号id:" + info.getLoginId()); + System.out.println("当前登录账号的类型:" + info.getLoginType()); + System.out.println("当前登录客户端的设备类型:" + info.getLoginDeviceType()); + System.out.println("当前 Token 的剩余有效期:" + info.getTokenTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在 + System.out.println("当前 Token 距离被冻结还剩:" + info.getTokenActiveTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在 + System.out.println("当前 Account-Session 的剩余有效期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在 + System.out.println("当前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在 + + // 返回给前端 + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 会话注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + // 退出登录会清除三个地方的数据: + // 1、Redis中保存的 Token 信息 + // 2、当前请求上下文中保存的 Token 信息 + // 3、Cookie 中保存的 Token 信息(如果未使用Cookie模式则不会清除) + StpUtil.logout(); + + // StpUtil.logout() 在未登录时也是可以调用成功的, + // 也就是说,无论客户端有没有登录,执行完 StpUtil.logout() 后,都会处于未登录状态 + System.out.println("当前是否处于登录状态:" + StpUtil.isLogin()); + + // 返回给前端 + return SaResult.ok("退出登录成功"); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/RouterCheckController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/RouterCheckController.java new file mode 100644 index 000000000..fe64ccc46 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/RouterCheckController.java @@ -0,0 +1,27 @@ +package com.pj.cases.use; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 为路由拦截鉴权准备的路示例 + * + * @author click33 + * @since 2022-10-15 + */ +@RestController +public class RouterCheckController { + + // 路由拦截鉴权测试 ---- http://localhost:8081/xxx + @RequestMapping({ + "/user/doLogin", "/user/doLogin2", + "/user/info", "/admin/info", "/goods/info", "/orders/info", "/notice/info", "/comment/info", + "/router/print", "/router/print2" + }) + public SaResult checkLogin() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/SaSessionController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/SaSessionController.java new file mode 100644 index 000000000..80db0cecd --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/SaSessionController.java @@ -0,0 +1,100 @@ +package com.pj.cases.use; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.pj.model.SysUser; + +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.SaSessionCustomUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * Sa-Token Session会话示例 + * + * @author click33 + * @since 2022-10-15 + */ +@RestController +@RequestMapping("/session/") +public class SaSessionController { + + /* + * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 + * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + */ + + // 简单存取值 ---- http://localhost:8081/session/getValue + @RequestMapping("getValue") + public SaResult getValue() { + // 获取当前登录账号的专属 SaSession 对象 + // 注意点1:只有登录后才可以调用这个方法 + // 注意点2:每个账号获取到的都是不同的 SaSession 对象,存取值时不会互相影响 + // 注意点3:SaSession 和 HttpSession 是两个完全不同的对象,不可混淆使用 + SaSession session = StpUtil.getSession(); + + // 存值 + session.set("name", "zhangsan"); + session.set("age", 18); + + // 取值 + Object name = session.get("name"); + String name2 = session.getString("name"); // 取值,并转化为 String 数据类型 + int age = session.getInt("age"); // 转 int 类型 + long age2 = session.getLong("age"); // 转 long 类型 + float age3 = session.getFloat("age"); // 转 float 类型 + double age4 = session.getDouble("age"); // 转 double 类型 + int age5 = session.get("age5", 22); // 取不到时就返回默认值 + int age6 = session.get("age5", () -> { // 取不到时就执行 lambda 获取值 + return 26; + }); + + /* + * 存取值范围是一次会话有效的,也就是说,在一次登录有效期内,你可以在一个请求里存值,然后在另一个请求里取值 + */ + + List list = Arrays.asList(name, name2, age, age2, age3, age4, age5, age6); + System.out.println(list); + + return SaResult.data(list); + } + + // 复杂存取值 ---- http://localhost:8081/session/getModel + @RequestMapping("getModel") + public SaResult getModel() { + // 实例化 + SysUser user = new SysUser(); + user.setId(10001); + user.setName("张三"); + user.setAge(19); + + // 写入这个对象到 SaSession 中 + StpUtil.getSession().set("user", user); + + // 然后我们就可以在任意代码处获取这个 user 了 + SysUser user2 = StpUtil.getSession().getModel("user", SysUser.class); + + // 返回 + return SaResult.data(user2); + } + + // 自定义Session ---- http://localhost:8081/session/customSession + @RequestMapping("customSession") + public SaResult customSession() { + + // 自定义 Session 就是指使用一个特定的 key,来获取 Session 对象 + SaSession roleSession = SaSessionCustomUtil.getSessionById("role-1001"); + + // 一样可以自由的存值写值 + roleSession.set("nnn", "lalala"); + System.out.println(roleSession.get("nnn")); + + // 返回 + return SaResult.ok(); + } + +} 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 new file mode 100644 index 000000000..593da152d --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,73 @@ +package com.pj.current; + +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.NotHttpBasicAuthException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import cn.dev33.satoken.exception.NotSafeException; +import cn.dev33.satoken.util.SaResult; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 拦截:未登录异常 + @ExceptionHandler(NotLoginException.class) + public SaResult handlerException(NotLoginException e) { + + // 打印堆栈,以供调试 + e.printStackTrace(); + + // 返回给前端 + return SaResult.error(e.getMessage()); + } + + // 拦截:缺少权限异常 + @ExceptionHandler(NotPermissionException.class) + public SaResult handlerException(NotPermissionException e) { + e.printStackTrace(); + return SaResult.error("缺少权限:" + e.getPermission()); + } + + // 拦截:缺少角色异常 + @ExceptionHandler(NotRoleException.class) + public SaResult handlerException(NotRoleException e) { + e.printStackTrace(); + return SaResult.error("缺少角色:" + e.getRole()); + } + + // 拦截:二级认证校验失败异常 + @ExceptionHandler(NotSafeException.class) + public SaResult handlerException(NotSafeException e) { + e.printStackTrace(); + return SaResult.error("二级认证校验失败:" + e.getService()); + } + + // 拦截:服务封禁异常 + @ExceptionHandler(DisableServiceException.class) + public SaResult handlerException(DisableServiceException e) { + e.printStackTrace(); + return SaResult.error("当前账号 " + e.getService() + " 服务已被封禁 (level=" + e.getLevel() + "):" + e.getDisableTime() + "秒后解封"); + } + + // 拦截:Http Basic 校验失败异常 + @ExceptionHandler(NotHttpBasicAuthException.class) + public SaResult handlerException(NotHttpBasicAuthException e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + + // 拦截:其它所有异常 + @ExceptionHandler(Exception.class) + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..81ff22e8e --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,27 @@ +package com.pj.current; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 处理 404 + * @author click33 + */ +@RestController +public class NotFoundHandle implements ErrorController { + + @RequestMapping("/error") + public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setStatus(200); + return SaResult.get(404, "not found", null); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/model/SysUser.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/model/SysUser.java new file mode 100644 index 000000000..4f8b8a2d8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/model/SysUser.java @@ -0,0 +1,91 @@ +package com.pj.model; + +import java.io.Serializable; + +/** + * User 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysUser implements Serializable { + + /** + * + */ + private static final long serialVersionUID = -2853125262828437774L; + + public SysUser() { + } + + public SysUser(long id, String name, int age) { + super(); + this.id = id; + this.name = name; + this.age = age; + } + + + /** + * 用户id + */ + private long id; + + /** + * 用户名称 + */ + private String name; + + /** + * 用户年龄 + */ + private int age; + + /** + * @return id + */ + public long getId() { + return id; + } + + /** + * @param id 要设置的 id + */ + public void setId(long id) { + this.id = id; + } + + /** + * @return name + */ + public String getName() { + return name; + } + + /** + * @param name 要设置的 name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return age + */ + public int getAge() { + return age; + } + + /** + * @param age 要设置的 age + */ + public void setAge(int age) { + this.age = age; + } + + @Override + public String toString() { + return "SysUser [id=" + id + ", name=" + name + ", age=" + age + "]"; + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTempTemplate.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTempTemplate.java new file mode 100644 index 000000000..b1dc56163 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTempTemplate.java @@ -0,0 +1,26 @@ +package com.pj.satoken; + +import cn.dev33.satoken.temp.SaTempTemplate; + +/** + * 自定义临时 token 认证组件子类 + * + * @author click33 + * @since 2025/4/9 + */ +//@Component +public class MySaTempTemplate extends SaTempTemplate { + + @Override + public String createToken(Object value, long timeout, boolean isRecordIndex) { + System.out.println("------- 自定义一些逻辑 createToken "); + return super.createToken(value, timeout, isRecordIndex); + } + + @Override + public Object parseToken(String token) { + System.out.println("------- 自定义一些逻辑 parseToken "); + return super.parseToken(token); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTokenListener.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTokenListener.java new file mode 100644 index 000000000..72524572b --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTokenListener.java @@ -0,0 +1,82 @@ +package com.pj.satoken; + +import cn.dev33.satoken.listener.SaTokenListener; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; + +/** + * Sa-Token 自定义侦听器的实现 + * + * @author click33 + * @since 2022-10-17 + */ +//@Component // 打开此注解,让 SpringBoot 扫描到组件,即可完成自定义侦听器的注入 +public class MySaTokenListener implements SaTokenListener { + + /** 每次登录时触发 */ + @Override + public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { + System.out.println("---------- 自定义侦听器实现 doLogin"); + } + + /** 每次注销时触发 */ + @Override + public void doLogout(String loginType, Object loginId, String tokenValue) { + System.out.println("---------- 自定义侦听器实现 doLogout"); + } + + /** 每次被踢下线时触发 */ + @Override + public void doKickout(String loginType, Object loginId, String tokenValue) { + System.out.println("---------- 自定义侦听器实现 doKickout"); + } + + /** 每次被顶下线时触发 */ + @Override + public void doReplaced(String loginType, Object loginId, String tokenValue) { + System.out.println("---------- 自定义侦听器实现 doReplaced"); + } + + /** 每次被封禁时触发 */ + @Override + public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { + System.out.println("---------- 自定义侦听器实现 doDisable"); + } + + /** 每次被解封时触发 */ + @Override + public void doUntieDisable(String loginType, Object loginId, String service) { + System.out.println("---------- 自定义侦听器实现 doUntieDisable"); + } + + /** 每次打开二级认证时触发 */ + @Override + public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { + System.out.println("---------- 自定义侦听器实现 doOpenSafe"); + } + + /** 每次关闭二级认证时触发 */ + @Override + public void doCloseSafe(String loginType, String tokenValue, String service) { + System.out.println("---------- 自定义侦听器实现 doCloseSafe"); + } + + /** 每次创建Session时触发 */ + @Override + public void doCreateSession(String id) { + System.out.println("---------- 自定义侦听器实现 doCreateSession"); + } + + /** 每次注销Session时触发 */ + @Override + public void doLogoutSession(String id) { + System.out.println("---------- 自定义侦听器实现 doLogoutSession"); + } + + /** 每次Token续期时触发 */ + @Override + public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { + System.out.println("---------- 自定义侦听器实现 doRenewTimeout"); + } + + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaLogForSlf4j.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaLogForSlf4j.java new file mode 100644 index 000000000..67599e794 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaLogForSlf4j.java @@ -0,0 +1,49 @@ +package com.pj.satoken; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import cn.dev33.satoken.log.SaLog; + +/** + * 将 Sa-Token log 信息转接到 Slf4j + * + * @author click33 + * @since 2022-11-2 + */ +//@Component +public class SaLogForSlf4j implements SaLog { + + Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); + + @Override + public void trace(String str, Object... args) { + log.trace(str, args); + } + + @Override + public void debug(String str, Object... args) { + log.debug(str, args); + } + + @Override + public void info(String str, Object... args) { + log.info(str, args); + } + + @Override + public void warn(String str, Object... args) { + log.trace(str, args); + } + + @Override + public void error(String str, Object... args) { + log.error(str, args); + } + + @Override + public void fatal(String str, Object... args) { + log.error(str, args); + } + +} 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 new file mode 100644 index 000000000..1f2499317 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,159 @@ +package com.pj.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.stp.StpUtil; +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; + + +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + * + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + + /** + * 注册 Sa-Token 拦截器打开注解鉴权功能 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册 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 表达式 + + // 权限校验 -- 不同模块认证不同权限 + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); + + // 甚至你可以随意的写一个打印语句 + SaRouter.match("/router/print", r -> System.out.println("----啦啦啦----")); + + // 写一个完整的 lambda + SaRouter.match("/router/print2", r -> { + System.out.println("----啦啦啦2----"); + // ... 其它代码 + }); + + /* + * 相关路由都定义在 com.pj.cases.use.RouterCheckController 中 + */ + + })).addPathPatterns("/**"); + + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // System.out.println("---------- sa全局认证 " + SaHolder.getRequest().getRequestPath()); + // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + + // 权限校验 -- 不同模块认证不同权限 + // 这里你可以写和拦截器鉴权同样的代码,不同点在于: + // 校验失败后不会进入全局异常组件,而是进入下面的 .setError 函数 + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) + .setBeforeAuth(r -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + ; + }) + ; + } + + /** + * CORS 跨域处理 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + + /** + * 重写 Sa-Token 框架内部算法策略 + */ + @PostConstruct + public void rewriteSaStrategy() { + // 重写Sa-Token的注解处理器,增加注解合并功能 + SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> { + return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); + }; + + // 重写 SaCheckELRootMap 扩展函数,增加注解鉴权 EL 表达式可使用的根对象 + SaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> { + System.out.println("--------- 执行 SaCheckELRootMap 增强,目前已包含的的跟对象包括:" + rootMap.keySet()); + // 新增 stpUser 根对象,使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权 + rootMap.put("stpUser", StpUserUtil.getStpLogic()); + }; + } + + + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..af3f5b4ac --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,46 @@ +package com.pj.satoken; + +import cn.dev33.satoken.stp.StpInterface; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义权限认证接口扩展,Sa-Token 将从此实现类获取每个账号拥有的权限码 + * + * @author click33 + * @since 2022-10-13 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user.add"); + list.add("user.update"); + list.add("user.get"); + // list.add("user.delete"); + list.add("art.*"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} 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 new file mode 100644 index 000000000..cc6cc9006 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java @@ -0,0 +1,1453 @@ +package com.pj.satoken; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaTwoParamFunction; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.SaTerminalInfo; +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; + +import java.util.List; + +/** + * 【User账号体系】Sa-Token 权限认证工具类 + * + * @author click33 + * @since 1.0.0 + */ +public class StpUserUtil { + + private StpUserUtil() {} + + /** + * 多账号体系下的类型标识 + */ + public static final String TYPE = "user"; + + /** + * 底层使用的 StpLogic 对象 + */ + public static StpLogic stpLogic = new StpLogic(TYPE); + + /** + * 获取当前 StpLogic 的账号类型 + * + * @return / + */ + public static String getLoginType(){ + return stpLogic.getLoginType(); + } + + /** + * 安全的重置 StpLogic 对象 + * + *
1、更改此账户的 StpLogic 对象 + *
2、put 到全局 StpLogic 集合中 + *
3、发送日志 + * + * @param newStpLogic / + */ + public static void setStpLogic(StpLogic newStpLogic) { + // 1、重置此账户的 StpLogic 对象 + stpLogic = newStpLogic; + + // 2、添加到全局 StpLogic 集合中 + // 以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic + SaManager.putStpLogic(newStpLogic); + + // 3、$$ 发布事件:更新了 stpLogic 对象 + SaTokenEventCenter.doSetStpLogic(stpLogic); + } + + /** + * 获取 StpLogic 对象 + * + * @return / + */ + public static StpLogic getStpLogic() { + return stpLogic; + } + + + // ------------------- 获取 token 相关 ------------------- + + /** + * 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀 + * + * @return / + */ + public static String getTokenName() { + return stpLogic.getTokenName(); + } + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + */ + public static void setTokenValue(String tokenValue){ + stpLogic.setTokenValue(tokenValue); + } + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + * @param cookieTimeout Cookie存活时间(秒) + */ + public static void setTokenValue(String tokenValue, int cookieTimeout){ + stpLogic.setTokenValue(tokenValue, cookieTimeout); + } + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + * @param loginParameter 登录参数 + */ + public static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){ + stpLogic.setTokenValue(tokenValue, loginParameter); + } + + /** + * 获取当前请求的 token 值 + * + * @return 当前tokenValue + */ + public static String getTokenValue() { + return stpLogic.getTokenValue(); + } + + /** + * 获取当前请求的 token 值 (不裁剪前缀) + * + * @return / + */ + public static String getTokenValueNotCut(){ + return stpLogic.getTokenValueNotCut(); + } + + /** + * 获取当前会话的 token 参数信息 + * + * @return token 参数信息 + */ + public static SaTokenInfo getTokenInfo() { + return stpLogic.getTokenInfo(); + } + + + // ------------------- 登录相关操作 ------------------- + + // --- 登录 + + /** + * 会话登录 + * + * @param id 账号id,建议的类型:(long | int | String) + */ + public static void login(Object id) { + stpLogic.login(id); + } + + /** + * 会话登录,并指定登录设备类型 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param deviceType 设备类型 + */ + public static void login(Object id, String deviceType) { + stpLogic.login(id, deviceType); + } + + /** + * 会话登录,并指定是否 [记住我] + * + * @param id 账号id,建议的类型:(long | int | String) + * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 + */ + public static void login(Object id, boolean isLastingCookie) { + stpLogic.login(id, isLastingCookie); + } + + /** + * 会话登录,并指定此次登录 token 的有效期, 单位:秒 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param timeout 此次登录 token 的有效期, 单位:秒 + */ + public static void login(Object id, long timeout) { + stpLogic.login(id, timeout); + } + + /** + * 会话登录,并指定所有登录参数 Model + * + * @param id 账号id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + */ + public static void login(Object id, SaLoginParameter loginParameter) { + stpLogic.login(id, loginParameter); + } + + /** + * 创建指定账号 id 的登录会话数据 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public static String createLoginSession(Object id) { + return stpLogic.createLoginSession(id); + } + + /** + * 创建指定账号 id 的登录会话数据 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + * @return 返回会话令牌 + */ + public static String createLoginSession(Object id, SaLoginParameter loginParameter) { + return stpLogic.createLoginSession(id, loginParameter); + } + + /** + * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public static String getOrCreateLoginSession(Object id) { + return stpLogic.getOrCreateLoginSession(id); + } + + // --- 注销 (根据 token) + + /** + * 在当前客户端会话注销 + */ + public static void logout() { + stpLogic.logout(); + } + + /** + * 在当前客户端会话注销,根据注销参数 + */ + public static void logout(SaLogoutParameter logoutParameter) { + stpLogic.logout(logoutParameter); + } + + /** + * 注销下线,根据指定 token + * + * @param tokenValue 指定 token + */ + public static void logoutByTokenValue(String tokenValue) { + stpLogic.logoutByTokenValue(tokenValue); + } + + /** + * 注销下线,根据指定 token、注销参数 + * + * @param tokenValue 指定 token + * @param logoutParameter / + */ + public static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.logoutByTokenValue(tokenValue, logoutParameter); + } + + /** + * 踢人下线,根据指定 token + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param tokenValue 指定 token + */ + public static void kickoutByTokenValue(String tokenValue) { + stpLogic.kickoutByTokenValue(tokenValue); + } + + /** + * 踢人下线,根据指定 token、注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param tokenValue 指定 token + * @param logoutParameter 注销参数 + */ + public static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.kickoutByTokenValue(tokenValue, logoutParameter); + } + + /** + * 顶人下线,根据指定 token + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param tokenValue 指定 token + */ + public static void replacedByTokenValue(String tokenValue) { + stpLogic.replacedByTokenValue(tokenValue); + } + + /** + * 顶人下线,根据指定 token、注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param tokenValue 指定 token + * @param logoutParameter / + */ + public static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.replacedByTokenValue(tokenValue, logoutParameter); + } + + // --- 注销 (根据 loginId) + + /** + * 会话注销,根据账号id + * + * @param loginId 账号id + */ + public static void logout(Object loginId) { + stpLogic.logout(loginId); + } + + /** + * 会话注销,根据账号id 和 设备类型 + * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型) + */ + public static void logout(Object loginId, String deviceType) { + stpLogic.logout(loginId, deviceType); + } + + /** + * 会话注销,根据账号id 和 注销参数 + * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void logout(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.logout(loginId, logoutParameter); + } + + /** + * 踢人下线,根据账号id + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + */ + public static void kickout(Object loginId) { + stpLogic.kickout(loginId); + } + + /** + * 踢人下线,根据账号id 和 设备类型 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型) + */ + public static void kickout(Object loginId, String deviceType) { + stpLogic.kickout(loginId, deviceType); + } + + /** + * 踢人下线,根据账号id 和 注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void kickout(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.kickout(loginId, logoutParameter); + } + + /** + * 顶人下线,根据账号id + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + */ + public static void replaced(Object loginId) { + stpLogic.replaced(loginId); + } + + /** + * 顶人下线,根据账号id 和 设备类型 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表顶替该账号的所有设备类型) + */ + public static void replaced(Object loginId, String deviceType) { + stpLogic.replaced(loginId, deviceType); + } + + /** + * 顶人下线,根据账号id 和 注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void replaced(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.replaced(loginId, logoutParameter); + } + + // --- 注销 (会话管理辅助方法) + + /** + * 在 Account-Session 上移除 Terminal 信息 (注销下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByLogout(session, terminal); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByKickout(session, terminal); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByReplaced(session, terminal); + } + + + // 会话查询 + + /** + * 判断当前会话是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ + public static boolean isLogin() { + return stpLogic.isLogin(); + } + + /** + * 判断指定账号是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ + public static boolean isLogin(Object loginId) { + return stpLogic.isLogin(loginId); + } + + /** + * 检验当前会话是否已经登录,如未登录,则抛出异常 + */ + public static void checkLogin() { + stpLogic.checkLogin(); + } + + /** + * 获取当前会话账号id,如果未登录,则抛出异常 + * + * @return 账号id + */ + public static Object getLoginId() { + return stpLogic.getLoginId(); + } + + /** + * 获取当前会话账号id, 如果未登录,则返回默认值 + * + * @param 返回类型 + * @param defaultValue 默认值 + * @return 登录id + */ + public static T getLoginId(T defaultValue) { + return stpLogic.getLoginId(defaultValue); + } + + /** + * 获取当前会话账号id, 如果未登录,则返回null + * + * @return 账号id + */ + public static Object getLoginIdDefaultNull() { + return stpLogic.getLoginIdDefaultNull(); + } + + /** + * 获取当前会话账号id, 并转换为 String 类型 + * + * @return 账号id + */ + public static String getLoginIdAsString() { + return stpLogic.getLoginIdAsString(); + } + + /** + * 获取当前会话账号id, 并转换为 int 类型 + * + * @return 账号id + */ + public static int getLoginIdAsInt() { + return stpLogic.getLoginIdAsInt(); + } + + /** + * 获取当前会话账号id, 并转换为 long 类型 + * + * @return 账号id + */ + public static long getLoginIdAsLong() { + return stpLogic.getLoginIdAsLong(); + } + + /** + * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶、被冻结等状态,则返回 null + * + * @param tokenValue token + * @return 账号id + */ + public static Object getLoginIdByToken(String tokenValue) { + return stpLogic.getLoginIdByToken(tokenValue); + } + + /** + * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结),则返回 null + * + * @param tokenValue token + * @return 账号id + */ + public Object getLoginIdByTokenNotThinkFreeze(String tokenValue) { + return stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue); + } + + /** + * 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) + * + * @param key 键值 + * @return 对应的扩展数据 + */ + public static Object getExtra(String key) { + return stpLogic.getExtra(key); + } + + /** + * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) + * + * @param tokenValue 指定的 Token 值 + * @param key 键值 + * @return 对应的扩展数据 + */ + public static Object getExtra(String tokenValue, String key) { + return stpLogic.getExtra(tokenValue, key); + } + + + // ------------------- Account-Session 相关 ------------------- + + /** + * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * + * @param loginId 账号id + * @param isCreate 是否新建 + * @return SaSession 对象 + */ + public static SaSession getSessionByLoginId(Object loginId, boolean isCreate) { + return stpLogic.getSessionByLoginId(loginId, isCreate); + } + + /** + * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,则返回 null + * + * @param sessionId SessionId + * @return Session对象 + */ + public static SaSession getSessionBySessionId(String sessionId) { + return stpLogic.getSessionBySessionId(sessionId); + } + + /** + * 获取指定账号 id 的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @param loginId 账号id + * @return SaSession 对象 + */ + public static SaSession getSessionByLoginId(Object loginId) { + return stpLogic.getSessionByLoginId(loginId); + } + + /** + * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * + * @param isCreate 是否新建 + * @return Session对象 + */ + public static SaSession getSession(boolean isCreate) { + return stpLogic.getSession(isCreate); + } + + /** + * 获取当前已登录账号的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @return Session对象 + */ + public static SaSession getSession() { + return stpLogic.getSession(); + } + + + // ------------------- Token-Session 相关 ------------------- + + /** + * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @param tokenValue Token值 + * @return Session对象 + */ + public static SaSession getTokenSessionByToken(String tokenValue) { + return stpLogic.getTokenSessionByToken(tokenValue); + } + + /** + * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @return Session对象 + */ + public static SaSession getTokenSession() { + return stpLogic.getTokenSession(); + } + + /** + * 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) + * + * @return Token-Session 对象 + */ + public static SaSession getAnonTokenSession() { + return stpLogic.getAnonTokenSession(); + } + + + // ------------------- Active-Timeout token 最低活跃度 验证相关 ------------------- + + /** + * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) + *

+ * 请注意: 即使 token 已被冻结 也可续签成功, + * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 + *

+ */ + public static void updateLastActiveToNow() { + stpLogic.updateLastActiveToNow(); + } + + /** + * 检查当前 token 是否已被冻结,如果是则抛出异常 + */ + public static void checkActiveTimeout() { + stpLogic.checkActiveTimeout(); + } + + + // ------------------- 过期时间相关 ------------------- + + /** + * 获取当前会话 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public static long getTokenTimeout() { + return stpLogic.getTokenTimeout(); + } + + /** + * 获取指定 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @param token 指定token + * @return token剩余有效时间 + */ + public static long getTokenTimeout(String token) { + return stpLogic.getTokenTimeout(token); + } + + /** + * 获取当前登录账号的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public static long getSessionTimeout() { + return stpLogic.getSessionTimeout(); + } + + /** + * 获取当前 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public static long getTokenSessionTimeout() { + return stpLogic.getTokenSessionTimeout(); + } + + /** + * 获取当前 token 剩余活跃有效期:当前 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) + * + * @return / + */ + public static long getTokenActiveTimeout() { + return stpLogic.getTokenActiveTimeout(); + } + + /** + * 重置当前 token 的 timeout 有效期 + * + * @param timeout 要修改成为的有效时间 (单位: 秒) + */ + public static void renewTimeout(long timeout) { + stpLogic.renewTimeout(timeout); + } + + /** + * 重置指定 token 的 timeout 有效期 + * + * @param tokenValue 指定 token + * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) + */ + public static void renewTimeout(String tokenValue, long timeout) { + stpLogic.renewTimeout(tokenValue, timeout); + } + + + // ------------------- 角色认证操作 ------------------- + + /** + * 获取:当前账号的角色集合 + * + * @return / + */ + public static List getRoleList() { + return stpLogic.getRoleList(); + } + + /** + * 获取:指定账号的角色集合 + * + * @param loginId 指定账号id + * @return / + */ + public static List getRoleList(Object loginId) { + return stpLogic.getRoleList(loginId); + } + + /** + * 判断:当前账号是否拥有指定角色, 返回 true 或 false + * + * @param role 角色 + * @return / + */ + public static boolean hasRole(String role) { + return stpLogic.hasRole(role); + } + + /** + * 判断:指定账号是否含有指定角色标识, 返回 true 或 false + * + * @param loginId 账号id + * @param role 角色标识 + * @return 是否含有指定角色标识 + */ + public static boolean hasRole(Object loginId, String role) { + return stpLogic.hasRole(loginId, role); + } + + /** + * 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] + * + * @param roleArray 角色标识数组 + * @return true或false + */ + public static boolean hasRoleAnd(String... roleArray){ + return stpLogic.hasRoleAnd(roleArray); + } + + /** + * 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] + * + * @param roleArray 角色标识数组 + * @return true或false + */ + public static boolean hasRoleOr(String... roleArray){ + return stpLogic.hasRoleOr(roleArray); + } + + /** + * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException + * + * @param role 角色标识 + */ + public static void checkRole(String role) { + stpLogic.checkRole(role); + } + + /** + * 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] + * + * @param roleArray 角色标识数组 + */ + public static void checkRoleAnd(String... roleArray){ + stpLogic.checkRoleAnd(roleArray); + } + + /** + * 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] + * + * @param roleArray 角色标识数组 + */ + public static void checkRoleOr(String... roleArray){ + stpLogic.checkRoleOr(roleArray); + } + + + // ------------------- 权限认证操作 ------------------- + + /** + * 获取:当前账号的权限码集合 + * + * @return / + */ + public static List getPermissionList() { + return stpLogic.getPermissionList(); + } + + /** + * 获取:指定账号的权限码集合 + * + * @param loginId 指定账号id + * @return / + */ + public static List getPermissionList(Object loginId) { + return stpLogic.getPermissionList(loginId); + } + + /** + * 判断:当前账号是否含有指定权限, 返回 true 或 false + * + * @param permission 权限码 + * @return 是否含有指定权限 + */ + public static boolean hasPermission(String permission) { + return stpLogic.hasPermission(permission); + } + + /** + * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false + * + * @param loginId 账号 id + * @param permission 权限码 + * @return 是否含有指定权限 + */ + public static boolean hasPermission(Object loginId, String permission) { + return stpLogic.hasPermission(loginId, permission); + } + + /** + * 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ] + * + * @param permissionArray 权限码数组 + * @return true 或 false + */ + public static boolean hasPermissionAnd(String... permissionArray){ + return stpLogic.hasPermissionAnd(permissionArray); + } + + /** + * 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] + * + * @param permissionArray 权限码数组 + * @return true 或 false + */ + public static boolean hasPermissionOr(String... permissionArray){ + return stpLogic.hasPermissionOr(permissionArray); + } + + /** + * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param permission 权限码 + */ + public static void checkPermission(String permission) { + stpLogic.checkPermission(permission); + } + + /** + * 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ] + * + * @param permissionArray 权限码数组 + */ + public static void checkPermissionAnd(String... permissionArray) { + stpLogic.checkPermissionAnd(permissionArray); + } + + /** + * 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] + * + * @param permissionArray 权限码数组 + */ + public static void checkPermissionOr(String... permissionArray) { + stpLogic.checkPermissionOr(permissionArray); + } + + + // ------------------- id 反查 token 相关操作 ------------------- + + /** + * 获取指定账号 id 的 token + *

+ * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + *

+ * + * @param loginId 账号id + * @return token值 + */ + public static String getTokenValueByLoginId(Object loginId) { + return stpLogic.getTokenValueByLoginId(loginId); + } + + /** + * 获取指定账号 id 指定设备类型端的 token + *

+ * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + *

+ * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return token值 + */ + public static String getTokenValueByLoginId(Object loginId, String deviceType) { + return stpLogic.getTokenValueByLoginId(loginId, deviceType); + } + + /** + * 获取指定账号 id 的 token 集合 + * + * @param loginId 账号id + * @return 此 loginId 的所有相关 token + */ + public static List getTokenValueListByLoginId(Object loginId) { + return stpLogic.getTokenValueListByLoginId(loginId); + } + + /** + * 获取指定账号 id 指定设备类型端的 token 集合 + * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return 此 loginId 的所有登录 token + */ + public static List getTokenValueListByLoginId(Object loginId, String deviceType) { + return stpLogic.getTokenValueListByLoginId(loginId, deviceType); + } + + /** + * 获取指定账号 id 已登录设备信息集合 + * + * @param loginId 账号id + * @return 此 loginId 的所有登录 token + */ + public static List getTerminalListByLoginId(Object loginId) { + return stpLogic.getTerminalListByLoginId(loginId); + } + + /** + * 获取指定账号 id 指定设备类型端的已登录设备信息集合 + * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return / + */ + public static List getTerminalListByLoginId(Object loginId, String deviceType) { + return stpLogic.getTerminalListByLoginId(loginId, deviceType); + } + + /** + * 获取指定账号 id 已登录设备信息集合,执行特定函数 + * + * @param loginId 账号id + * @param function 需要执行的函数 + */ + public static void forEachTerminalList(Object loginId, SaTwoParamFunction function) { + stpLogic.forEachTerminalList(loginId, function); + } + + /** + * 返回当前会话的登录设备类型 + * + * @return 当前令牌的登录设备类型 + */ + public static String getLoginDeviceType() { + return stpLogic.getLoginDeviceType(); + } + + /** + * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 + */ + public static String getLoginDeviceTypeByToken(String tokenValue) { + return stpLogic.getLoginDeviceTypeByToken(tokenValue); + } + + /** + * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 + * + * @return / + */ + public static long getTokenLastActiveTime() { + return stpLogic.getTokenLastActiveTime(); + } + + /** + * 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 + * @param deviceId / + * @return / + */ + public static boolean isTrustDeviceId(Object userId, String deviceId) { + return stpLogic.isTrustDeviceId(userId, deviceId); + } + + + + // ------------------- 会话管理 ------------------- + + /** + * 根据条件查询缓存中所有的 token + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return token集合 + */ + public static List searchTokenValue(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchTokenValue(keyword, start, size, sortType); + } + + /** + * 根据条件查询缓存中所有的 SessionId + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return sessionId集合 + */ + public static List searchSessionId(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchSessionId(keyword, start, size, sortType); + } + + /** + * 根据条件查询缓存中所有的 Token-Session-Id + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return sessionId集合 + */ + public static List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchTokenSessionId(keyword, start, size, sortType); + } + + + // ------------------- 账号封禁 ------------------- + + /** + * 封禁:指定账号 + *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) + * + * @param loginId 指定账号id + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disable(Object loginId, long time) { + stpLogic.disable(loginId, time); + } + + /** + * 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) + * + * @param loginId 账号id + * @return / + */ + public static boolean isDisable(Object loginId) { + return stpLogic.isDisable(loginId); + } + + /** + * 校验:指定账号是否已被封禁,如果被封禁则抛出异常 + * + * @param loginId 账号id + */ + public static void checkDisable(Object loginId) { + stpLogic.checkDisable(loginId); + } + + /** + * 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * + * @param loginId 账号id + * @return / + */ + public static long getDisableTime(Object loginId) { + return stpLogic.getDisableTime(loginId); + } + + /** + * 解封:指定账号 + * + * @param loginId 账号id + */ + public static void untieDisable(Object loginId) { + stpLogic.untieDisable(loginId); + } + + + // ------------------- 分类封禁 ------------------- + + /** + * 封禁:指定账号的指定服务 + *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) + * + * @param loginId 指定账号id + * @param service 指定服务 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disable(Object loginId, String service, long time) { + stpLogic.disable(loginId, service, time); + } + + /** + * 判断:指定账号的指定服务 是否已被封禁(true=已被封禁, false=未被封禁) + * + * @param loginId 账号id + * @param service 指定服务 + * @return / + */ + public static boolean isDisable(Object loginId, String service) { + return stpLogic.isDisable(loginId, service); + } + + /** + * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 + * + * @param loginId 账号id + * @param services 指定服务,可以指定多个 + */ + public static void checkDisable(Object loginId, String... services) { + stpLogic.checkDisable(loginId, services); + } + + /** + * 获取:指定账号 指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * + * @param loginId 账号id + * @param service 指定服务 + * @return see note + */ + public static long getDisableTime(Object loginId, String service) { + return stpLogic.getDisableTime(loginId, service); + } + + /** + * 解封:指定账号、指定服务 + * + * @param loginId 账号id + * @param services 指定服务,可以指定多个 + */ + public static void untieDisable(Object loginId, String... services) { + stpLogic.untieDisable(loginId, services); + } + + + // ------------------- 阶梯封禁 ------------------- + + /** + * 封禁:指定账号,并指定封禁等级 + * + * @param loginId 指定账号id + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disableLevel(Object loginId, int level, long time) { + stpLogic.disableLevel(loginId, level, time); + } + + /** + * 封禁:指定账号的指定服务,并指定封禁等级 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disableLevel(Object loginId, String service, int level, long time) { + stpLogic.disableLevel(loginId, service, level, time); + } + + /** + * 判断:指定账号是否已被封禁到指定等级 + * + * @param loginId 指定账号id + * @param level 指定封禁等级 + * @return / + */ + public static boolean isDisableLevel(Object loginId, int level) { + return stpLogic.isDisableLevel(loginId, level); + } + + /** + * 判断:指定账号的指定服务,是否已被封禁到指定等级 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 指定封禁等级 + * @return / + */ + public static boolean isDisableLevel(Object loginId, String service, int level) { + return stpLogic.isDisableLevel(loginId, service, level); + } + + /** + * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) + * + * @param loginId 指定账号id + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + */ + public static void checkDisableLevel(Object loginId, int level) { + stpLogic.checkDisableLevel(loginId, level); + } + + /** + * 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常) + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + */ + public static void checkDisableLevel(Object loginId, String service, int level) { + stpLogic.checkDisableLevel(loginId, service, level); + } + + /** + * 获取:指定账号被封禁的等级,如果未被封禁则返回-2 + * + * @param loginId 指定账号id + * @return / + */ + public static int getDisableLevel(Object loginId) { + return stpLogic.getDisableLevel(loginId); + } + + /** + * 获取:指定账号的 指定服务 被封禁的等级,如果未被封禁则返回-2 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @return / + */ + public static int getDisableLevel(Object loginId, String service) { + return stpLogic.getDisableLevel(loginId, service); + } + + + // ------------------- 临时身份切换 ------------------- + + /** + * 临时切换身份为指定账号id + * + * @param loginId 指定loginId + */ + public static void switchTo(Object loginId) { + stpLogic.switchTo(loginId); + } + + /** + * 结束临时切换身份 + */ + public static void endSwitch() { + stpLogic.endSwitch(); + } + + /** + * 判断当前请求是否正处于 [ 身份临时切换 ] 中 + * + * @return / + */ + public static boolean isSwitch() { + return stpLogic.isSwitch(); + } + + /** + * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 + * + * @param loginId 指定账号id + * @param function 要执行的方法 + */ + public static void switchTo(Object loginId, SaFunction function) { + stpLogic.switchTo(loginId, function); + } + + + // ------------------- 二级认证 ------------------- + + /** + * 在当前会话 开启二级认证 + * + * @param safeTime 维持时间 (单位: 秒) + */ + public static void openSafe(long safeTime) { + stpLogic.openSafe(safeTime); + } + + /** + * 在当前会话 开启二级认证 + * + * @param service 业务标识 + * @param safeTime 维持时间 (单位: 秒) + */ + public static void openSafe(String service, long safeTime) { + stpLogic.openSafe(service, safeTime); + } + + /** + * 判断:当前会话是否处于二级认证时间内 + * + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public static boolean isSafe() { + return stpLogic.isSafe(); + } + + /** + * 判断:当前会话 是否处于指定业务的二级认证时间内 + * + * @param service 业务标识 + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public static boolean isSafe(String service) { + return stpLogic.isSafe(service); + } + + /** + * 判断:指定 token 是否处于二级认证时间内 + * + * @param tokenValue Token 值 + * @param service 业务标识 + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public static boolean isSafe(String tokenValue, String service) { + return stpLogic.isSafe(tokenValue, service); + } + + /** + * 校验:当前会话是否已通过二级认证,如未通过则抛出异常 + */ + public static void checkSafe() { + stpLogic.checkSafe(); + } + + /** + * 校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常 + * + * @param service 业务标识 + */ + public static void checkSafe(String service) { + stpLogic.checkSafe(service); + } + + /** + * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) + * + * @return 剩余有效时间 + */ + public static long getSafeTime() { + return stpLogic.getSafeTime(); + } + + /** + * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) + * + * @param service 业务标识 + * @return 剩余有效时间 + */ + public static long getSafeTime(String service) { + return stpLogic.getSafeTime(service); + } + + /** + * 在当前会话 结束二级认证 + */ + public static void closeSafe() { + stpLogic.closeSafe(); + } + + /** + * 在当前会话 结束指定业务标识的二级认证 + * + * @param service 业务标识 + */ + public static void closeSafe(String service) { + stpLogic.closeSafe(service); + } + + + // ------------------- Bean 对象、字段代理 ------------------- + + /** + * 根据当前配置对象创建一个 SaLoginParameter 对象 + * + * @return / + */ + public static SaLoginParameter createSaLoginParameter() { + return stpLogic.createSaLoginParameter(); + } + + + // ------------------- 过期方法 ------------------- + + /** + *

请更换为 getLoginDeviceType

+ * 返回当前会话的登录设备类型 + * + * @return 当前令牌的登录设备类型 + */ + @Deprecated + public static String getLoginDevice() { + return stpLogic.getLoginDevice(); + } + + /** + *

请更换为 getLoginDeviceTypeByToken

+ * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 + */ + @Deprecated + public static String getLoginDeviceByToken(String tokenValue) { + return stpLogic.getLoginDeviceByToken(tokenValue); + } + +} 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 000000000..2c01a5ae5 --- /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 000000000..9495168b9 --- /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 000000000..eb980411c --- /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 000000000..c3e06bac4 --- /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 000000000..49e2da67e --- /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 000000000..8cb022030 --- /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.AnnotatedElement; + +/** + * 注解 CheckAccount 的处理器 + * + * @author click33 + * + */ +@Component +public class CheckAccountHandler implements SaAnnotationHandlerInterface { + + // 指定这个处理器要处理哪个注解 + @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("账号或密码错误,未通过校验"); + } + } + +} 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 000000000..199fc572a --- /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.AnnotatedElement; + +/** + * 注解 SaUserCheckLogin 的处理器 + * + * @author click33 + */ +@Component +public class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckLogin.class; + } + + @Override + public void checkMethod(SaUserCheckLogin at, AnnotatedElement element) { + 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 000000000..b447fa871 --- /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.AnnotatedElement; + +/** + * 注解 SaUserCheckPermission 的处理器 + * + * @author click33 + */ +@Component +public class SaUserCheckPermissionHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckPermission.class; + } + + @Override + public void checkMethod(SaUserCheckPermission at, AnnotatedElement element) { + 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 000000000..6dc507ff3 --- /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.AnnotatedElement; + +/** + * 注解 SaUserCheckRole 的处理器 + * + * @author click33 + */ +@Component +public class SaUserCheckRoleHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckRole.class; + } + + @Override + public void checkMethod(SaUserCheckRole at, AnnotatedElement element) { + 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 000000000..d797d5c5a --- /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.AnnotatedElement; + +/** + * 注解 SaUserCheckPermission 的处理器 + * + * @author click33 + */ +@Component +public class SaUserCheckSafeHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaUserCheckSafe.class; + } + + @Override + public void checkMethod(SaUserCheckSafe at, AnnotatedElement element) { + SaCheckSafeHandler._checkMethod(StpUserUtil.TYPE, at.value()); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckLogin.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckLogin.java new file mode 100644 index 000000000..45e60c5f7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckLogin.java @@ -0,0 +1,22 @@ +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; + +/** + * 登录认证(User版):只有登录之后才能进入该方法 + *

可标注在函数、类上(效果等同于标注在此类的所有方法上) + * @author click33 + * + */ +@SaCheckLogin(type = StpUserUtil.TYPE) +@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/merge_annotation/SaUserCheckPermission.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckPermission.java new file mode 100644 index 000000000..641f23c6e --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckPermission.java @@ -0,0 +1,56 @@ +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; + +/** + * 权限认证(User版):必须具有指定权限才能进入该方法 + *

可标注在函数、类上(效果等同于标注在此类的所有方法上) + * @author click33 + * + */ +@SaCheckPermission(type = StpUserUtil.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface SaUserCheckPermission { + + /** + * 需要校验的权限码 + * @return 需要校验的权限码 + */ + @AliasFor(annotation = SaCheckPermission.class) + String [] value() default {}; + + /** + * 验证模式:AND | OR,默认AND + * @return 验证模式 + */ + @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/merge_annotation/SaUserCheckRole.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckRole.java new file mode 100644 index 000000000..ff81b6764 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckRole.java @@ -0,0 +1,38 @@ +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; + +/** + * 角色认证(User版):必须具有指定角色标识才能进入该方法 + *

可标注在函数、类上(效果等同于标注在此类的所有方法上) + * @author click33 + * + */ +@SaCheckRole(type = StpUserUtil.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface SaUserCheckRole { + + /** + * 需要校验的角色标识 + * @return 需要校验的角色标识 + */ + @AliasFor(annotation = SaCheckRole.class) + String [] value() default {}; + + /** + * 验证模式:AND | OR,默认AND + * @return 验证模式 + */ + @AliasFor(annotation = SaCheckRole.class) + SaMode mode() default SaMode.AND; + +} 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 000000000..60bd4e56e --- /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-case/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-case/src/main/resources/application.yml new file mode 100644 index 000000000..cc4bedb0f --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/resources/application.yml @@ -0,0 +1,51 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + # jwt 秘钥 + jwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-device-lock-h5/common.js b/sa-token-demo/sa-token-demo-device-lock-h5/common.js new file mode 100644 index 000000000..72bbbae4d --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock-h5/common.js @@ -0,0 +1,69 @@ +// 服务器接口主机地址 +var baseUrl = "http://localhost:8081"; + +// 封装一下Ajax +function ajax(path, data, successFn) { + console.log(baseUrl + path); + fetch(baseUrl + path, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'satoken': localStorage.getItem('satoken') + }, + body: serializeToQueryString(data), + }) + .then(response => response.json()) + .then(res => { + console.log('返回数据:', res); + successFn(res); + }) + .catch(error => { + console.error('提交失败:', error); + return alert("异常:" + JSON.stringify(error)); + }); +} + +// 获取本地的 设备id +function getLocalDeviceId() { + let localDeviceId = localStorage.getItem('local-device-id'); + if(!localDeviceId) { + localDeviceId = randomString(60); + localStorage.setItem('local-device-id', localDeviceId); + } + return localDeviceId; +} + + + +// ------------ 工具方法 --------------- + +// 从url中查询到指定名称的参数值 +function getParam(name, defaultValue){ + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i value != null) // 过滤 null 和 undefined + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); +} + +// 随机生成字符串 +function randomString(len) { +  len = len || 32; +  var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'; +  var maxPos = $chars.length; +  var str = ''; +  for (i = 0; i < len; i++) { +    str += $chars.charAt(Math.floor(Math.random() * maxPos)); +  } +  return str; +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-device-lock-h5/device-lock-auth.html b/sa-token-demo/sa-token-demo-device-lock-h5/device-lock-auth.html new file mode 100644 index 000000000..e9e559a57 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock-h5/device-lock-auth.html @@ -0,0 +1,69 @@ + + + + + 设备锁测试-认证页 + + + +

+ + + + + diff --git a/sa-token-demo/sa-token-demo-device-lock-h5/index.html b/sa-token-demo/sa-token-demo-device-lock-h5/index.html new file mode 100644 index 000000000..f9c109e8d --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock-h5/index.html @@ -0,0 +1,43 @@ + + + + + 设备锁测试-首页 + + +

设备锁测试-首页

+

当前是否登录:

+

+ 登录   + 注销 + +

+ + + + diff --git a/sa-token-demo/sa-token-demo-device-lock-h5/login.html b/sa-token-demo/sa-token-demo-device-lock-h5/login.html new file mode 100644 index 000000000..350958947 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock-h5/login.html @@ -0,0 +1,47 @@ + + + + + 设备锁测试-登录页 + + + + + + + + + diff --git a/sa-token-demo/sa-token-demo-device-lock/pom.xml b/sa-token-demo/sa-token-demo-device-lock/pom.xml new file mode 100644 index 000000000..6702d3e20 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-device-lock + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + + 1.45.0 + com.pj.SaTokenDeviceLockApplication + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/SaTokenDeviceLockApplication.java b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/SaTokenDeviceLockApplication.java new file mode 100644 index 000000000..b2ee68e66 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/SaTokenDeviceLockApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +/** + * Sa-Token 测试 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenDeviceLockApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenDeviceLockApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..ddc2c3e12 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,22 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + @ExceptionHandler + public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..0c5fa56bd --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,27 @@ +package com.pj.current; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 处理 404 + * @author click33 + */ +@RestController +public class NotFoundHandle implements ErrorController { + + @RequestMapping("/error") + public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setStatus(200); + return SaResult.get(404, "not found", null); + } + +} diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..dd0ab6806 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,87 @@ +package com.pj.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // 输出 API 请求日志,方便调试代码 +// SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行 + .setBeforeAuth(obj -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + + // ---------- 设置跨域响应头 ---------- + // 允许指定域访问跨域资源 + .setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..8531eaf64 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,90 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.pj.util.DeviceLockCheckUtil; +import com.pj.util.PhoneCodeUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 登录测试 + * + * @author click33 + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + @Autowired + SysUserMockDao userMockDao; + + // 账号密码登录 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd, String deviceId) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + long userId = userMockDao.getUserIdByName(name); + + // 登录前,检测设备锁 + if ( ! StpUtil.isTrustDeviceId(userId, deviceId)) { + DeviceLockCheckUtil.setDeviceIdToUserId(deviceId, 10001); + // 与前端约定好,返回421表示此设备需要验证 + return SaResult.get(421, "新设备登录,需要验证设备", deviceId); + } + + // 登录 + return login(userId, deviceId); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.data(StpUtil.isLogin()); + } + + // 注销登录 + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + + // 返回设备id绑定的 userId 的手机号,脱敏形式 + @RequestMapping("getPhone") + public SaResult getPhone(String deviceId) { + long userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId); + String phone = userMockDao.getPhoneByUserId(userId); + return SaResult.data(phone.substring(0, 3) + "****" + phone.substring(7)); + } + + // 发送验证码 + @RequestMapping("sendCode") + public SaResult sendCode(String deviceId) { + long userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId); + String phone = userMockDao.getPhoneByUserId(userId); + PhoneCodeUtil.sendCode(phone); + return SaResult.ok(); + } + + // 验证验证码 + @RequestMapping("checkCode") + public SaResult checkCode(String deviceId, String code) { + long userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId); + String phone = userMockDao.getPhoneByUserId(userId); + PhoneCodeUtil.checkCode(phone, code); + // 校验通过,开始登录 + return login(userId, deviceId); + } + + // 指定账号登录 + private SaResult login(long userId, String deviceId) { + StpUtil.login(userId, new SaLoginParameter().setDeviceId(deviceId)); + return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); + } + +} diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/SysUserMockDao.java b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/SysUserMockDao.java new file mode 100644 index 000000000..7d2cbe8bc --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/SysUserMockDao.java @@ -0,0 +1,24 @@ +package com.pj.test; + +import org.springframework.stereotype.Service; + +/** + * 模拟数据库操作类 + * + * @author click33 + * @since 2025/3/5 + */ +@Service +public class SysUserMockDao { + + // 返回指定 userId 绑定的手机号 + public String getPhoneByUserId(long userId) { + return "13112341234"; + } + + // 返回指定用户名对应的 userId + public long getUserIdByName(String name) { + return 10001; + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/DeviceLockCheckUtil.java b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/DeviceLockCheckUtil.java new file mode 100644 index 000000000..b4f67bd74 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/DeviceLockCheckUtil.java @@ -0,0 +1,42 @@ +package com.pj.util; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 设备锁操作工具类 + * @author click33 + * @since 2025/3/5 + */ +public class DeviceLockCheckUtil { + + /** + * 保存设备id与用户id的映射关系 + * @param deviceId / + * @param userId / + */ + public static void setDeviceIdToUserId(String deviceId, long userId) { + if(SaFoxUtil.isEmpty(deviceId) || SaFoxUtil.isEmpty(userId)) { + throw new RuntimeException("设备id或用户id不能为空"); + } + SaManager.getSaTokenDao().set(saveKeyPrefix() + deviceId, String.valueOf(userId), 1200); + } + + /** + * 返回设备id绑定的用户id + * @param deviceId / + */ + public static long getUserIdByDeviceId(String deviceId) { + String userIdStr = SaManager.getSaTokenDao().get(saveKeyPrefix() + deviceId); + if(userIdStr == null) { + throw new RuntimeException("此设备id目前未绑定任何用户"); + } + return Long.parseLong(userIdStr); + } + + // 返回数据保存时使用的前缀 + public static Object saveKeyPrefix() { + return SaManager.getConfig().getTokenName() + ":device-to-userid:"; + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/PhoneCodeUtil.java b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/PhoneCodeUtil.java new file mode 100644 index 000000000..7327b08dc --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/PhoneCodeUtil.java @@ -0,0 +1,31 @@ +package com.pj.util;//package com.pj.oauth2.custom; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 手机验证码工具类 (仅做逻辑模拟,不做真实发送) + * + * @author click33 + * @since 2024/8/23 + */ +public class PhoneCodeUtil { + + // 指定手机号发送验证码 + public static void sendCode(String phone) { + String code = SaFoxUtil.getRandomNumber(100000, 999999) + ""; + SaManager.getSaTokenDao().set("phone_code:" + phone, code, 60 * 5); + System.out.println("手机号:" + phone + ",验证码:" + code + ",已发送成功"); + } + + // 校验验证码是否正确,不正确则抛出异常 + public static void checkCode(String phone, String code) { + String oldCode = SaManager.getSaTokenDao().get("phone_code:" + phone); + if( ! code.equals(oldCode) ) { + throw new RuntimeException("验证码错误"); + } + // 验证通过后,立即删除验证码 + SaManager.getSaTokenDao().delete("phone_code:" + phone); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-device-lock/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-device-lock/src/main/resources/application.yml new file mode 100644 index 000000000..d0e2718e4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-device-lock/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..fd2972828 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/pom.xml @@ -0,0 +1,79 @@ + + 4.0.0 + com.pj + sa-token-demo-dubbo-consumer + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + + 2.5.15 + + + + + 1.8 + 3.1.1 + 1.45.0 + 2.7.21 + 1.4.2 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + org.apache.commons + commons-pool2 + + + + + org.apache.dubbo + dubbo-spring-boot-starter + ${dubbo.version} + + + + + org.apache.dubbo + dubbo-registry-nacos + ${dubbo.version} + + + com.alibaba.nacos + nacos-client + ${nacos.version} + + + + + cn.dev33 + sa-token-dubbo + ${sa-token.version} + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/DubboConsumerApplication.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/DubboConsumerApplication.java new file mode 100644 index 000000000..f4088e604 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/DubboConsumerApplication.java @@ -0,0 +1,22 @@ +package com.pj; + +import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Dubbo 服务消费端 + * + * @author click33 + * + */ +@EnableDubbo +@SpringBootApplication +public class DubboConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(DubboConsumerApplication.class, args); + System.out.println("DubboConsumerApplication 启动成功"); + } + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/controller/TestController.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/controller/TestController.java new file mode 100644 index 000000000..9c8d9af74 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/controller/TestController.java @@ -0,0 +1,73 @@ +package com.pj.controller; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.pj.service.DemoService; +import org.apache.dubbo.config.annotation.DubboReference; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @DubboReference + private DemoService demoService; + + // Consumer端登录,状态传播到Provider端 --- http://localhost:8081/test + @RequestMapping("test") + public SaResult test() { + demoService.isLogin("----------- 登录前 "); + + StpUtil.login(10001); + + demoService.isLogin("----------- 登录后 "); + + return SaResult.ok(); + } + + // Provider端登录,状态回传到Consumer端 --- http://localhost:8081/test2 + @RequestMapping("test2") + public SaResult test2() { + System.out.println("----------- 登录前 "); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + + demoService.doLogin(10002); + + System.out.println("----------- 登录后 "); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + + return SaResult.ok(); + } + + // Consumer端登录,状态在Consumer端保持 --- http://localhost:8081/test3 + @RequestMapping("test3") + public SaResult test3() { + System.out.println("----------- 登录前 "); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + + StpUtil.login(10003); + demoService.isLogin("----------- Provider状态"); + + System.out.println("----------- 登录后 "); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + + return SaResult.ok(); + } + + // Provider端登录,状态在Provider端保持 --- http://localhost:8081/test4 + @RequestMapping("test4") + public SaResult test4() { + // 登录 + demoService.doLogin(10004); + + // 打印一下 + demoService.isLogin("----------- 会话信息 "); + + return SaResult.ok(); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/service/DemoService.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/service/DemoService.java new file mode 100644 index 000000000..e502673d3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/service/DemoService.java @@ -0,0 +1,16 @@ +package com.pj.service; + +public interface DemoService { + + /** + * 登录 + * @param loginId 账号id + */ + void doLogin(Object loginId); + + /** + * 判断是否登录,打印状态 + */ + void isLogin(String str); + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/resources/application.yml new file mode 100644 index 000000000..a877a3664 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/resources/application.yml @@ -0,0 +1,24 @@ +server: + # 端口号 + port: 8081 + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + +dubbo: + application: + # 服务名称 + name: dubbo-consumer-demo + registry: + # 注册中心地址 + address: nacos://127.0.0.1:8001 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 new file mode 100644 index 000000000..f7c5f32c0 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/pom.xml @@ -0,0 +1,79 @@ + + 4.0.0 + com.pj + sa-token-demo-dubbo-provider + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + + 2.5.15 + + + + + 1.8 + 3.1.1 + 1.45.0 + 2.7.21 + 1.4.2 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + org.apache.commons + commons-pool2 + + + + + org.apache.dubbo + dubbo-spring-boot-starter + ${dubbo.version} + + + + + org.apache.dubbo + dubbo-registry-nacos + ${dubbo.version} + + + com.alibaba.nacos + nacos-client + ${nacos.version} + + + + + cn.dev33 + sa-token-dubbo + ${sa-token.version} + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/DubboProviderApplication.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/DubboProviderApplication.java new file mode 100644 index 000000000..728f998eb --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/DubboProviderApplication.java @@ -0,0 +1,22 @@ +package com.pj; + +import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Dubbo 服务提供端 + * + * @author click33 + * + */ +@EnableDubbo +@SpringBootApplication +public class DubboProviderApplication { + + public static void main(String[] args) { + SpringApplication.run(DubboProviderApplication.class, args); + System.out.println("DubboProviderApplication 启动成功"); + } + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/controller/TestController.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/controller/TestController.java new file mode 100644 index 000000000..b745296b6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/controller/TestController.java @@ -0,0 +1,28 @@ +package com.pj.controller; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.pj.service.DemoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @Autowired + private DemoService demoService; + + // test + @RequestMapping("test") + public SaResult test() { + demoService.isLogin("----------- 登录前 " + StpUtil.isLogin()); + + StpUtil.login(10001); + + demoService.isLogin("----------- 登录后 " + StpUtil.isLogin()); + + return SaResult.ok(); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoService.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoService.java new file mode 100644 index 000000000..e502673d3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoService.java @@ -0,0 +1,16 @@ +package com.pj.service; + +public interface DemoService { + + /** + * 登录 + * @param loginId 账号id + */ + void doLogin(Object loginId); + + /** + * 判断是否登录,打印状态 + */ + void isLogin(String str); + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoServiceImpl.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoServiceImpl.java new file mode 100644 index 000000000..ab7341efe --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoServiceImpl.java @@ -0,0 +1,23 @@ +package com.pj.service; + +import org.apache.dubbo.config.annotation.DubboService; + +import cn.dev33.satoken.stp.StpUtil; + +@DubboService() +public class DemoServiceImpl implements DemoService { + + @Override + public void doLogin(Object loginId) { + StpUtil.login(loginId); + } + + @Override + public void isLogin(String str) { + System.out.println(str); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + } + +} + diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/resources/application.yml new file mode 100644 index 000000000..1aac2ea79 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/resources/application.yml @@ -0,0 +1,38 @@ +server: + # 端口号 + port: 8080 + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + +# Dubbo +dubbo: + # 服务名 + application: + name: dubbo-provider-demo + # 扫描包 + scan: + base-packages: com.pj + # 注册中心地址 + registry: + address: nacos://127.0.0.1:8001 + # 协议 + protocol: + name: dubbo + port: 12345 + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..a2717c6a9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/pom.xml @@ -0,0 +1,80 @@ + + 4.0.0 + com.pj + sa-token-demo-dubbo3-consumer + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + + + 3.4.3 + + + + + 17 + 3.1.1 + 1.45.0 + 3.2.2 + 2.2.2 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + org.apache.commons + commons-pool2 + + + + + org.apache.dubbo + dubbo-spring-boot-starter + ${dubbo.version} + + + + + org.apache.dubbo + dubbo-registry-nacos + ${dubbo.version} + + + com.alibaba.nacos + nacos-client + ${nacos.version} + + + + + cn.dev33 + sa-token-dubbo3 + ${sa-token.version} + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/Dubbo3ConsumerApplication.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/Dubbo3ConsumerApplication.java new file mode 100644 index 000000000..1df58fcaa --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/Dubbo3ConsumerApplication.java @@ -0,0 +1,22 @@ +package com.pj; + +import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Dubbo3 服务消费端 + * + * @author click33 + * + */ +@EnableDubbo +@SpringBootApplication +public class Dubbo3ConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(Dubbo3ConsumerApplication.class, args); + System.out.println("Dubbo3ConsumerApplication 启动成功"); + } + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/controller/TestController.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/controller/TestController.java new file mode 100644 index 000000000..fb40bde24 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/controller/TestController.java @@ -0,0 +1,73 @@ +package com.pj.controller; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.pj.service.DemoService; +import org.apache.dubbo.config.annotation.DubboReference; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @DubboReference + private DemoService demoService; + + // Consumer端登录,状态传播到Provider端 + @RequestMapping("test") + public SaResult test() { + demoService.isLogin("----------- 登录前 "); + + StpUtil.login(10001); + + demoService.isLogin("----------- 登录后 "); + + return SaResult.ok(); + } + + // Provider端登录,状态回传到Consumer端 + @RequestMapping("test2") + public SaResult test2() { + System.out.println("----------- 登录前 "); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + + demoService.doLogin(10002); + + System.out.println("----------- 登录后 "); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + + return SaResult.ok(); + } + + // Consumer端登录,状态在Consumer端保持 + @RequestMapping("test3") + public SaResult test3() { + System.out.println("----------- 登录前 "); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + + StpUtil.login(10003); + demoService.isLogin("----------- Provider状态"); + + System.out.println("----------- 登录后 "); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + + return SaResult.ok(); + } + + // Provider端登录,状态在Provider端保持 + @RequestMapping("test4") + public SaResult test4() { + // 登录 + demoService.doLogin(10004); + + // 打印一下 + demoService.isLogin("----------- 会话信息 "); + + return SaResult.ok(); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/service/DemoService.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/service/DemoService.java new file mode 100644 index 000000000..e502673d3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/service/DemoService.java @@ -0,0 +1,16 @@ +package com.pj.service; + +public interface DemoService { + + /** + * 登录 + * @param loginId 账号id + */ + void doLogin(Object loginId); + + /** + * 判断是否登录,打印状态 + */ + void isLogin(String str); + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/resources/application.yml new file mode 100644 index 000000000..9a64a4591 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + # 端口号 + port: 8081 + +spring: + data: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + +dubbo: + application: + # 服务名称 + name: dubbo-consumer-demo + registry: + # 注册中心地址 + address: nacos://127.0.0.1:8001 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 new file mode 100644 index 000000000..5a275953d --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/pom.xml @@ -0,0 +1,80 @@ + + 4.0.0 + com.pj + sa-token-demo-dubbo3-provider + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + + + 3.4.3 + + + + + 17 + 3.1.1 + 1.45.0 + 3.2.2 + 2.2.2 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + org.apache.commons + commons-pool2 + + + + + org.apache.dubbo + dubbo-spring-boot-starter + ${dubbo.version} + + + + + org.apache.dubbo + dubbo-registry-nacos + ${dubbo.version} + + + com.alibaba.nacos + nacos-client + ${nacos.version} + + + + + cn.dev33 + sa-token-dubbo3 + ${sa-token.version} + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/Dubbo3ProviderApplication.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/Dubbo3ProviderApplication.java new file mode 100644 index 000000000..70e66b503 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/Dubbo3ProviderApplication.java @@ -0,0 +1,22 @@ +package com.pj; + +import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Dubbo3 服务提供端 + * + * @author click33 + * + */ +@EnableDubbo +@SpringBootApplication +public class Dubbo3ProviderApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(Dubbo3ProviderApplication.class, args); + System.out.println("Dubbo3ProviderApplication 启动成功"); + } + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/controller/TestController.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/controller/TestController.java new file mode 100644 index 000000000..c3f352b5c --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/controller/TestController.java @@ -0,0 +1,37 @@ +package com.pj.controller; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.pj.service.DemoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + +// 如果把 @Autowired 改为 @DubboReference +// 则可能在首次调用 dubbo 服务时控制台出现以下异常(只打印异常信息,不影响调用): +// java.lang.reflect.InaccessibleObjectException: Unable to make field private byte java.lang.StackTraceElement.format accessible: +// module java.base does not "opens java.lang" to unnamed module @3a52dba3 +// +// 在启动参数上加上如下即可解决: +// --add-opens java.base/java.math=ALL-UNNAMED + + @Autowired + public DemoService demoService; + + // test + @RequestMapping("test") + public SaResult test() { + demoService.isLogin("----------- 登录前 " + StpUtil.isLogin()); + + StpUtil.login(10001); + + demoService.isLogin("----------- 登录后 " + StpUtil.isLogin()); + + return SaResult.ok(); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoService.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoService.java new file mode 100644 index 000000000..e502673d3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoService.java @@ -0,0 +1,16 @@ +package com.pj.service; + +public interface DemoService { + + /** + * 登录 + * @param loginId 账号id + */ + void doLogin(Object loginId); + + /** + * 判断是否登录,打印状态 + */ + void isLogin(String str); + +} diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoServiceImpl.java b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoServiceImpl.java new file mode 100644 index 000000000..ab7341efe --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoServiceImpl.java @@ -0,0 +1,23 @@ +package com.pj.service; + +import org.apache.dubbo.config.annotation.DubboService; + +import cn.dev33.satoken.stp.StpUtil; + +@DubboService() +public class DemoServiceImpl implements DemoService { + + @Override + public void doLogin(Object loginId) { + StpUtil.login(loginId); + } + + @Override + public void isLogin(String str) { + System.out.println(str); + System.out.println("Token值:" + StpUtil.getTokenValue()); + System.out.println("是否登录:" + StpUtil.isLogin()); + } + +} + diff --git a/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/resources/application.yml new file mode 100644 index 000000000..e432fa80e --- /dev/null +++ b/sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/resources/application.yml @@ -0,0 +1,39 @@ +server: + # 端口号 + port: 8080 + +spring: + data: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + +# Dubbo +dubbo: + # 服务名 + application: + name: dubbo-provider-demo + # 扫描包 + scan: + base-packages: com.pj + # 注册中心地址 + registry: + address: nacos://127.0.0.1:8001 + # 协议 + protocol: + name: dubbo + port: 12345 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-first-run/pom.xml b/sa-token-demo/sa-token-demo-first-run/pom.xml new file mode 100644 index 000000000..d0988fc73 --- /dev/null +++ b/sa-token-demo/sa-token-demo-first-run/pom.xml @@ -0,0 +1,73 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-first-run + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-apikey + ${sa-token.version} + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + cn.dev33 + sa-token-oauth2 + ${sa-token.version} + + + cn.dev33 + sa-token-sign + ${sa-token.version} + + + cn.dev33 + sa-token-jwt + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + diff --git a/sa-token-demo/sa-token-demo-first-run/src/main/java/com/pj/SaTokenDemoFirstRunApplication.java b/sa-token-demo/sa-token-demo-first-run/src/main/java/com/pj/SaTokenDemoFirstRunApplication.java new file mode 100644 index 000000000..2f966620b --- /dev/null +++ b/sa-token-demo/sa-token-demo-first-run/src/main/java/com/pj/SaTokenDemoFirstRunApplication.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 源码首次运行引导 Demo。 + *

+ * 强制引入 apikey、sso、oauth2、sign 四个可选模块,引导 IDEA 编译相关插件后再运行其它 demo。 + *

+ * + * @author click33 + */ +@SpringBootApplication +public class SaTokenDemoFirstRunApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenDemoFirstRunApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-first-run/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-first-run/src/main/resources/application.yml new file mode 100644 index 000000000..ff679393f --- /dev/null +++ b/sa-token-demo/sa-token-demo-first-run/src/main/resources/application.yml @@ -0,0 +1,13 @@ +# 端口 +server: + port: 8099 + +# sa-token 配置 +sa-token: + token-name: satoken + timeout: 2592000 + active-timeout: -1 + is-concurrent: true + is-share: false + token-style: uuid + is-log: true diff --git a/sa-token-demo/sa-token-demo-freemarker/pom.xml b/sa-token-demo/sa-token-demo-freemarker/pom.xml new file mode 100644 index 000000000..dea040175 --- /dev/null +++ b/sa-token-demo/sa-token-demo-freemarker/pom.xml @@ -0,0 +1,71 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-freemarker + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-freemarker + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-devtools + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + diff --git a/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/SaTokenFreemarkerDemoApplication.java b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/SaTokenFreemarkerDemoApplication.java new file mode 100644 index 000000000..c7a561e02 --- /dev/null +++ b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/SaTokenFreemarkerDemoApplication.java @@ -0,0 +1,19 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 参考链接:https://blog.csdn.net/m0_64210833/article/details/135994864 + */ +@SpringBootApplication +public class SaTokenFreemarkerDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenFreemarkerDemoApplication.class, args); + System.out.println("\n启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); + System.out.println("\n测试访问:http://localhost:8081/"); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..a5b6b806d --- /dev/null +++ b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,39 @@ +package com.pj.satoken; + +import cn.dev33.satoken.freemarker.dialect.SaTokenTemplateModel; +import cn.dev33.satoken.stp.StpUtil; +import freemarker.template.TemplateModelException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +import javax.annotation.PostConstruct; + + +/** + * [Sa-Token 权限认证] 配置类 + * + * @author click33 + */ +@Configuration +public class SaTokenConfigure { + + @Autowired + FreeMarkerConfigurer configurer; + + /** + * 注入 Sa-Token Freemarker 标签模板模型 对象 + */ + @PostConstruct + public void setSaTokenTemplateModel() throws TemplateModelException { + + // 注入 Sa-Token Freemarker 标签模板模型,使之可以在 xxx.ftl 文件中使用 sa 标签, + // 例如:<#if sa.login()>... + configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel()); + + // 注入 Sa-Token Freemarker 全局对象,使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法, + // 例如:${stp.getSession().get('name')} + configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic); + } + +} diff --git a/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..6cfd8edb7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,43 @@ +package com.pj.satoken; + +import cn.dev33.satoken.stp.StpInterface; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/GlobalException.java new file mode 100644 index 000000000..92d092cf7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/GlobalException.java @@ -0,0 +1,23 @@ +package com.pj.test; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..a6b7fc505 --- /dev/null +++ b/sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,39 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +/** + * 测试 Controller + * + * @author click33 + */ +@RestController +public class TestController { + + // 首页 + @RequestMapping("/") + public Object index() { + return new ModelAndView("index"); + } + + // 登录 + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue="10001") String id) { + StpUtil.login(id); + StpUtil.getSession().set("name", "zhangsan"); + return SaResult.ok(); + } + + // 注销 + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-freemarker/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-freemarker/src/main/resources/application.yml new file mode 100644 index 000000000..8cb7f5fbc --- /dev/null +++ b/sa-token-demo/sa-token-demo-freemarker/src/main/resources/application.yml @@ -0,0 +1,16 @@ +# 端口 +server: + port: 8081 + +spring: + # Freemarker 相关配置 + freemarker: + # 指定模板文件的目录 + template-loader-path: classpath:/templates + # 指定Freemarker模板文件的后缀名 + suffix: .ftl + # 关闭模板缓存,方便测试 + cache: false + # 检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 + settings: + template_update_delay: 0 \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-freemarker/src/main/resources/templates/index.ftl b/sa-token-demo/sa-token-demo-freemarker/src/main/resources/templates/index.ftl new file mode 100644 index 000000000..6989aee01 --- /dev/null +++ b/sa-token-demo/sa-token-demo-freemarker/src/main/resources/templates/index.ftl @@ -0,0 +1,39 @@ + + + + Sa-Token 集成 Freemarker 标签方言 + + + + +
+

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

+

当前是否登录:<#if stp.isLogin()>是<#else>否

+

+ 登录 + 注销 +

+ +

登录之后才能显示:<@sa.login>value

+

不登录才能显示:<@sa.notLogin>value

+ +

具有角色 admin 才能显示:<@sa.hasRole value="admin">value

+

同时具备多个角色才能显示:<@sa.hasRoleAnd value="admin, ceo, cto">value

+

只要具有其中一个角色就能显示:<@sa.hasRoleOr value="admin, ceo, cto">value

+

不具有角色 admin 才能显示:<@sa.notRole value="admin">value

+ +

具有权限 user-add 才能显示:<@sa.hasPermission value="user-add">value

+

同时具备多个权限才能显示:<@sa.hasPermissionAnd value="user-add, user-delete, user-get">value

+

只要具有其中一个权限就能显示:<@sa.hasPermissionOr value="user-add, user-delete, user-get">value

+

不具有权限 user-add 才能显示:<@sa.notPermission value="user-add">value

+ +

+ 从SaSession中取值: + <#if stp.isLogin()> + ${stp.getSession().get('name')} + +

+ +
+ + diff --git a/sa-token-demo/sa-token-demo-grpc/client/pom.xml b/sa-token-demo/sa-token-demo-grpc/client/pom.xml new file mode 100644 index 000000000..e1799ba5f --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/client/pom.xml @@ -0,0 +1,20 @@ + + + + sa-token-demo-grpc + com.lym + 0.0.1-SNAPSHOT + + 4.0.0 + + client + + + 8 + 8 + UTF-8 + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/Client.java b/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/Client.java new file mode 100644 index 000000000..3a3d69e9e --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/Client.java @@ -0,0 +1,18 @@ +package com.lym; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * @author lym + * @description + * @date 2022/8/26 10:58 + **/ +@SpringBootApplication +@EnableDiscoveryClient +public class Client { + public static void main(String[] args) { + SpringApplication.run(Client.class); + } +} diff --git a/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/controller/TestController.java b/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/controller/TestController.java new file mode 100644 index 000000000..da36bdaee --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/controller/TestController.java @@ -0,0 +1,48 @@ +package com.lym.controller; + +import cn.dev33.satoken.stp.StpUtil; +import com.lym.grpc.client.GrpcAuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author lym + * @description + * @date 2022/8/26 11:01 + **/ +@RestController +public class TestController { + @Autowired + private GrpcAuthService grpcAuthService; + + // 客户端登录,状态带到服务端。 + @RequestMapping("test") + public void test() { + System.out.println("登录前:" + grpcAuthService.isLogin()); + System.out.println("登录前:" + StpUtil.isLogin()); + + StpUtil.login(1); + + System.out.println("登录后:" + grpcAuthService.isLogin()); + System.out.println("登录后:" + StpUtil.getTokenValue()); + System.out.println("登录后:" + StpUtil.getLoginId()); + } + + // 服务端登录,登录状态带回客户端 + @RequestMapping("test2") + public String test2() { + System.out.println("登录前:" + grpcAuthService.isLogin()); + System.out.println("登录前:" + StpUtil.isLogin()); + + String token = grpcAuthService.login(3); + + System.out.println("登录后:" + grpcAuthService.isLogin()); + System.out.println("登录后:" + StpUtil.getTokenValue()); + System.out.println("登录后:" + StpUtil.getLoginId()); + assert StpUtil.getTokenValue().equals(token); + + return token; + } + +} diff --git a/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/grpc/client/GrpcAuthService.java b/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/grpc/client/GrpcAuthService.java new file mode 100644 index 000000000..53da9e69b --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/grpc/client/GrpcAuthService.java @@ -0,0 +1,28 @@ +package com.lym.grpc.client; + +import com.google.protobuf.Empty; +import com.lym.grpc.auth.Auth; +import com.lym.grpc.auth.AuthServiceGrpc; +import net.devh.boot.grpc.client.inject.GrpcClient; +import org.springframework.stereotype.Service; + +/** + * @author lym + * @description + * @date 2022/8/26 11:02 + **/ +@Service +public class GrpcAuthService { + @GrpcClient("test-server") + private AuthServiceGrpc.AuthServiceBlockingStub grpcAuthService; + + public boolean isLogin() { + Auth.GrpcBool resp = grpcAuthService.isLogin(Empty.getDefaultInstance()); + return resp.getVal(); + } + + public String login(Integer id) { + Auth.GrpcString resp = grpcAuthService.login(Auth.GrpcInt.newBuilder().setVal(id).build()); + return resp.getVal(); + } +} diff --git a/sa-token-demo/sa-token-demo-grpc/client/src/main/proto/auth.proto b/sa-token-demo/sa-token-demo-grpc/client/src/main/proto/auth.proto new file mode 100644 index 000000000..2113a5d41 --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/client/src/main/proto/auth.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package auth; + +option java_package = "com.lym.grpc.auth"; +import "google/protobuf/empty.proto"; + +message GrpcBool{ + bool val = 1; +} + +message GrpcInt{ + int32 val = 1; +} + +message GrpcString{ + string val = 1; +} + + +service AuthService{ + rpc isLogin(google.protobuf.Empty) returns (GrpcBool); + rpc login(GrpcInt) returns (GrpcString); +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-grpc/client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-grpc/client/src/main/resources/application.yml new file mode 100644 index 000000000..d85332afb --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/client/src/main/resources/application.yml @@ -0,0 +1,18 @@ +server: + port: 2222 +spring: + application: + name: test-client + redis: + host: localhost + cloud: + nacos: + server-addr: localhost:8848 +sa-token: + is-read-cookie: false +grpc: + server: + port: 2223 + client: + test-server: + negotiation-type: PLAINTEXT \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-grpc/pom.xml b/sa-token-demo/sa-token-demo-grpc/pom.xml new file mode 100644 index 000000000..1be7b6212 --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + pom + + client + server + + + org.springframework.boot + spring-boot-starter-parent + 2.6.3 + + + com.lym + sa-token-demo-grpc + 0.0.1-SNAPSHOT + sa-token-demo-grpc + sa-token-demo-grpc + + + 8 + 8 + 1.8 + UTF-8 + UTF-8 + UTF-8 + 1.18.10 + 1.45.0 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + cn.dev33 + sa-token-grpc + ${sa-token.version} + + + + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2021.0.1.0 + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + 2021.0.1 + pom + import + + + + + + + + kr.motd.maven + os-maven-plugin + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + com.google.protobuf:protoc:3.1.0:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:1.12.0:exe:${os.detected.classifier} + + + + + compile + compile-custom + + + + + + + + diff --git a/sa-token-demo/sa-token-demo-grpc/server/pom.xml b/sa-token-demo/sa-token-demo-grpc/server/pom.xml new file mode 100644 index 000000000..a48404a40 --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/server/pom.xml @@ -0,0 +1,20 @@ + + + + sa-token-demo-grpc + com.lym + 0.0.1-SNAPSHOT + + 4.0.0 + + server + + + 8 + 8 + UTF-8 + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/Server.java b/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/Server.java new file mode 100644 index 000000000..f9314ac73 --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/Server.java @@ -0,0 +1,18 @@ +package com.lym; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * @author lym + * @description + * @date 2022/8/26 10:58 + **/ +@SpringBootApplication +@EnableDiscoveryClient +public class Server { + public static void main(String[] args) { + SpringApplication.run(Server.class); + } +} diff --git a/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/grpc/server/GrpcAuthService.java b/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/grpc/server/GrpcAuthService.java new file mode 100644 index 000000000..40cf0c836 --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/grpc/server/GrpcAuthService.java @@ -0,0 +1,37 @@ +package com.lym.grpc.server; + +import cn.dev33.satoken.stp.StpUtil; +import com.google.protobuf.Empty; +import com.lym.grpc.auth.Auth; +import com.lym.grpc.auth.AuthServiceGrpc; +import com.lym.service.AuthService; +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.server.service.GrpcService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author lym + * @description + * @date 2022/8/26 11:29 + **/ +@GrpcService +public class GrpcAuthService extends AuthServiceGrpc.AuthServiceImplBase { + + @Autowired + private AuthService authService; + + @Override + public void isLogin(Empty request, StreamObserver responseObserver) { + boolean isLogin = authService.isLogin(); + responseObserver.onNext(Auth.GrpcBool.newBuilder().setVal(isLogin).build()); + responseObserver.onCompleted(); + } + + @Override + public void login(Auth.GrpcInt request, StreamObserver responseObserver) { + StpUtil.login(request.getVal()); + Auth.GrpcString resp = Auth.GrpcString.newBuilder().setVal(StpUtil.getTokenValue()).build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); + } +} diff --git a/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/service/AuthService.java b/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/service/AuthService.java new file mode 100644 index 000000000..f14ce36b9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/service/AuthService.java @@ -0,0 +1,23 @@ +package com.lym.service; + +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.stereotype.Service; + +/** + * @author lym + * @description + * @date 2022/8/26 11:30 + **/ +@Service +public class AuthService { + public boolean isLogin() { + if (StpUtil.isLogin()) { + System.out.println("id:" + StpUtil.getLoginIdAsInt()); + System.out.println("token:" + StpUtil.getTokenValue()); + } else { + System.out.println("未登录"); + } + + return StpUtil.isLogin(); + } +} diff --git a/sa-token-demo/sa-token-demo-grpc/server/src/main/proto/auth.proto b/sa-token-demo/sa-token-demo-grpc/server/src/main/proto/auth.proto new file mode 100644 index 000000000..2113a5d41 --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/server/src/main/proto/auth.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package auth; + +option java_package = "com.lym.grpc.auth"; +import "google/protobuf/empty.proto"; + +message GrpcBool{ + bool val = 1; +} + +message GrpcInt{ + int32 val = 1; +} + +message GrpcString{ + string val = 1; +} + + +service AuthService{ + rpc isLogin(google.protobuf.Empty) returns (GrpcBool); + rpc login(GrpcInt) returns (GrpcString); +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-grpc/server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-grpc/server/src/main/resources/application.yml new file mode 100644 index 000000000..feff9074e --- /dev/null +++ b/sa-token-demo/sa-token-demo-grpc/server/src/main/resources/application.yml @@ -0,0 +1,16 @@ +server: + port: 5555 +spring: + application: + name: test-server + redis: + host: localhost + cloud: + nacos: + server-addr: localhost:8848 +sa-token: + is-read-cookie: false + +grpc: + server: + port: 5556 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 new file mode 100644 index 000000000..0c83aa686 --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/pom.xml @@ -0,0 +1,59 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-hutool-timed-cache + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-hutool-timed-cache + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/SaTokenDemoApplication.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/SaTokenDemoApplication.java new file mode 100644 index 000000000..19f19d424 --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/SaTokenDemoApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 Hutool-TimedCache 示例 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenDemoApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + System.out.println(SaManager.getSaTokenDao()); + } + +} diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..1d6ebb1cd --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,57 @@ +package com.pj.current; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.exception.DisableServiceException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 不同异常返回不同状态码 + AjaxJson aj = null; + if (e instanceof NotLoginException) { // 如果是未登录异常 + NotLoginException ee = (NotLoginException) e; + aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); + } + else if(e instanceof NotRoleException) { // 如果是角色异常 + NotRoleException ee = (NotRoleException) e; + aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); + } + else if(e instanceof NotPermissionException) { // 如果是权限异常 + NotPermissionException ee = (NotPermissionException) e; + aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); + } + else if(e instanceof DisableServiceException) { // 如果是被封禁异常 + DisableServiceException ee = (DisableServiceException) e; + aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); + } + else { // 普通异常, 输出:500 + 异常信息 + aj = AjaxJson.getError(e.getMessage()); + } + + // 返回给前端 + return aj; + } + +} diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..81ff22e8e --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,27 @@ +package com.pj.current; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 处理 404 + * @author click33 + */ +@RestController +public class NotFoundHandle implements ErrorController { + + @RequestMapping("/error") + public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setStatus(200); + return SaResult.get(404, "not found", null); + } + +} diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..9dd25cd95 --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,69 @@ +package com.pj.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行 (BeforeAuth不受 includeList 与 excludeList 的限制,所有请求都会进入) + .setBeforeAuth(r -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + ; + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..b6cc79f71 --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..34eea9a19 --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/StressTestController.java new file mode 100644 index 000000000..493134c92 --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/StressTestController.java @@ -0,0 +1,59 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.pj.util.Ttime; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +/** + * 压力测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/s-test/") +public class StressTestController { + + // 测试 浏览器访问: http://localhost:8081/s-test/login + // 测试前,请先将 is-read-cookie 配置为 false + @RequestMapping("login") + public SaResult login() { +// StpUtil.getTokenSession().logout(); +// StpUtil.logoutByLoginId(10001); + + int count = 10; // 循环多少轮 + int loginCount = 10000; // 每轮循环多少次 + + // 循环10次 取平均时间 + List list = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + System.out.println("\n---------------------第" + i + "轮---------------------"); + Ttime t = new Ttime().start(); + // 每次登录的次数 + for (int j = 1; j <= loginCount; j++) { + StpUtil.login("1000" + j, "PC-" + j); + if(j % 1000 == 0) { + System.out.println("已登录:" + j); + } + } + t.end(); + list.add((t.returnMs() + 0.0) / 1000); + System.out.println("第" + i + "轮" + "用时:" + t.toString()); + } +// System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); + + System.out.println("\n---------------------测试结果---------------------"); + System.out.println(list.size() + "次测试: " + list); + double ss = 0; + for (int i = 0; i < list.size(); i++) { + ss += list.get(i); + } + System.out.println("平均用时: " + ss / list.size()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..9838063e8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,30 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + // 测试 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("------------进来了"); + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8081/test/test2 + @RequestMapping("test2") + public SaResult test2() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/AjaxJson.java similarity index 100% rename from sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/util/AjaxJson.java rename to sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/AjaxJson.java diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/Ttime.java b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/Ttime.java new file mode 100644 index 000000000..dec4f1cbe --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/Ttime.java @@ -0,0 +1,63 @@ +package com.pj.util; + + +/** + * 用于测试用时 + * @author click33 + * + */ +public class Ttime { + + private long start=0; //开始时间 + private long end=0; //结束时间 + + public static Ttime t = new Ttime(); //static快捷使用 + + /** + * 开始计时 + * @return + */ + public Ttime start() { + start=System.currentTimeMillis(); + return this; + } + + + /** + * 结束计时 + */ + public Ttime end() { + end=System.currentTimeMillis(); + return this; + } + + + /** + * 返回所用毫秒数 + */ + public long returnMs() { + return end-start; + } + + /** + * 格式化输出结果 + */ + public void outTime() { + System.out.println(this.toString()); + } + + /** + * 结束并格式化输出结果 + */ + public void endOutTime() { + this.end().outTime(); + } + + @Override + public String toString() { + return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s + } + + + +} diff --git a/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/resources/application.yml new file mode 100644 index 000000000..1889895e5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-jwt/.gitignore b/sa-token-demo/sa-token-demo-jwt/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-jwt/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-jwt/pom.xml b/sa-token-demo/sa-token-demo-jwt/pom.xml index 5677ce5a8..d025e264d 100644 --- a/sa-token-demo/sa-token-demo-jwt/pom.xml +++ b/sa-token-demo/sa-token-demo-jwt/pom.xml @@ -10,13 +10,13 @@ org.springframework.boot spring-boot-starter-parent - 2.0.0.RELEASE + 2.5.14 - + - 1.24.0 + 1.45.0 @@ -26,52 +26,32 @@ org.springframework.boot spring-boot-starter-web - - org.springframework.boot - spring-boot-starter-aop - - + cn.dev33 sa-token-spring-boot-starter - ${sa-token-version} + ${sa-token.version} - - - io.jsonwebtoken - jjwt - 0.9.1 - - - - - - - + cn.dev33 - sa-token-dao-redis-jackson - ${sa-token-version} - --> + sa-token-jwt + ${sa-token.version} + - - + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + org.apache.commons commons-pool2 - --> + - - - org.springframework.boot @@ -79,12 +59,13 @@ true + - javax.xml.bind - jaxb-api - + org.springframework.boot + spring-boot-starter-test + test - + diff --git a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/SaTokenJwtDemoApplication.java b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/SaTokenJwtDemoApplication.java index f06b978ea..b86a73941 100644 --- a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/SaTokenJwtDemoApplication.java +++ b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/SaTokenJwtDemoApplication.java @@ -1,16 +1,15 @@ package com.pj; +import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import cn.dev33.satoken.SaManager; - @SpringBootApplication public class SaTokenJwtDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenJwtDemoApplication.class, args); - System.out.println("\n启动成功:sa-token配置如下:" + SaManager.getConfig()); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } - + } \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..75ef0e096 --- /dev/null +++ b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,39 @@ +package com.pj.satoken; + +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.jwt.StpLogicJwtForSimple; +import cn.dev33.satoken.stp.StpLogic; +import org.springframework.context.annotation.Bean; +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("/**"); + } + + /** + * Sa-Token 整合 jwt + */ + @Bean + public StpLogic getStpLogicJwt() { + return new StpLogicJwtForSimple(); +// return new StpLogicJwtForMixin(); +// return new StpLogicJwtForStateless(); + } + +} diff --git a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java deleted file mode 100644 index 3c89c650c..000000000 --- a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/satoken/jwt/SaTokenJwtUtil.java +++ /dev/null @@ -1,235 +0,0 @@ -package com.pj.satoken.jwt; - -import java.util.Date; - -import org.springframework.stereotype.Component; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.context.model.SaStorage; -import cn.dev33.satoken.dao.SaTokenDao; -import cn.dev33.satoken.exception.NotLoginException; -import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.session.SaSession; -import cn.dev33.satoken.stp.SaLoginModel; -import cn.dev33.satoken.stp.SaTokenInfo; -import cn.dev33.satoken.stp.StpLogic; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaTokenConsts; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureAlgorithm; - -@Component -public class SaTokenJwtUtil { - - /** - * 秘钥 (随便手打几个字母就好了) - */ - public static final String BASE64_SECURITY = "79e7c69681b8270162386e6daa53d1dd"; - - /** - * token有效期 (单位: 秒) - */ - public static final long TIMEOUT = 60 * 60 * 2; - - - public static final String LOGIN_ID_KEY = "loginId"; - - - /** - * 根据userId生成token - * @param loginId 账号id - * @param base64Security 秘钥 - * @return jwt-token - */ - public static String createToken(Object loginId) { - // 判断,不可使用默认秘钥 -// if(BASE64_SECURITY.equals("79e7c69681b8270162386e6daa53d1dd")) { -// throw new SaTokenException("请更换秘钥"); -// } - // 在这里你可以使用官方提供的claim方法构建载荷,也可以使用setPayload自定义载荷,但是两者不可一起使用 - JwtBuilder builder = Jwts.builder() - .setHeaderParam("type", "JWT") - .claim(LOGIN_ID_KEY, loginId) - .setIssuedAt(new Date()) // 签发日期 - .setExpiration(new Date(System.currentTimeMillis() + 1000 * TIMEOUT)) // 有效截止日期 - .signWith(SignatureAlgorithm.HS256, BASE64_SECURITY.getBytes()); // 加密算法 - //生成JWT - return builder.compact(); - } - - /** - * 从一个jwt里面解析出Claims - * @param tokenValue token值 - * @param base64Security 秘钥 - * @return Claims对象 - */ - public static Claims getClaims(String tokenValue) { -// System.out.println(tokenValue); - Claims claims = Jwts.parser() - .setSigningKey(BASE64_SECURITY.getBytes()) - .parseClaimsJws(tokenValue).getBody(); - return claims; - } - - /** - * 从一个jwt里面解析loginId - * @param tokenValue token值 - * @param base64Security 秘钥 - * @return loginId - */ - public static String getLoginId(String tokenValue) { - try { - Object loginId = getClaims(tokenValue).get(LOGIN_ID_KEY); - if(loginId == null) { - return null; - } - return String.valueOf(loginId); - } catch (ExpiredJwtException e) { -// throw NotLoginException.newInstance(StpUtil.TYPE, NotLoginException.TOKEN_TIMEOUT); - return NotLoginException.TOKEN_TIMEOUT; - } catch (MalformedJwtException e) { - throw NotLoginException.newInstance(StpUtil.stpLogic.loginType, NotLoginException.INVALID_TOKEN); - } catch (Exception e) { - throw new SaTokenException(e); - } - } - - - - static { - - // 判断秘钥 - if(BASE64_SECURITY.equals("79e7c69681b8270162386e6daa53d1dd")) { - String warn = "-------------------------------------\n"; - warn += "请更换JWT秘钥,不要使用示例默认秘钥\n"; - warn += "-------------------------------------"; - System.err.println(warn); - } - - // 提前调用一下方法,促使其属性初始化 - StpUtil.getLoginType(); - - // 修改默认实现 - StpUtil.stpLogic = new StpLogic("login") { - - // 重写 (随机生成一个tokenValue) - @Override - public String createTokenValue(Object loginId) { - return SaTokenJwtUtil.createToken(loginId); - } - - // 重写 (在当前会话上登录id ) - @Override - public void login(Object loginId, SaLoginModel loginModel) { - // ------ 1、获取相应对象 - SaStorage storage = SaManager.getSaTokenContext().getStorage(); - SaTokenConfig config = getConfig(); - // ------ 2、生成一个token - String tokenValue = createTokenValue(loginId); - storage.set(splicingKeyJustCreatedSave(), tokenValue); // 将token保存到本次request里 - if(config.getIsReadCookie() == true){ // cookie注入 - SaManager.getSaTokenContext().getResponse().addCookie(getTokenName(), tokenValue, "/", config.getCookieDomain(), (int)config.getTimeout()); - } - } - - // 重写 (获取指定token对应的登录id) - @Override - public String getLoginIdNotHandle(String tokenValue) { - try { - return SaTokenJwtUtil.getLoginId(tokenValue); - } catch (Exception e) { - return null; - } - } - - // 重写 (当前会话注销登录) - @Override - public void logout() { - // 如果连token都没有,那么无需执行任何操作 - String tokenValue = getTokenValue(); - if(tokenValue == null) { - return; - } - // 如果打开了cookie模式,把cookie清除掉 - if(getConfig().getIsReadCookie() == true){ - SaManager.getSaTokenContext().getResponse().deleteCookie(getTokenName()); - } - } - - // 重写 (获取指定key的session) - @Override - public SaSession getSessionBySessionId(String sessionId, boolean isCreate) { - throw new SaTokenException("jwt has not session"); - } - - // 重写 (获取当前登录者的token剩余有效时间 (单位: 秒)) - @Override - public long getTokenTimeout() { - // 如果没有token - String tokenValue = getTokenValue(); - if(tokenValue == null) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - // 开始取值 - Claims claims = null; - try { - claims = SaTokenJwtUtil.getClaims(tokenValue); - } catch (Exception e) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - if(claims == null) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - Date expiration = claims.getExpiration(); - if(expiration == null) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - return (expiration.getTime() - System.currentTimeMillis()) / 1000; - } - - // 重写 (返回当前token的登录设备) - @Override - public String getLoginDevice() { - return SaTokenConsts.DEFAULT_LOGIN_DEVICE; - } - - // 重写 (获取当前会话的token信息) - @Override - public SaTokenInfo getTokenInfo() { - SaTokenInfo info = new SaTokenInfo(); - info.tokenName = getTokenName(); - info.tokenValue = getTokenValue(); - info.isLogin = isLogin(); - info.loginId = getLoginIdDefaultNull(); - info.loginType = getLoginType(); - info.tokenTimeout = getTokenTimeout(); -// info.sessionTimeout = getSessionTimeout(); -// info.tokenSessionTimeout = getTokenSessionTimeout(); -// info.tokenActivityTimeout = getTokenActivityTimeout(); - info.loginDevice = getLoginDevice(); - return info; - } - - - }; - - } - - - - - - - - - - - - - -} diff --git a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/GlobalException.java index f128bcac9..e06c4f01d 100644 --- a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/GlobalException.java +++ b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/GlobalException.java @@ -1,12 +1,9 @@ package com.pj.test; -import java.io.IOException; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.pj.util.AjaxJson; @@ -21,15 +18,6 @@ @RestControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin") public class GlobalException { - // 在每个控制器之前触发的操作 - @ModelAttribute - public void get(HttpServletRequest request) throws IOException { - - } - - - - // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) @@ -48,53 +36,12 @@ public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpSe aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; - aj = AjaxJson.getNotJur("无此权限:" + ee.getCode()); + aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; - - // 输出到客户端 -// response.setContentType("application/json; charset=utf-8"); // http说明,我要返回JSON对象 -// response.getWriter().print(new ObjectMapper().writeValueAsString(aj)); } - - - - // 全局异常拦截(拦截项目中的NotLoginException异常) -// @ExceptionHandler(NotLoginException.class) -// public AjaxJson handlerNotLoginException(NotLoginException nle, HttpServletRequest request, HttpServletResponse response) -// throws Exception { -// -// // 打印堆栈,以供调试 -// nle.printStackTrace(); -// -// // 判断场景值,定制化异常信息 -// String message = ""; -// if(nle.getType().equals(NotLoginException.NOT_TOKEN)) { -// message = "未提供token"; -// } -// else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) { -// message = "token无效"; -// } -// else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) { -// message = "token已过期"; -// } -// else if(nle.getType().equals(NotLoginException.BE_REPLACED)) { -// message = "token已被顶下线"; -// } -// else if(nle.getType().equals(NotLoginException.KICK_OUT)) { -// message = "token已被踢下线"; -// } -// else { -// message = "当前会话未登录"; -// } -// -// // 返回给前端 -// return AjaxJson.getError(message); -// } - - } diff --git a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/TestJwtController.java b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/TestJwtController.java index 6111c13b6..ef94c696a 100644 --- a/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/TestJwtController.java +++ b/sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/TestJwtController.java @@ -1,29 +1,27 @@ package com.pj.test; -import java.util.Date; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.pj.util.AjaxJson; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; -import cn.dev33.satoken.stp.SaTokenInfo; -import cn.dev33.satoken.stp.StpUtil; +import java.util.Date; /** * 测试专用Controller - * @author kong + * @author click33 * */ @RestController @RequestMapping("/test/") public class TestJwtController { - - // 测试登录接口, 浏览器访问: http://localhost:8081/test/login @RequestMapping("login") public AjaxJson login(@RequestParam(defaultValue="10001") String id) { @@ -31,16 +29,16 @@ public AjaxJson login(@RequestParam(defaultValue="10001") String id) { System.out.println("当前会话的token:" + StpUtil.getTokenValue()); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull()); - - StpUtil.login(id); // 在当前会话登录此账号 + + StpUtil.login(id, new SaLoginParameter().setExtra("name", "张三")); // 在当前会话登录此账号 System.out.println("登录成功"); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginId()); // System.out.println("当前登录账号并转为int:" + StpUtil.getLoginIdAsInt()); - System.out.println("当前登录设备:" + StpUtil.getLoginDevice()); + System.out.println("当前登录设备:" + StpUtil.getLoginDeviceType()); // System.out.println("当前token信息:" + StpUtil.getTokenInfo()); - return AjaxJson.getSuccess(); + return AjaxJson.getSuccess().setData(StpUtil.getTokenValue()); } // 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo @@ -51,7 +49,7 @@ public AjaxJson tokenInfo() { System.out.println(tokenInfo); return AjaxJson.getSuccessData(tokenInfo); } - + // 测试会话session接口, 浏览器访问: http://localhost:8081/test/session @RequestMapping("session") @@ -70,11 +68,12 @@ public AjaxJson session() throws JsonProcessingException { // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") + @SaCheckLogin public AjaxJson test() { System.out.println(); System.out.println("--------------进入请求--------------"); - StpUtil.login(10001); - System.out.println(StpUtil.getTokenInfo().getTokenValue()); + System.out.println(StpUtil.getExtra("username")); + System.out.println(StpUtil.getExtra("nick")); return AjaxJson.getSuccess(); } diff --git a/sa-token-demo/sa-token-demo-jwt/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-jwt/src/main/resources/application.yml index 14a193d46..91c2c6002 100644 --- a/sa-token-demo/sa-token-demo-jwt/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-jwt/src/main/resources/application.yml @@ -2,20 +2,25 @@ server: port: 8081 -# sa-token配置 +# sa-token 配置 sa-token: - # token名称 (同时也是cookie名称) + # token 名称 (同时也是 cookie 名称) token-name: satoken - # token有效期,单位s 默认30天, -1代表永不过期 + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 - # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 - activity-timeout: -1 - # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true - # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) - is-share: true - # token风格 + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid + # 是否输出操作日志 + is-log: true + # jwt秘钥 + jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk + spring: # redis配置 redis: diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/pom.xml b/sa-token-demo/sa-token-demo-loveqq-boot/pom.xml new file mode 100644 index 000000000..4ee33a80f --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + com.kfyty + loveqq-framework + 1.1.2 + + + + sa-token-demo-loveqq-boot + 0.0.1-SNAPSHOT + + + 17 + 17 + 17 + 17 + 1.45.0 + + + + + + com.kfyty + loveqq-boot + ${loveqq.framework.version} + + + + + com.kfyty + loveqq-boot-starter-netty + ${loveqq.framework.version} + + + + + cn.dev33 + sa-token-loveqq-boot-starter + ${sa-token.version} + + + + + com.kfyty + loveqq-boot-starter-logback + ${loveqq.framework.version} + + + + + org.yaml + snakeyaml + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 17 + 17 + UTF-8 + + + + + diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/SaTokenLoveqqApplication.java b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/SaTokenLoveqqApplication.java new file mode 100644 index 000000000..a43195ab7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/SaTokenLoveqqApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import com.kfyty.loveqq.framework.boot.K; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.BootApplication; +import com.kfyty.loveqq.framework.web.core.autoconfig.annotation.EnableWebMvc; + +/** + * Sa-Token 整合 loveqq-framework 示例 + * + * @author kfyty725 + */ +@EnableWebMvc +@BootApplication +public class SaTokenLoveqqApplication { + + public static void main(String[] args) { + K.run(SaTokenLoveqqApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/MyFilter.java b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/MyFilter.java new file mode 100644 index 000000000..12a6eaf2f --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/MyFilter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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 com.pj.satoken; + +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; +import cn.dev33.satoken.stp.StpUtil; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.web.core.filter.Filter; +import com.kfyty.loveqq.framework.web.core.filter.FilterChain; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +/** + * 自定义过滤器 + */ +@Component +public class MyFilter implements Filter { + /** + * 实现该方法,可以实现 servlet/reactor 的统一 + * 但是该方法内部是同步方法,若需要异步,可以实现仅 reactor 支持的 {@link Filter#doFilter(ServerRequest, ServerResponse, FilterChain)} 方法 + * + * @param request 请求 + * @param response 响应 + */ + @Override + public Continue doFilter(ServerRequest request, ServerResponse response) { + System.out.println("进入自定义过滤器"); + + // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 + SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); + try { + System.out.println(StpUtil.isLogin()); + } finally { + SaTokenContextUtil.clearContext(prev); + } + + return Continue.TRUE; + } +} diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..03f88147b --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,39 @@ +package com.pj.satoken; + +import cn.dev33.satoken.loveqq.boot.filter.SaRequestFilter; +import cn.dev33.satoken.util.SaResult; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + * + */ +@Configuration +public class SaTokenConfigure { + + /** + * 注册 [sa-token全局过滤器] + */ + @Bean + public SaRequestFilter getSaReactorFilter() { + return new SaRequestFilter() + // 指定 [拦截路由] + .addInclude("/**") + // 指定 [放行路由] + .addExclude("/favicon.ico") + // 指定[认证函数]: 每次请求执行 + .setAuth(r -> { + System.out.println("---------- sa全局认证"); + // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); + }) + // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }) + ; + } +} diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..473612742 --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,42 @@ +package com.pj.satoken; + +import cn.dev33.satoken.stp.StpInterface; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } +} diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/GlobalException.java new file mode 100644 index 000000000..9dc5b51bd --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/GlobalException.java @@ -0,0 +1,18 @@ +package com.pj.test; + +import cn.dev33.satoken.util.SaResult; +import com.kfyty.loveqq.framework.web.core.annotation.ExceptionHandler; +import com.kfyty.loveqq.framework.web.core.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } +} diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..cab37d89d --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,116 @@ +package com.pj.test; + +import cn.dev33.satoken.loveqq.boot.context.SaReactorHolder; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; +import com.kfyty.loveqq.framework.web.core.annotation.GetMapping; +import com.kfyty.loveqq.framework.web.core.annotation.RequestMapping; +import com.kfyty.loveqq.framework.web.core.annotation.RestController; +import com.kfyty.loveqq.framework.web.core.annotation.bind.CookieValue; +import com.kfyty.loveqq.framework.web.core.annotation.bind.RequestParam; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * 测试专用 Controller + * 本示例是基于 reactor 编写,如果是 servlet,去除 SaReactorHolder/SaTokenContextUtil 包装,直接调用 sa-token api 即可 + * + * @author click33 + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + UserService userService; + + // 登录测试:Controller 里调用 Sa-Token API --- http://localhost:8081/test/login + @GetMapping("login") + public Mono login(@RequestParam(defaultValue = "10001") String id) { + return SaReactorHolder.sync(() -> { + StpUtil.login(id); + return SaResult.ok("登录成功"); + }); + } + + // API测试:手动设置上下文、try-finally 形式 --- http://localhost:8081/test/isLogin + @GetMapping("isLogin") + public SaResult isLogin(ServerRequest request, ServerResponse response) { + try { + SaTokenContextUtil.setContext(request, response); + System.out.println("是否登录:" + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + } finally { + SaTokenContextUtil.clearContext(null); + } + } + + // API测试:手动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin2 + @GetMapping("isLogin2") + public SaResult isLogin2(ServerRequest request, ServerResponse response) { + SaResult res = SaTokenContextUtil.setContext(request, response, () -> { + System.out.println("是否登录:" + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + }); + return SaResult.data(res); + } + + // API测试:自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3 + @GetMapping("isLogin3") + public Mono isLogin3() { + return SaReactorHolder.sync(() -> { + System.out.println("是否登录:" + StpUtil.isLogin()); + userService.isLogin(); + return SaResult.data(StpUtil.getTokenInfo()); + }); + } + + // API测试:自动设置上下文、调用 userService Mono 方法 --- http://localhost:8081/test/isLogin4 + @GetMapping("isLogin4") + public Mono isLogin4() { + return userService.findUserIdByNamePwd("ZhangSan", "123456") + .flatMap(userId -> SaReactorHolder.sync(() -> { + StpUtil.login(userId); + return SaResult.data(StpUtil.getTokenInfo()); + })); + } + + // API测试:切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5 + @GetMapping("isLogin5") + public Mono isLogin5() { + System.out.println("线程id-----" + Thread.currentThread().getId()); + // 要点:在流里调用 Sa-Token API 之前,必须用 SaReactorHolder.sync( () -> {} ) 进行包裹 + return Mono.delay(Duration.ofSeconds(1)) + .doOnNext(r -> System.out.println("线程id-----" + Thread.currentThread().getId())) + .map(r -> SaReactorHolder.sync(() -> userService.isLogin())) + .map(r -> userService.findUserIdByNamePwd("ZhangSan", "123456")) + .map(r -> SaReactorHolder.sync(() -> userService.isLogin())) + .flatMap(isLogin -> { + System.out.println("是否登录 " + isLogin); + return SaReactorHolder.sync(() -> { + System.out.println("是否登录 " + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + }); + }); + } + + // API测试:使用上下文无关的API --- http://localhost:8081/test/isLogin6 + @GetMapping("isLogin6") + public SaResult isLogin6(@CookieValue("satoken") String satoken) { + System.out.println("token 为:" + satoken); + System.out.println("登录人:" + StpUtil.getLoginIdByToken(satoken)); + return SaResult.ok("登录人:" + StpUtil.getLoginIdByToken(satoken)); + } + + // 测试 浏览器访问: http://localhost:8081/test/test + @GetMapping("test") + public SaResult test() { + System.out.println("线程id------- " + Thread.currentThread().getId()); + return SaResult.ok(); + } +} diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/UserService.java b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/UserService.java new file mode 100644 index 000000000..65fbec056 --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/UserService.java @@ -0,0 +1,24 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Service; +import reactor.core.publisher.Mono; + +/** + * 模拟 Service 方法 + * @author click33 + * @since 2025/4/6 + */ +@Service +public class UserService { + + public boolean isLogin() { + System.out.println("UserService 里调用 API 测试,是否登录:" + StpUtil.isLogin()); + return StpUtil.isLogin(); + } + + public Mono findUserIdByNamePwd(String name, String pwd) { + // ... + return Mono.just(10001L); + } +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-loveqq-boot/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/resources/application.yml new file mode 100644 index 000000000..7babd8054 --- /dev/null +++ b/sa-token-demo/sa-token-demo-loveqq-boot/src/main/resources/application.yml @@ -0,0 +1,4 @@ +# 端口 +k: + server: + port: 8081 diff --git a/sa-token-demo/sa-token-demo-oauth2-client/.gitignore b/sa-token-demo/sa-token-demo-oauth2-client/.gitignore deleted file mode 100644 index 304e8d54a..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-client/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -target/ -.project -.classpath -.settings - -/.idea/ - -node_modules/ -bin/ -.settings/ -unpackage/ -/.apt_generated/ -/.apt_generated_tests/ diff --git a/sa-token-demo/sa-token-demo-oauth2-client/pom.xml b/sa-token-demo/sa-token-demo-oauth2-client/pom.xml deleted file mode 100644 index 7050a7bf9..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-client/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ - - 4.0.0 - com.pj - sa-token-demo-oauth2-client - 0.0.1-SNAPSHOT - - - - org.springframework.boot - spring-boot-starter-parent - 2.3.3.RELEASE - - - - - 1.8 - 3.1.1 - - 1.24.0 - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} - - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - - - com.ejlchina - okhttps - 3.1.1 - - - - - org.springframework.boot - spring-boot-devtools - true - - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java b/sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java deleted file mode 100644 index e0cb545e4..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.pj.oauth2; - -import javax.servlet.http.HttpServletRequest; - -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.ModelAndView; - -import com.ejlchina.okhttps.OkHttps; -import com.pj.utils.SoMap; - -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-OAuth2 Client端 控制器 - * @author kong - */ -@RestController -public class SaOAuthClientController { - - // 相关参数配置 - private String clientId = "1001"; // 应用id - private String clientSecret = "aaaa-bbbb-cccc-dddd-eeee"; // 应用秘钥 - private String serverUrl = "http://sa-oauth-server.com:8001"; // 服务端接口 - - // 进入首页 - @RequestMapping("/") - public Object index(HttpServletRequest request) { - request.setAttribute("uid", StpUtil.getLoginIdDefaultNull()); - return new ModelAndView("index.html"); - } - - // 根据Code码进行登录,获取 Access-Token 和 openid - @RequestMapping("/codeLogin") - public SaResult codeLogin(String code) { - // 调用Server端接口,获取 Access-Token 以及其他信息 - String str = OkHttps.sync(serverUrl + "/oauth2/token") - .addBodyPara("grant_type", "authorization_code") - .addBodyPara("code", code) - .addBodyPara("client_id", clientId) - .addBodyPara("client_secret", clientSecret) - .post() - .getBody() - .toString(); - SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); - - // code不等于200 代表请求失败 - if(so.getInt("code") != 200) { - return SaResult.error(so.getString("msg")); - } - - // 根据openid获取其对应的userId - SoMap data = so.getMap("data"); - long uid = getUserIdByOpenid(data.getString("openid")); - data.set("uid", uid); - - // 返回相关参数 - StpUtil.login(uid); - return SaResult.data(data); - } - - // 根据 Refresh-Token 去刷新 Access-Token - @RequestMapping("/refresh") - public SaResult refresh(String refreshToken) { - // 调用Server端接口,通过 Refresh-Token 刷新出一个新的 Access-Token - String str = OkHttps.sync(serverUrl + "/oauth2/refresh") - .addBodyPara("grant_type", "refresh_token") - .addBodyPara("client_id", clientId) - .addBodyPara("client_secret", clientSecret) - .addBodyPara("refresh_token", refreshToken) - .post() - .getBody() - .toString(); - SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); - - // code不等于200 代表请求失败 - if(so.getInt("code") != 200) { - return SaResult.error(so.getString("msg")); - } - - // 返回相关参数 (data=新的Access-Token ) - SoMap data = so.getMap("data"); - return SaResult.data(data); - } - - // 模式三:密码式-授权登录 - @RequestMapping("/passwordLogin") - public SaResult passwordLogin(String username, String password) { - // 模式三:密码式-授权登录 - String str = OkHttps.sync(serverUrl + "/oauth2/token") - .addBodyPara("grant_type", "password") - .addBodyPara("client_id", clientId) - .addBodyPara("username", username) - .addBodyPara("password", password) - .post() - .getBody() - .toString(); - SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); - - // code不等于200 代表请求失败 - if(so.getInt("code") != 200) { - return SaResult.error(so.getString("msg")); - } - - // 根据openid获取其对应的userId - SoMap data = so.getMap("data"); - long uid = getUserIdByOpenid(data.getString("openid")); - data.set("uid", uid); - - // 返回相关参数 - StpUtil.login(uid); - return SaResult.data(data); - } - - // 模式四:获取应用的 Client-Token - @RequestMapping("/clientToken") - public SaResult clientToken() { - // 调用Server端接口 - String str = OkHttps.sync(serverUrl + "/oauth2/client_token") - .addBodyPara("grant_type", "client_credentials") - .addBodyPara("client_id", clientId) - .addBodyPara("client_secret", clientSecret) - .post() - .getBody() - .toString(); - SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); - - // code不等于200 代表请求失败 - if(so.getInt("code") != 200) { - return SaResult.error(so.getString("msg")); - } - - // 返回相关参数 (data=新的Client-Token ) - SoMap data = so.getMap("data"); - return SaResult.data(data); - } - - // 注销登录 - @RequestMapping("/logout") - public SaResult logout() { - StpUtil.logout(); - return SaResult.ok(); - } - - // 根据 Access-Token 置换相关的资源: 获取账号昵称、头像、性别等信息 - @RequestMapping("/getUserinfo") - public SaResult getUserinfo(String accessToken) { - // 调用Server端接口,查询开放的资源 - String str = OkHttps.sync(serverUrl + "/oauth2/userinfo") - .addBodyPara("access_token", accessToken) - .post() - .getBody() - .toString(); - SoMap so = SoMap.getSoMap().setJsonString(str); - System.out.println("返回结果: " + so); - - // code不等于200 代表请求失败 - if(so.getInt("code") != 200) { - return SaResult.error(so.getString("msg")); - } - - // 返回相关参数 (data=获取到的资源 ) - SoMap data = so.getMap("data"); - return SaResult.data(data); - } - - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); - } - - - // ------------ 模拟方法 ------------------ - // 模拟方法:根据openid获取userId - private long getUserIdByOpenid(String openid) { - // 此方法仅做模拟,实际开发要根据具体业务逻辑来获取userId - return 10001; - } - -} diff --git a/sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java b/sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java deleted file mode 100644 index 5258a6d3f..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java +++ /dev/null @@ -1,737 +0,0 @@ -package com.pj.utils; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -import javax.servlet.http.HttpServletRequest; - -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦 - *

所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用 - *

最新:2020-12-10 新增部分构造方法 - * @author kong - */ -public class SoMap extends LinkedHashMap { - - private static final long serialVersionUID = 1L; - - public SoMap() { - } - - /** 以下元素会在isNull函数中被判定为Null, */ - public static final Object[] NULL_ELEMENT_ARRAY = {null, ""}; - public static final List NULL_ELEMENT_LIST; - - static { - NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY); - } - - // ============================= 读值 ============================= - - /** 获取一个值 */ - @Override - public Object get(Object key) { - if("this".equals(key)) { - return this; - } - return super.get(key); - } - - /** 如果为空,则返回默认值 */ - public Object get(Object key, Object defaultValue) { - Object value = get(key); - if(valueIsNull(value)) { - return defaultValue; - } - return value; - } - - /** 转为String并返回 */ - public String getString(String key) { - Object value = get(key); - if(value == null) { - return null; - } - return String.valueOf(value); - } - - /** 如果为空,则返回默认值 */ - public String getString(String key, String defaultValue) { - Object value = get(key); - if(valueIsNull(value)) { - return defaultValue; - } - return String.valueOf(value); - } - - /** 转为int并返回 */ - public int getInt(String key) { - Object value = get(key); - if(valueIsNull(value)) { - return 0; - } - return Integer.valueOf(String.valueOf(value)); - } - /** 转为int并返回,同时指定默认值 */ - public int getInt(String key, int defaultValue) { - Object value = get(key); - if(valueIsNull(value)) { - return defaultValue; - } - return Integer.valueOf(String.valueOf(value)); - } - - /** 转为long并返回 */ - public long getLong(String key) { - Object value = get(key); - if(valueIsNull(value)) { - return 0; - } - return Long.valueOf(String.valueOf(value)); - } - - /** 转为double并返回 */ - public double getDouble(String key) { - Object value = get(key); - if(valueIsNull(value)) { - return 0.0; - } - return Double.valueOf(String.valueOf(value)); - } - - /** 转为boolean并返回 */ - public boolean getBoolean(String key) { - Object value = get(key); - if(valueIsNull(value)) { - return false; - } - return Boolean.valueOf(String.valueOf(value)); - } - - /** 转为Date并返回,根据自定义格式 */ - public Date getDateByFormat(String key, String format) { - try { - return new SimpleDateFormat(format).parse(getString(key)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** 转为Date并返回,根据格式: yyyy-MM-dd */ - public Date getDate(String key) { - return getDateByFormat(key, "yyyy-MM-dd"); - } - - /** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */ - public Date getDateTime(String key) { - return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss"); - } - - /** 转为Map并返回 */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public SoMap getMap(String key) { - Object value = get(key); - if(value == null) { - return SoMap.getSoMap(); - } - if(value instanceof Map) { - return SoMap.getSoMap((Map)value); - } - if(value instanceof String) { - return SoMap.getSoMap().setJsonString((String)value); - } - throw new RuntimeException("值无法转化为SoMap: " + value); - } - - /** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */ - @SuppressWarnings("unchecked") - public List getList(String key) { - Object value = get(key); - List list = null; - if(value == null || value.equals("")) { - list = new ArrayList(); - } - else if(value instanceof List) { - list = (List)value; - } else { - list = new ArrayList(); - list.add(value); - } - return list; - } - - /** 获取集合 (指定泛型类型) */ - public List getList(String key, Class cs) { - List list = getList(key); - List list2 = new ArrayList(); - for (Object obj : list) { - T objC = getValueByClass(obj, cs); - list2.add(objC); - } - return list2; - } - - /** 获取集合(逗号分隔式),(指定类型) */ - public List getListByComma(String key, Class cs) { - String listStr = getString(key); - if(listStr == null || listStr.equals("")) { - return new ArrayList<>(); - } - // 开始转化 - String [] arr = listStr.split(","); - List list = new ArrayList(); - for (String str : arr) { - if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) { - str = str.trim(); - } - T objC = getValueByClass(str, cs); - list.add(objC); - } - return list; - } - - - /** 根据指定类型从map中取值,返回实体对象 */ - public T getModel(Class cs) { - try { - return getModelByObject(cs.newInstance()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** 从map中取值,塞到一个对象中 */ - public T getModelByObject(T obj) { - // 获取类型 - Class cs = obj.getClass(); - // 循环复制 - for (Field field : cs.getDeclaredFields()) { - try { - // 获取对象 - Object value = this.get(field.getName()); - if(value == null) { - continue; - } - field.setAccessible(true); - Object valueConvert = getValueByClass(value, field.getType()); - field.set(obj, valueConvert); - } catch (IllegalArgumentException | IllegalAccessException e) { - throw new RuntimeException("属性取值出错:" + field.getName(), e); - } - } - return obj; - } - - - - /** - * 将指定值转化为指定类型并返回 - * @param obj - * @param cs - * @param - * @return - */ - @SuppressWarnings("unchecked") - public static T getValueByClass(Object obj, Class cs) { - String obj2 = String.valueOf(obj); - Object obj3 = null; - if (cs.equals(String.class)) { - obj3 = obj2; - } else if (cs.equals(int.class) || cs.equals(Integer.class)) { - obj3 = Integer.valueOf(obj2); - } else if (cs.equals(long.class) || cs.equals(Long.class)) { - obj3 = Long.valueOf(obj2); - } else if (cs.equals(short.class) || cs.equals(Short.class)) { - obj3 = Short.valueOf(obj2); - } else if (cs.equals(byte.class) || cs.equals(Byte.class)) { - obj3 = Byte.valueOf(obj2); - } else if (cs.equals(float.class) || cs.equals(Float.class)) { - obj3 = Float.valueOf(obj2); - } else if (cs.equals(double.class) || cs.equals(Double.class)) { - obj3 = Double.valueOf(obj2); - } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) { - obj3 = Boolean.valueOf(obj2); - } else { - obj3 = (T)obj; - } - return (T)obj3; - } - - - // ============================= 写值 ============================= - - /** - * 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去) - */ - public void setDefaultValue(String key, Object defaultValue) { - if(isNull(key)) { - set(key, defaultValue); - } - } - - /** set一个值,连缀风格 */ - public SoMap set(String key, Object value) { - // 防止敏感key - if(key.toLowerCase().equals("this")) { - return this; - } - put(key, value); - return this; - } - - /** 将一个Map塞进SoMap */ - public SoMap setMap(Map map) { - if(map != null) { - for (String key : map.keySet()) { - this.set(key, map.get(key)); - } - } - return this; - } - - /** 将一个对象解析塞进SoMap */ - public SoMap setModel(Object model) { - if(model == null) { - return this; - } - Field[] fields = model.getClass().getDeclaredFields(); - for (Field field : fields) { - try{ - field.setAccessible(true); - boolean isStatic = Modifier.isStatic(field.getModifiers()); - if(!isStatic) { - this.set(field.getName(), field.get(model)); - } - }catch (Exception e){ - throw new RuntimeException(e); - } - } - return this; - } - - /** 将json字符串解析后塞进SoMap */ - public SoMap setJsonString(String jsonString) { - try { - @SuppressWarnings("unchecked") - Map map = new ObjectMapper().readValue(jsonString, Map.class); - return this.setMap(map); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - - // ============================= 删值 ============================= - - /** delete一个值,连缀风格 */ - public SoMap delete(String key) { - remove(key); - return this; - } - - /** 清理所有value为null的字段 */ - public SoMap clearNull() { - Iterator iterator = this.keySet().iterator(); - while(iterator.hasNext()) { - String key = iterator.next(); - if(this.isNull(key)) { - iterator.remove(); - this.remove(key); - } - - } - return this; - } - /** 清理指定key */ - public SoMap clearIn(String ...keys) { - List keys2 = Arrays.asList(keys); - Iterator iterator = this.keySet().iterator(); - while(iterator.hasNext()) { - String key = iterator.next(); - if(keys2.contains(key) == true) { - iterator.remove(); - this.remove(key); - } - } - return this; - } - /** 清理掉不在列表中的key */ - public SoMap clearNotIn(String ...keys) { - List keys2 = Arrays.asList(keys); - Iterator iterator = this.keySet().iterator(); - while(iterator.hasNext()) { - String key = iterator.next(); - if(keys2.contains(key) == false) { - iterator.remove(); - this.remove(key); - } - - } - return this; - } - /** 清理掉所有key */ - public SoMap clearAll() { - clear(); - return this; - } - - - // ============================= 快速构建 ============================= - - /** 构建一个SoMap并返回 */ - public static SoMap getSoMap() { - return new SoMap(); - } - /** 构建一个SoMap并返回 */ - public static SoMap getSoMap(String key, Object value) { - return new SoMap().set(key, value); - } - /** 构建一个SoMap并返回 */ - public static SoMap getSoMap(Map map) { - return new SoMap().setMap(map); - } - - /** 将一个对象集合解析成为SoMap */ - public static SoMap getSoMapByModel(Object model) { - return SoMap.getSoMap().setModel(model); - } - - /** 将一个对象集合解析成为SoMap集合 */ - public static List getSoMapByList(List list) { - List listMap = new ArrayList(); - for (Object model : list) { - listMap.add(getSoMapByModel(model)); - } - return listMap; - } - - /** 克隆指定key,返回一个新的SoMap */ - public SoMap cloneKeys(String... keys) { - SoMap so = new SoMap(); - for (String key : keys) { - so.set(key, this.get(key)); - } - return so; - } - /** 克隆所有key,返回一个新的SoMap */ - public SoMap cloneSoMap() { - SoMap so = new SoMap(); - for (String key : this.keySet()) { - so.set(key, this.get(key)); - } - return so; - } - - /** 将所有key转为大写 */ - public SoMap toUpperCase() { - SoMap so = new SoMap(); - for (String key : this.keySet()) { - so.set(key.toUpperCase(), this.get(key)); - } - this.clearAll().setMap(so); - return this; - } - /** 将所有key转为小写 */ - public SoMap toLowerCase() { - SoMap so = new SoMap(); - for (String key : this.keySet()) { - so.set(key.toLowerCase(), this.get(key)); - } - this.clearAll().setMap(so); - return this; - } - /** 将所有key中下划线转为中划线模式 (kebab-case风格) */ - public SoMap toKebabCase() { - SoMap so = new SoMap(); - for (String key : this.keySet()) { - so.set(wordEachKebabCase(key), this.get(key)); - } - this.clearAll().setMap(so); - return this; - } - /** 将所有key中下划线转为小驼峰模式 */ - public SoMap toHumpCase() { - SoMap so = new SoMap(); - for (String key : this.keySet()) { - so.set(wordEachBigFs(key), this.get(key)); - } - this.clearAll().setMap(so); - return this; - } - /** 将所有key中小驼峰转为下划线模式 */ - public SoMap humpToLineCase() { - SoMap so = new SoMap(); - for (String key : this.keySet()) { - so.set(wordHumpToLine(key), this.get(key)); - } - this.clearAll().setMap(so); - return this; - } - - - - - // ============================= 辅助方法 ============================= - - - /** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */ - public boolean isNull(String key) { - return valueIsNull(get(key)); - } - - /** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */ - public boolean isContainNull(String ...keys) { - for (String key : keys) { - if(this.isNull(key)) { - return true; - } - } - return false; - } - - /** 与isNull()相反 */ - public boolean isNotNull(String key) { - return !isNull(key); - } - /** 指定key的value是否为null,作用同isNotNull() */ - public boolean has(String key) { - return !isNull(key); - } - - /** 指定value在此SoMap的判断标准中是否为null */ - public boolean valueIsNull(Object value) { - return NULL_ELEMENT_LIST.contains(value); - } - - /** 验证指定key不为空,为空则抛出异常 */ - public SoMap checkNull(String ...keys) { - for (String key : keys) { - if(this.isNull(key)) { - throw new RuntimeException("参数" + key + "不能为空"); - } - } - return this; - } - - static Pattern patternNumber = Pattern.compile("[0-9]*"); - /** 指定key是否为数字 */ - public boolean isNumber(String key) { - String value = getString(key); - if(value == null) { - return false; - } - return patternNumber.matcher(value).matches(); - } - - - - - /** - * 转为JSON字符串 - */ - public String toJsonString() { - try { -// SoMap so = SoMap.getSoMap(this); - return new ObjectMapper().writeValueAsString(this); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - -// /** -// * 转为JSON字符串, 带格式的 -// */ -// public String toJsonFormatString() { -// try { -// return JSON.toJSONString(this, true); -// } catch (Exception e) { -// throw new RuntimeException(e); -// } -// } - - // ============================= web辅助 ============================= - - - /** - * 返回当前request请求的的所有参数 - * @return - */ - public static SoMap getRequestSoMap() { - // 大善人SpringMVC提供的封装 - ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if(servletRequestAttributes == null) { - throw new RuntimeException("当前线程非JavaWeb环境"); - } - // 当前request - HttpServletRequest request = servletRequestAttributes.getRequest(); - if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) { - initRequestSoMap(request); - } - return (SoMap)request.getAttribute("currentSoMap"); - } - - /** 初始化当前request的 SoMap */ - private static void initRequestSoMap(HttpServletRequest request) { - SoMap soMap = new SoMap(); - Map parameterMap = request.getParameterMap(); // 获取所有参数 - for (String key : parameterMap.keySet()) { - try { - String[] values = parameterMap.get(key); // 获得values - if(values.length == 1) { - soMap.set(key, values[0]); - } else { - List list = new ArrayList(); - for (String v : values) { - list.add(v); - } - soMap.set(key, list); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - request.setAttribute("currentSoMap", soMap); - } - - /** - * 验证返回当前线程是否为JavaWeb环境 - * @return - */ - public static boolean isJavaWeb() { - ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装 - if(servletRequestAttributes == null) { - return false; - } - return true; - } - - - - // ============================= 常见key (以下key经常用,所以封装以下,方便写代码) ============================= - - /** get 当前页 */ - public int getKeyPageNo() { - int pageNo = getInt("pageNo", 1); - if(pageNo <= 0) { - pageNo = 1; - } - return pageNo; - } - /** get 页大小 */ - public int getKeyPageSize() { - int pageSize = getInt("pageSize", 10); - if(pageSize <= 0 || pageSize > 1000) { - pageSize = 10; - } - return pageSize; - } - - /** get 排序方式 */ - public int getKeySortType() { - return getInt("sortType"); - } - - - - - - - // ============================= 工具方法 ============================= - - - /** - * 将一个一维集合转换为树形集合 - * @param list 集合 - * @param idKey id标识key - * @param parentIdKey 父id标识key - * @param childListKey 子节点标识key - * @return 转换后的tree集合 - */ - public static List listToTree(List list, String idKey, String parentIdKey, String childListKey) { - // 声明新的集合,存储tree形数据 - List newTreeList = new ArrayList(); - // 声明hash-Map,方便查找数据 - SoMap hash = new SoMap(); - // 将数组转为Object的形式,key为数组中的id - for (int i = 0; i < list.size(); i++) { - SoMap json = (SoMap) list.get(i); - hash.put(json.getString(idKey), json); - } - // 遍历结果集 - for (int j = 0; j < list.size(); j++) { - // 单条记录 - SoMap aVal = (SoMap) list.get(j); - // 在hash中取出key为单条记录中pid的值 - SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString()); - // 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中 - if (hashVp != null) { - // 检查是否有child属性,有则添加,没有则新建 - if (hashVp.get(childListKey) != null) { - @SuppressWarnings("unchecked") - List ch = (List) hashVp.get(childListKey); - ch.add(aVal); - hashVp.put(childListKey, ch); - } else { - List ch = new ArrayList(); - ch.add(aVal); - hashVp.put(childListKey, ch); - } - } else { - newTreeList.add(aVal); - } - } - return newTreeList; - } - - - - /** 指定字符串的字符串下划线转大写模式 */ - private static String wordEachBig(String str){ - String newStr = ""; - for (String s : str.split("_")) { - newStr += wordFirstBig(s); - } - return newStr; - } - /** 返回下划线转小驼峰形式 */ - private static String wordEachBigFs(String str){ - return wordFirstSmall(wordEachBig(str)); - } - - /** 将指定单词首字母大写 */ - private static String wordFirstBig(String str) { - return str.substring(0, 1).toUpperCase() + str.substring(1, str.length()); - } - - /** 将指定单词首字母小写 */ - private static String wordFirstSmall(String str) { - return str.substring(0, 1).toLowerCase() + str.substring(1, str.length()); - } - - /** 下划线转中划线 */ - private static String wordEachKebabCase(String str) { - return str.replaceAll("_", "-"); - } - - /** 驼峰转下划线 */ - private static String wordHumpToLine(String str) { - return str.replaceAll("[A-Z]", "_$0").toLowerCase(); - } - - -} diff --git a/sa-token-demo/sa-token-demo-oauth2-client/src/main/resources/templates/index.html b/sa-token-demo/sa-token-demo-oauth2-client/src/main/resources/templates/index.html deleted file mode 100644 index ee520f896..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-client/src/main/resources/templates/index.html +++ /dev/null @@ -1,258 +0,0 @@ - - - - - Sa-OAuth2-Client端-测试页 - - - - - - - - - - diff --git a/sa-token-demo/sa-token-demo-oauth2-server/.gitignore b/sa-token-demo/sa-token-demo-oauth2-server/.gitignore deleted file mode 100644 index 304e8d54a..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-server/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -target/ -.project -.classpath -.settings - -/.idea/ - -node_modules/ -bin/ -.settings/ -unpackage/ -/.apt_generated/ -/.apt_generated_tests/ diff --git a/sa-token-demo/sa-token-demo-oauth2-server/pom.xml b/sa-token-demo/sa-token-demo-oauth2-server/pom.xml deleted file mode 100644 index 98b0b10f9..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-server/pom.xml +++ /dev/null @@ -1,81 +0,0 @@ - - 4.0.0 - com.pj - sa-token-demo-oauth2-server - 0.0.1-SNAPSHOT - - - - org.springframework.boot - spring-boot-starter-parent - 2.3.3.RELEASE - - - - - 1.8 - 3.1.1 - - 1.24.0 - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} - - - - - cn.dev33 - sa-token-oauth2 - ${sa-token-version} - - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa-token-version} - - - org.apache.commons - commons-pool2 - - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - - - org.springframework.boot - spring-boot-devtools - true - - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java b/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java deleted file mode 100644 index a588eee02..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.pj; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * 启动:Sa-OAuth2 Server端 - * @author kong - */ -@SpringBootApplication -public class SaOAuth2ServerApplication { - - public static void main(String[] args) { - SpringApplication.run(SaOAuth2ServerApplication.class, args); - System.out.println("\nSa-Token-OAuth Server端启动成功"); - } - -} diff --git a/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java b/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java deleted file mode 100644 index 00b53033e..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.pj.oauth2; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.ModelAndView; - -import cn.dev33.satoken.context.SaHolder; -import cn.dev33.satoken.oauth2.config.SaOAuth2Config; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Handle; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Util; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-OAuth2 Server端 控制器 - * @author kong - * - */ -@RestController -public class SaOAuth2ServerController { - - // 处理所有OAuth相关请求 - @RequestMapping("/oauth2/*") - public Object request() { - System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl()); - return SaOAuth2Handle.serverRequest(); - } - - // Sa-OAuth2 定制化配置 - @Autowired - public void setSaOAuth2Config(SaOAuth2Config cfg) { - cfg. - // 未登录的视图 - setNotLoginView(()->{ - return new ModelAndView("login.html"); - }). - // 登录处理函数 - setDoLoginHandle((name, pwd) -> { - if("sa".equals(name) && "123456".equals(pwd)) { - StpUtil.login(10001); - return SaResult.ok(); - } - return SaResult.error("账号名或密码错误"); - }). - // 授权确认视图 - setConfirmView((clientId, scope)->{ - Map map = new HashMap<>(); - map.put("clientId", clientId); - map.put("scope", scope); - return new ModelAndView("confirm.html", map); - }) - ; - } - - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); - } - - - // ---------- 开放相关资源接口: Client端根据 Access-Token ,置换相关资源 ------------ - - // 获取Userinfo信息:昵称、头像、性别等等 - @RequestMapping("/oauth2/userinfo") - public SaResult userinfo() { - // 获取 Access-Token 对应的账号id - String accessToken = SaHolder.getRequest().getParamNotNull("access_token"); - Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken); - System.out.println("-------- 此Access-Token对应的账号id: " + loginId); - - // 校验 Access-Token 是否具有权限: userinfo - SaOAuth2Util.checkScope(accessToken, "userinfo"); - - // 模拟账号信息 (真实环境需要查询数据库获取信息) - Map map = new LinkedHashMap(); - map.put("nickname", "shengzhang_"); - map.put("avatar", "http://xxx.com/1.jpg"); - map.put("age", "18"); - map.put("sex", "男"); - map.put("address", "山东省 青岛市 城阳区"); - return SaResult.data(map); - } - -} diff --git a/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2TemplateImpl.java b/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2TemplateImpl.java deleted file mode 100644 index 471f75b16..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2TemplateImpl.java +++ /dev/null @@ -1,38 +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 kong - */ -@Component -public class SaOAuth2TemplateImpl extends SaOAuth2Template { - - // 根据 id 获取 Client 信息 - @Override - public SaClientModel getClientModel(String clientId) { - // 此为模拟数据,真实环境需要从数据库查询 - if("1001".equals(clientId)) { - return new SaClientModel() - .setClientId("10001") - .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") - .setAllowUrl("*") - .setContractScope("userinfo"); - } - return null; - } - - // 根据ClientId 和 LoginId 获取openid - @Override - public String getOpenid(String clientId, Object loginId) { - // 此为模拟数据,真实环境需要从数据库查询 - return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"; - } - - // -------------- 其它需要重写的函数 - -} diff --git a/sa-token-demo/sa-token-demo-oauth2-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-oauth2-server/src/main/resources/application.yml deleted file mode 100644 index 6909289f4..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-server/src/main/resources/application.yml +++ /dev/null @@ -1,41 +0,0 @@ -server: - port: 8001 - -# sa-token配置 -sa-token: - # token名称 (同时也是cookie名称) - token-name: satoken-server - # OAuth2.0 配置 - oauth2: - is-code: true - is-implicit: true - is-password: true - is-client: true - -spring: - # redis配置 - redis: - # Redis数据库索引(默认为0) - database: 1 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - # password: - # 连接超时时间(毫秒) - timeout: 1000ms - lettuce: - pool: - # 连接池最大连接数 - max-active: 200 - # 连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - # 连接池中的最大空闲连接 - max-idle: 10 - # 连接池中的最小空闲连接 - min-idle: 0 - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2-server/src/main/resources/templates/login.html b/sa-token-demo/sa-token-demo-oauth2-server/src/main/resources/templates/login.html deleted file mode 100644 index 5972291fd..000000000 --- a/sa-token-demo/sa-token-demo-oauth2-server/src/main/resources/templates/login.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Sa-OAuth2-认证中心-登录页 - - - - - - - - - - diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/index.html b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/index.html new file mode 100644 index 000000000..86a6ad962 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/index.html @@ -0,0 +1,633 @@ + + + + + Sa-Token-OAuth2 Client 端 - 测试页 + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000..dd51e185b --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/pom.xml @@ -0,0 +1,69 @@ + + 4.0.0 + com.pj + sa-token-demo-oauth2-client + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + 1.8 + 3.1.1 + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + com.ejlchina + okhttps + 3.1.1 + + + + + org.springframework.boot + spring-boot-devtools + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/SaOAuth2ClientApplication.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/SaOAuth2ClientApplication.java similarity index 97% rename from sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/SaOAuth2ClientApplication.java rename to sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/SaOAuth2ClientApplication.java index 0bbe6c48f..2312e2c8c 100644 --- a/sa-token-demo/sa-token-demo-oauth2-client/src/main/java/com/pj/SaOAuth2ClientApplication.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/SaOAuth2ClientApplication.java @@ -5,7 +5,7 @@ /** * 启动:Sa-OAuth2 ClientServer端 - * @author kong + * @author click33 */ @SpringBootApplication public class SaOAuth2ClientApplication { 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 new file mode 100644 index 000000000..6769068eb --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java @@ -0,0 +1,184 @@ +package com.pj.oauth2; + +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 javax.servlet.http.HttpServletRequest; + +/** + * Sa-OAuth2 Client端 控制器 + * @author click33 + */ +@RestController +public class SaOAuthClientController { + + // 相关参数配置 + 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) { + request.setAttribute("uid", StpUtil.getLoginIdDefaultNull()); + return new ModelAndView("index.html"); + } + + // 根据Code码进行登录,获取 Access-Token 和 openid + @RequestMapping("/codeLogin") + public SaResult codeLogin(String code) throws JsonProcessingException { + // 调用Server端接口,获取 Access-Token 以及其他信息 + String str = OkHttps.sync(serverUrl + "/oauth2/token") + .addBodyPara("grant_type", "authorization_code") + .addBodyPara("code", code) + .addBodyPara("client_id", clientId) + .addBodyPara("client_secret", clientSecret) + .post() + .getBody() + .toString(); + SoMap so = SoMap.getSoMap().setJsonString(str); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); + + // code不等于200 代表请求失败 + if(so.getInt("code") != 200) { + return SaResult.error(so.getString("msg")); + } + + // 根据openid获取其对应的userId + long uid = getUserIdByOpenid(so.getString("openid")); + so.set("uid", uid); + + // 返回相关参数 + StpUtil.login(uid); + return SaResult.data(so); + } + + // 根据 Refresh-Token 去刷新 Access-Token + @RequestMapping("/refresh") + public SaResult refresh(String refreshToken) throws JsonProcessingException { + // 调用Server端接口,通过 Refresh-Token 刷新出一个新的 Access-Token + String str = OkHttps.sync(serverUrl + "/oauth2/refresh") + .addBodyPara("grant_type", "refresh_token") + .addBodyPara("client_id", clientId) + .addBodyPara("client_secret", clientSecret) + .addBodyPara("refresh_token", refreshToken) + .post() + .getBody() + .toString(); + SoMap so = SoMap.getSoMap().setJsonString(str); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); + + // code不等于200 代表请求失败 + if(so.getInt("code") != 200) { + return SaResult.error(so.getString("msg")); + } + + // 返回相关参数 + return SaResult.data(so); + } + + // 模式三:密码式-授权登录 + @RequestMapping("/passwordLogin") + public SaResult passwordLogin(String username, String password) throws JsonProcessingException { + // 模式三:密码式-授权登录 + String str = OkHttps.sync(serverUrl + "/oauth2/token") + .addBodyPara("grant_type", "password") + .addBodyPara("client_id", clientId) + .addBodyPara("client_secret", clientSecret) + .addBodyPara("username", username) + .addBodyPara("password", password) + .post() + .getBody() + .toString(); + SoMap so = SoMap.getSoMap().setJsonString(str); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); + + // code不等于200 代表请求失败 + if(so.getInt("code") != 200) { + return SaResult.error(so.getString("msg")); + } + + // 根据openid获取其对应的userId + long uid = getUserIdByOpenid(so.getString("openid")); + so.set("uid", uid); + + // 返回相关参数 + StpUtil.login(uid); + return SaResult.data(so); + } + + // 模式四:获取应用的 Client-Token + @RequestMapping("/clientToken") + public SaResult clientToken() throws JsonProcessingException { + // 调用Server端接口 + String str = OkHttps.sync(serverUrl + "/oauth2/client_token") + .addBodyPara("grant_type", "client_credentials") + .addBodyPara("client_id", clientId) + .addBodyPara("client_secret", clientSecret) + .post() + .getBody() + .toString(); + SoMap so = SoMap.getSoMap().setJsonString(str); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); + + // code不等于200 代表请求失败 + if(so.getInt("code") != 200) { + return SaResult.error(so.getString("msg")); + } + + // 返回相关参数 + return SaResult.data(so); + } + + // 注销登录 + @RequestMapping("/logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + + // 根据 Access-Token 置换相关的资源: 获取账号昵称、头像、性别等信息 + @RequestMapping("/getUserinfo") + public SaResult getUserinfo(String accessToken) throws JsonProcessingException { + // 调用Server端接口,查询开放的资源 + String str = OkHttps.sync(serverUrl + "/oauth2/userinfo") + .addBodyPara("access_token", accessToken) + .post() + .getBody() + .toString(); + SoMap so = SoMap.getSoMap().setJsonString(str); + System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); + + // code不等于200 代表请求失败 + if(so.getInt("code") != 200) { + return SaResult.error(so.getString("msg")); + } + + // 返回相关参数 (data=获取到的资源 ) + return SaResult.data(so); + } + + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + + + // ------------ 模拟方法 ------------------ + // 模拟方法:根据openid获取userId + private long getUserIdByOpenid(String openid) { + // 此方法仅做模拟,实际开发要根据具体业务逻辑来获取userId + return 10001; + } + +} diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java new file mode 100644 index 000000000..dbb58d9d3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java @@ -0,0 +1,737 @@ +package com.pj.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦 + *

所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用 + *

最新:2020-12-10 新增部分构造方法 + * @author click33 + */ +public class SoMap extends LinkedHashMap { + + private static final long serialVersionUID = 1L; + + public SoMap() { + } + + /** 以下元素会在isNull函数中被判定为Null, */ + public static final Object[] NULL_ELEMENT_ARRAY = {null, ""}; + public static final List NULL_ELEMENT_LIST; + + static { + NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY); + } + + // ============================= 读值 ============================= + + /** 获取一个值 */ + @Override + public Object get(Object key) { + if("this".equals(key)) { + return this; + } + return super.get(key); + } + + /** 如果为空,则返回默认值 */ + public Object get(Object key, Object defaultValue) { + Object value = get(key); + if(valueIsNull(value)) { + return defaultValue; + } + return value; + } + + /** 转为String并返回 */ + public String getString(String key) { + Object value = get(key); + if(value == null) { + return null; + } + return String.valueOf(value); + } + + /** 如果为空,则返回默认值 */ + public String getString(String key, String defaultValue) { + Object value = get(key); + if(valueIsNull(value)) { + return defaultValue; + } + return String.valueOf(value); + } + + /** 转为int并返回 */ + public int getInt(String key) { + Object value = get(key); + if(valueIsNull(value)) { + return 0; + } + return Integer.valueOf(String.valueOf(value)); + } + /** 转为int并返回,同时指定默认值 */ + public int getInt(String key, int defaultValue) { + Object value = get(key); + if(valueIsNull(value)) { + return defaultValue; + } + return Integer.valueOf(String.valueOf(value)); + } + + /** 转为long并返回 */ + public long getLong(String key) { + Object value = get(key); + if(valueIsNull(value)) { + return 0; + } + return Long.valueOf(String.valueOf(value)); + } + + /** 转为double并返回 */ + public double getDouble(String key) { + Object value = get(key); + if(valueIsNull(value)) { + return 0.0; + } + return Double.valueOf(String.valueOf(value)); + } + + /** 转为boolean并返回 */ + public boolean getBoolean(String key) { + Object value = get(key); + if(valueIsNull(value)) { + return false; + } + return Boolean.valueOf(String.valueOf(value)); + } + + /** 转为Date并返回,根据自定义格式 */ + public Date getDateByFormat(String key, String format) { + try { + return new SimpleDateFormat(format).parse(getString(key)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** 转为Date并返回,根据格式: yyyy-MM-dd */ + public Date getDate(String key) { + return getDateByFormat(key, "yyyy-MM-dd"); + } + + /** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */ + public Date getDateTime(String key) { + return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss"); + } + + /** 转为Map并返回 */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public SoMap getMap(String key) { + Object value = get(key); + if(value == null) { + return SoMap.getSoMap(); + } + if(value instanceof Map) { + return SoMap.getSoMap((Map)value); + } + if(value instanceof String) { + return SoMap.getSoMap().setJsonString((String)value); + } + throw new RuntimeException("值无法转化为SoMap: " + value); + } + + /** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */ + @SuppressWarnings("unchecked") + public List getList(String key) { + Object value = get(key); + List list = null; + if(value == null || value.equals("")) { + list = new ArrayList(); + } + else if(value instanceof List) { + list = (List)value; + } else { + list = new ArrayList(); + list.add(value); + } + return list; + } + + /** 获取集合 (指定泛型类型) */ + public List getList(String key, Class cs) { + List list = getList(key); + List list2 = new ArrayList(); + for (Object obj : list) { + T objC = getValueByClass(obj, cs); + list2.add(objC); + } + return list2; + } + + /** 获取集合(逗号分隔式),(指定类型) */ + public List getListByComma(String key, Class cs) { + String listStr = getString(key); + if(listStr == null || listStr.equals("")) { + return new ArrayList<>(); + } + // 开始转化 + String [] arr = listStr.split(","); + List list = new ArrayList(); + for (String str : arr) { + if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) { + str = str.trim(); + } + T objC = getValueByClass(str, cs); + list.add(objC); + } + return list; + } + + + /** 根据指定类型从map中取值,返回实体对象 */ + public T getModel(Class cs) { + try { + return getModelByObject(cs.newInstance()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** 从map中取值,塞到一个对象中 */ + public T getModelByObject(T obj) { + // 获取类型 + Class cs = obj.getClass(); + // 循环复制 + for (Field field : cs.getDeclaredFields()) { + try { + // 获取对象 + Object value = this.get(field.getName()); + if(value == null) { + continue; + } + field.setAccessible(true); + Object valueConvert = getValueByClass(value, field.getType()); + field.set(obj, valueConvert); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException("属性取值出错:" + field.getName(), e); + } + } + return obj; + } + + + + /** + * 将指定值转化为指定类型并返回 + * @param obj + * @param cs + * @param + * @return + */ + @SuppressWarnings("unchecked") + public static T getValueByClass(Object obj, Class cs) { + String obj2 = String.valueOf(obj); + Object obj3 = null; + if (cs.equals(String.class)) { + obj3 = obj2; + } else if (cs.equals(int.class) || cs.equals(Integer.class)) { + obj3 = Integer.valueOf(obj2); + } else if (cs.equals(long.class) || cs.equals(Long.class)) { + obj3 = Long.valueOf(obj2); + } else if (cs.equals(short.class) || cs.equals(Short.class)) { + obj3 = Short.valueOf(obj2); + } else if (cs.equals(byte.class) || cs.equals(Byte.class)) { + obj3 = Byte.valueOf(obj2); + } else if (cs.equals(float.class) || cs.equals(Float.class)) { + obj3 = Float.valueOf(obj2); + } else if (cs.equals(double.class) || cs.equals(Double.class)) { + obj3 = Double.valueOf(obj2); + } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) { + obj3 = Boolean.valueOf(obj2); + } else { + obj3 = (T)obj; + } + return (T)obj3; + } + + + // ============================= 写值 ============================= + + /** + * 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去) + */ + public void setDefaultValue(String key, Object defaultValue) { + if(isNull(key)) { + set(key, defaultValue); + } + } + + /** set一个值,连缀风格 */ + public SoMap set(String key, Object value) { + // 防止敏感key + if(key.toLowerCase().equals("this")) { + return this; + } + put(key, value); + return this; + } + + /** 将一个Map塞进SoMap */ + public SoMap setMap(Map map) { + if(map != null) { + for (String key : map.keySet()) { + this.set(key, map.get(key)); + } + } + return this; + } + + /** 将一个对象解析塞进SoMap */ + public SoMap setModel(Object model) { + if(model == null) { + return this; + } + Field[] fields = model.getClass().getDeclaredFields(); + for (Field field : fields) { + try{ + field.setAccessible(true); + boolean isStatic = Modifier.isStatic(field.getModifiers()); + if(!isStatic) { + this.set(field.getName(), field.get(model)); + } + }catch (Exception e){ + throw new RuntimeException(e); + } + } + return this; + } + + /** 将json字符串解析后塞进SoMap */ + public SoMap setJsonString(String jsonString) { + try { + @SuppressWarnings("unchecked") + Map map = new ObjectMapper().readValue(jsonString, Map.class); + return this.setMap(map); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + + // ============================= 删值 ============================= + + /** delete一个值,连缀风格 */ + public SoMap delete(String key) { + remove(key); + return this; + } + + /** 清理所有value为null的字段 */ + public SoMap clearNull() { + Iterator iterator = this.keySet().iterator(); + while(iterator.hasNext()) { + String key = iterator.next(); + if(this.isNull(key)) { + iterator.remove(); + this.remove(key); + } + + } + return this; + } + /** 清理指定key */ + public SoMap clearIn(String ...keys) { + List keys2 = Arrays.asList(keys); + Iterator iterator = this.keySet().iterator(); + while(iterator.hasNext()) { + String key = iterator.next(); + if(keys2.contains(key) == true) { + iterator.remove(); + this.remove(key); + } + } + return this; + } + /** 清理掉不在列表中的key */ + public SoMap clearNotIn(String ...keys) { + List keys2 = Arrays.asList(keys); + Iterator iterator = this.keySet().iterator(); + while(iterator.hasNext()) { + String key = iterator.next(); + if(keys2.contains(key) == false) { + iterator.remove(); + this.remove(key); + } + + } + return this; + } + /** 清理掉所有key */ + public SoMap clearAll() { + clear(); + return this; + } + + + // ============================= 快速构建 ============================= + + /** 构建一个SoMap并返回 */ + public static SoMap getSoMap() { + return new SoMap(); + } + /** 构建一个SoMap并返回 */ + public static SoMap getSoMap(String key, Object value) { + return new SoMap().set(key, value); + } + /** 构建一个SoMap并返回 */ + public static SoMap getSoMap(Map map) { + return new SoMap().setMap(map); + } + + /** 将一个对象集合解析成为SoMap */ + public static SoMap getSoMapByModel(Object model) { + return SoMap.getSoMap().setModel(model); + } + + /** 将一个对象集合解析成为SoMap集合 */ + public static List getSoMapByList(List list) { + List listMap = new ArrayList(); + for (Object model : list) { + listMap.add(getSoMapByModel(model)); + } + return listMap; + } + + /** 克隆指定key,返回一个新的SoMap */ + public SoMap cloneKeys(String... keys) { + SoMap so = new SoMap(); + for (String key : keys) { + so.set(key, this.get(key)); + } + return so; + } + /** 克隆所有key,返回一个新的SoMap */ + public SoMap cloneSoMap() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(key, this.get(key)); + } + return so; + } + + /** 将所有key转为大写 */ + public SoMap toUpperCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(key.toUpperCase(), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + /** 将所有key转为小写 */ + public SoMap toLowerCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(key.toLowerCase(), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + /** 将所有key中下划线转为中划线模式 (kebab-case风格) */ + public SoMap toKebabCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(wordEachKebabCase(key), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + /** 将所有key中下划线转为小驼峰模式 */ + public SoMap toHumpCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(wordEachBigFs(key), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + /** 将所有key中小驼峰转为下划线模式 */ + public SoMap humpToLineCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(wordHumpToLine(key), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + + + + + // ============================= 辅助方法 ============================= + + + /** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */ + public boolean isNull(String key) { + return valueIsNull(get(key)); + } + + /** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */ + public boolean isContainNull(String ...keys) { + for (String key : keys) { + if(this.isNull(key)) { + return true; + } + } + return false; + } + + /** 与isNull()相反 */ + public boolean isNotNull(String key) { + return !isNull(key); + } + /** 指定key的value是否为null,作用同isNotNull() */ + public boolean has(String key) { + return !isNull(key); + } + + /** 指定value在此SoMap的判断标准中是否为null */ + public boolean valueIsNull(Object value) { + return NULL_ELEMENT_LIST.contains(value); + } + + /** 验证指定key不为空,为空则抛出异常 */ + public SoMap checkNull(String ...keys) { + for (String key : keys) { + if(this.isNull(key)) { + throw new RuntimeException("参数" + key + "不能为空"); + } + } + return this; + } + + static Pattern patternNumber = Pattern.compile("[0-9]*"); + /** 指定key是否为数字 */ + public boolean isNumber(String key) { + String value = getString(key); + if(value == null) { + return false; + } + return patternNumber.matcher(value).matches(); + } + + + + + /** + * 转为JSON字符串 + */ + public String toJsonString() { + try { +// SoMap so = SoMap.getSoMap(this); + return new ObjectMapper().writeValueAsString(this); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +// /** +// * 转为JSON字符串, 带格式的 +// */ +// public String toJsonFormatString() { +// try { +// return JSON.toJSONString(this, true); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// } + + // ============================= web辅助 ============================= + + + /** + * 返回当前request请求的的所有参数 + * @return + */ + public static SoMap getRequestSoMap() { + // 大善人SpringMVC提供的封装 + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if(servletRequestAttributes == null) { + throw new RuntimeException("当前线程非JavaWeb环境"); + } + // 当前request + HttpServletRequest request = servletRequestAttributes.getRequest(); + if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) { + initRequestSoMap(request); + } + return (SoMap)request.getAttribute("currentSoMap"); + } + + /** 初始化当前request的 SoMap */ + private static void initRequestSoMap(HttpServletRequest request) { + SoMap soMap = new SoMap(); + Map parameterMap = request.getParameterMap(); // 获取所有参数 + for (String key : parameterMap.keySet()) { + try { + String[] values = parameterMap.get(key); // 获得values + if(values.length == 1) { + soMap.set(key, values[0]); + } else { + List list = new ArrayList(); + for (String v : values) { + list.add(v); + } + soMap.set(key, list); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + request.setAttribute("currentSoMap", soMap); + } + + /** + * 验证返回当前线程是否为JavaWeb环境 + * @return + */ + public static boolean isJavaWeb() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装 + if(servletRequestAttributes == null) { + return false; + } + return true; + } + + + + // ============================= 常见key (以下key经常用,所以封装以下,方便写代码) ============================= + + /** get 当前页 */ + public int getKeyPageNo() { + int pageNo = getInt("pageNo", 1); + if(pageNo <= 0) { + pageNo = 1; + } + return pageNo; + } + /** get 页大小 */ + public int getKeyPageSize() { + int pageSize = getInt("pageSize", 10); + if(pageSize <= 0 || pageSize > 1000) { + pageSize = 10; + } + return pageSize; + } + + /** get 排序方式 */ + public int getKeySortType() { + return getInt("sortType"); + } + + + + + + + // ============================= 工具方法 ============================= + + + /** + * 将一个一维集合转换为树形集合 + * @param list 集合 + * @param idKey id标识key + * @param parentIdKey 父id标识key + * @param childListKey 子节点标识key + * @return 转换后的tree集合 + */ + public static List listToTree(List list, String idKey, String parentIdKey, String childListKey) { + // 声明新的集合,存储tree形数据 + List newTreeList = new ArrayList(); + // 声明hash-Map,方便查找数据 + SoMap hash = new SoMap(); + // 将数组转为Object的形式,key为数组中的id + for (int i = 0; i < list.size(); i++) { + SoMap json = (SoMap) list.get(i); + hash.put(json.getString(idKey), json); + } + // 遍历结果集 + for (int j = 0; j < list.size(); j++) { + // 单条记录 + SoMap aVal = (SoMap) list.get(j); + // 在hash中取出key为单条记录中pid的值 + SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString()); + // 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中 + if (hashVp != null) { + // 检查是否有child属性,有则添加,没有则新建 + if (hashVp.get(childListKey) != null) { + @SuppressWarnings("unchecked") + List ch = (List) hashVp.get(childListKey); + ch.add(aVal); + hashVp.put(childListKey, ch); + } else { + List ch = new ArrayList(); + ch.add(aVal); + hashVp.put(childListKey, ch); + } + } else { + newTreeList.add(aVal); + } + } + return newTreeList; + } + + + + /** 指定字符串的字符串下划线转大写模式 */ + private static String wordEachBig(String str){ + String newStr = ""; + for (String s : str.split("_")) { + newStr += wordFirstBig(s); + } + return newStr; + } + /** 返回下划线转小驼峰形式 */ + private static String wordEachBigFs(String str){ + return wordFirstSmall(wordEachBig(str)); + } + + /** 将指定单词首字母大写 */ + private static String wordFirstBig(String str) { + return str.substring(0, 1).toUpperCase() + str.substring(1, str.length()); + } + + /** 将指定单词首字母小写 */ + private static String wordFirstSmall(String str) { + return str.substring(0, 1).toLowerCase() + str.substring(1, str.length()); + } + + /** 下划线转中划线 */ + private static String wordEachKebabCase(String str) { + return str.replaceAll("_", "-"); + } + + /** 驼峰转下划线 */ + private static String wordHumpToLine(String str) { + return str.replaceAll("[A-Z]", "_$0").toLowerCase(); + } + + +} diff --git a/sa-token-demo/sa-token-demo-oauth2-client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/application.yml similarity index 100% rename from sa-token-demo/sa-token-demo-oauth2-client/src/main/resources/application.yml rename to sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/application.yml 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 new file mode 100644 index 000000000..86d7e66e8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html @@ -0,0 +1,258 @@ + + + + + Sa-OAuth2-Client端-测试页 + + + + + + + + + + diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.css b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.css new file mode 100644 index 000000000..f7be05c8e --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.css @@ -0,0 +1,48 @@ +*{margin: 0; padding: 0;} +body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;} +::-webkit-input-placeholder{color: #ccc;} + +/* 视图盒子 */ +.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;} +/* 背景 EAEFF3 */ +.bg-1{height: 100%; background: #E4B17F;} + +/* 内容盒子 */ +.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;} +.content-box{display: none;} +.region-default{display: block;} +.message-box{width: 100%;; text-align: center;} + +/* 登录盒子 */ +/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */ +.login-box{width: 450px; margin: auto; max-width: 90%; height: 100%;} +.login-box{display: flex; align-items: center; text-align: center;} + +/* 表单 */ +.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;} +.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;} +.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;} + +/* 输入框 */ +.from-item{border: 0px #000 solid; margin-bottom: 15px;} +.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;} +.s-input{font-size: 12px;} +.s-input:focus{border-color: #409eff} + +/* 登录按钮 */ +.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;} +.s-btn:hover{background-color: #50aEFF;} + +/* 重置按钮 */ +.reset-box{text-align: left; font-size: 12px;} +.reset-box a{text-decoration: none;} +.reset-box a:hover{text-decoration: underline;} + +/* 确认授权按钮 */ +.confirm-btn{text-indent: 0; cursor: pointer; background-color: #409EFF; border: 1px #409EFF solid; color: #FFF; padding: 5px 15px;} +.confirm-btn:hover{background-color: #50aEFF;} + +/* loading框样式 */ +.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} +.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} +.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.js b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.js new file mode 100644 index 000000000..2e53369a8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.js @@ -0,0 +1,194 @@ +// OAuth-Server 后端 接口地址 +var baseUrl = "http://sa-oauth-server.com:8000"; + + +// ----------------------------------- 相关事件 ----------------------------------- + +// 显示默认区域 +function showDefaultRegion(){ + $('.content-box').hide(); + $('.region-default').show(); +} +// 显示登录框区域 +function showLoginRegion(){ + $('.content-box').hide(); + $('.region-login').show(); +} +// 显示确认授权框区域 +function showConfirmRegion(){ + $('.content-box').hide(); + $('.region-confirm').show(); + $('.show-clientId').text(getParam('client_id')); + $('.show-scope').text(getParam('scope')); +} + +// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码 +function tryJump(){ + var data = location.search.substr(1); + sa.ajax("/oauth2/getRedirectUri", data, function(res) { + // 情况1:客户端未登录,返回 code=401,提示用户登录 + if(res.code === 401) { + showLoginRegion(); + return; + } + + // 情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认 + if(res.code === 411) { + showConfirmRegion(); + return; + } + + // 情况3:已登录且请求的 scope 已确认授权,返回 code=200,data=最终重定向 url 地址(携带code码参数) + if(res.code == 200) { + console.log('跳转:', res.redirect_uri); + location.href = res.redirect_uri; + return; + } + + console.log('未知状态码,', res.code, res); + layer.alert('错误:' + JSON.stringify(res)) + }) +} + +// 登录事件 +function doLogin() { + // 开始登录 + var data = { + name: $('[name=name]').val(), + pwd: $('[name=pwd]').val() + }; + sa.ajax("/oauth2/doLogin", data, function(res) { + if(res.code == 200) { + localStorage.setItem('satoken', res.satoken); + layer.msg('登录成功', {anim: 0, icon: 6 }); + setTimeout(function() { + location.reload(); + }, 800); + } else { + layer.msg(res.msg, {anim: 6, icon: 2 }); + } + }) +} + +// 确认授权事件 +function yes() { + var data = location.search.substr(1) + '&build_redirect_uri=true'; + sa.ajax("/oauth2/doConfirm", data, function(res) { + if(res.code == 200) { + layer.msg('确认授权成功,即将跳转...', {anim: 0, icon: 6 }); + setTimeout(function() { + console.log('跳转:', res.redirect_uri); + location.href = res.redirect_uri; + }, 800); + } else { + layer.msg(res.msg, {anim: 6, icon: 2 }); + } + }) +} + +// 拒绝授权事件 +function no() { + var url = joinParam(getParam('redirect_uri'), "handle=refuse&msg=用户拒绝了授权"); + location.href = url; +} + +// 页面加载完毕后触发 +window.onload = function() { + tryJump(); + + // 绑定回车事件 + $('[name=name],[name=pwd]').bind('keypress', function(event){ + if(event.keyCode == "13") { + $('.login-btn').click(); + } + }); + + // 输入框获取焦点 + $("[name=name]").focus(); +} + + + +// ----------------------------------- 工具函数封装 ----------------------------------- + +// sa +var sa = {}; + +// 打开loading +sa.loading = function(msg) { + layer.closeAll(); // 开始前先把所有弹窗关了 + return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'}); +}; + +// 隐藏loading +sa.hideLoading = function() { + layer.closeAll(); +}; + +// 封装一下Ajax +sa.ajax = function(url, data, successFn) { + sa.loading("加载中..."); + $.ajax({ + url: baseUrl + url, + type: "post", + data: data, + dataType: 'json', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'satoken': localStorage.getItem('satoken') + }, + success: function(res){ + sa.hideLoading(); + console.log('返回数据:', res); + successFn(res); + }, + error: function(xhr, type, errorThrown){ + sa.hideLoading(); + if(xhr.status == 0){ + return alert('无法连接到服务器,请检查网络'); + } + return alert("异常:" + JSON.stringify(xhr)); + } + }); +} + +// 从url中查询到指定名称的参数值 +function getParam(name, defaultValue){ + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i -1 && index < url.length - 1) { + // 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 & + if(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) { + return url + '&' + parameStr; + } else { + return url + parameStr; + } + } +} + +// 打印信息 +var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.com/"; +console.log(str); + diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/oauth2-authorize.html b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/oauth2-authorize.html new file mode 100644 index 000000000..c39fe6bca --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/oauth2-authorize.html @@ -0,0 +1,81 @@ + + + + Sa-OAuth2-Server 认证中心(前后端分离版) + + + + + +
+
+ + + + +
+ +
+ + + + + +
+ +
+ + +
+ This page is provided by Sa-Token-OAuth2 +
+
+ + + + + + + + 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 new file mode 100644 index 000000000..dbb8d9fbe --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml @@ -0,0 +1,88 @@ + + 4.0.0 + com.pj + sa-token-demo-oauth2-server + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + 1.8 + 3.1.1 + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-oauth2 + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + cn.dev33 + sa-token-jwt + ${sa-token.version} + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + + \ 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/SaOAuth2ServerApplication.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java new file mode 100644 index 000000000..e7bef9358 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java @@ -0,0 +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 + */ +@SpringBootApplication +public class SaOAuth2ServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SaOAuth2ServerApplication.class, args); + 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/mock/SaClientMockDao.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/mock/SaClientMockDao.java new file mode 100644 index 000000000..ff881b8c6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/mock/SaClientMockDao.java @@ -0,0 +1,91 @@ +package com.pj.mock; + +import cn.dev33.satoken.oauth2.consts.GrantType; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * SaClientModel 模拟查询操作 + * + * @author click33 + * @since 2024/11/15 + */ +@Component +public class SaClientMockDao { + + public List list; + + /** + * 构造方法,添加三个模拟应用 + */ + public void init(){ + list = new ArrayList<>(); + + // 模拟应用1 + SaClientModel client1 = new SaClientModel() + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowRedirectUris("*") // 所有允许授权的 url + .addContractScopes("openid", "unionid", "userid", "userinfo", "oidc") // 所有签约的权限 + .setSubjectId("1000001") // 主体 id (可选) + .addAllowGrantTypes( // 所有允许的授权模式 + GrantType.authorization_code, // 授权码式 + GrantType.implicit, // 隐藏式 + GrantType.refresh_token, // 刷新令牌 + GrantType.password, // 密码式 + GrantType.client_credentials, // 客户端模式 + "phone_code" // 自定义授权模式 手机号验证码登录 + ); + list.add(client1); + + // 模拟应用2 + SaClientModel client2 = new SaClientModel() + .setClientId("1002") + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") + .addAllowRedirectUris("*") + .addContractScopes("openid", "unionid", "userid", "userinfo", "oidc") + .setSubjectId("1000001") // 主体 id (可选) + .addAllowGrantTypes( + GrantType.authorization_code, + GrantType.implicit, + GrantType.refresh_token, + GrantType.password, + GrantType.client_credentials + ); + list.add(client2); + + // 模拟应用3 + SaClientModel client3 = new SaClientModel() + .setClientId("1003") + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") + .addAllowRedirectUris("*") + .addContractScopes("openid", "unionid", "userid", "userinfo", "oidc") + .addAllowGrantTypes( + GrantType.authorization_code, + GrantType.implicit, + GrantType.refresh_token, + GrantType.password, + GrantType.client_credentials + ); + list.add(client3); + } + + /** + * 根据应用 id 查找对应的应用,找不到则返回 null + * @param clientId 应用 id + * @return 应用对象 + */ + public SaClientModel getClientModel(String clientId) { + if(list == null) { + init(); + } + return list.stream() + .filter(e -> e.getClientId().equals(clientId)) + .findFirst() + .orElse(null); + } + +} \ 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/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 000000000..f46cf4761 --- /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,34 @@ +package com.pj.oauth2; + +import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import com.pj.mock.SaClientMockDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Sa-Token OAuth2:自定义数据加载器 + * + * @author click33 + */ +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + + @Autowired + SaClientMockDao saClientMockDao; + + // 根据 clientId 获取 Client 信息 + @Override + public SaClientModel getClientModel(String clientId) { + // 此为模拟数据,真实环境需要从数据库查询 + return saClientMockDao.getClientModel(clientId); + } + + // 根据 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/SaOAuth2ResourcesController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ResourcesController.java new file mode 100644 index 000000000..82d9b3bc7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ResourcesController.java @@ -0,0 +1,51 @@ +package com.pj.oauth2; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.template.SaOAuth2Util; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sa-Token OAuth2 Resources 端 Controller + * + *

Resources 端:OAuth2 资源端,允许 Client 端根据 Access-Token 置换相关资源

+ * + *

在 OAuth2 中,认证端和资源端: + * 1、可以在一个 Controller 中,也可以在不同的 Controller 中 + * 2、可以在同一个项目中,也可以在不同的项目中(在不同项目中时需要两端连同一个 Redis ) + *

+ * + * @author click33 + * @since 2024/12/6 + */ +@RestController +public class SaOAuth2ResourcesController { + + // 示例:获取 userinfo 信息:昵称、头像、性别等等 + @RequestMapping("/oauth2/userinfo") + public SaResult userinfo() { + // 获取 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.checkAccessTokenScope(accessToken, "userinfo"); + + // 模拟账号信息 (真实环境需要查询数据库获取信息) + 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.ok().setMap(map); + } + +} \ 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/SaOAuth2ServerController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java new file mode 100644 index 000000000..163d550b0 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java @@ -0,0 +1,59 @@ +package com.pj.oauth2; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import java.util.HashMap; +import java.util.Map; + +/** + * Sa-Token-OAuth2 Server 认证端 Controller + * + * @author click33 + */ +@RestController +public class SaOAuth2ServerController { + + // OAuth2-Server 端:处理所有 OAuth2 相关请求 + @RequestMapping("/oauth2/*") + public Object request() { + System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl()); + return SaOAuth2ServerProcessor.instance.dister(); + } + + // Sa-Token OAuth2 定制化配置 + @Autowired + public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + // 未登录的视图 + SaOAuth2Strategy.instance.notLoginView = ()->{ + return new ModelAndView("login.html"); + }; + + // 登录处理函数 + SaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> { + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok().set("satoken", StpUtil.getTokenValue()); + } + return SaResult.error("账号名或密码错误"); + }; + + // 授权确认视图 + SaOAuth2Strategy.instance.confirmView = (clientId, scopes)->{ + Map map = new HashMap<>(); + map.put("clientId", clientId); + map.put("scope", scopes); + return new ModelAndView("confirm.html", map); + }; + + } + +} diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/CustomPasswordGrantTypeHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/CustomPasswordGrantTypeHandler.java new file mode 100644 index 000000000..c41f0d456 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/CustomPasswordGrantTypeHandler.java @@ -0,0 +1,27 @@ +//package com.pj.oauth2.custom_grant_type; +// +//import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +//import cn.dev33.satoken.oauth2.granttype.handler.PasswordGrantTypeHandler; +//import cn.dev33.satoken.oauth2.granttype.handler.model.PasswordAuthResult; +//import org.springframework.stereotype.Component; +// +///** +// * 自定义 Password Grant_Type 授权模式处理器认证过程 +// * +// * @author click33 +// * @since 2025/5/11 +// */ +//@Component +//public class CustomPasswordGrantTypeHandler extends PasswordGrantTypeHandler { +// +// @Override +// public PasswordAuthResult loginByUsernamePassword(String username, String password) { +// if("sa".equals(username) && "123456".equals(password)) { +// long userId = 10001; +// return new PasswordAuthResult(userId); +// } else { +// throw new SaOAuth2Exception("无效账号密码"); +// } +// } +// +//} \ 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_grant_type/PhoneCodeGrantTypeHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/PhoneCodeGrantTypeHandler.java new file mode 100644 index 000000000..a34c44bb0 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/PhoneCodeGrantTypeHandler.java @@ -0,0 +1,57 @@ +//package com.pj.oauth2.custom_grant_type; +// +//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, atm -> atm.grantType = "phone_code"); +// 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_grant_type/PhoneLoginController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/PhoneLoginController.java new file mode 100644 index 000000000..2be986544 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/PhoneLoginController.java @@ -0,0 +1,26 @@ +//package com.pj.oauth2.custom_grant_type; +// +//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_scope/CustomOidcScopeHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_scope/CustomOidcScopeHandler.java new file mode 100644 index 000000000..efdc06f19 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_scope/CustomOidcScopeHandler.java @@ -0,0 +1,33 @@ +//package com.pj.oauth2.custom_scope; +// +//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", "linXiaoLin"); // 昵称 +// idToken.extraData.put("picture", "https://sa-token.com/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_scope/UserinfoScopeHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_scope/UserinfoScopeHandler.java new file mode 100644 index 000000000..5e52987d4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_scope/UserinfoScopeHandler.java @@ -0,0 +1,52 @@ +//package com.pj.oauth2.custom_scope; +// +//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; +// +///** +// * 自定义 userinfo scope 处理器 +// * @author click33 +// * @since 2024/8/20 +// */ +//@Component +//public class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface { +// +// // 指示当前处理器所要处理的 scope +// @Override +// public String getHandlerScope() { +// return "userinfo"; +// } +// +// // 当构建的 AccessToken 具有此权限时,所需要执行的方法 +// @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); +// } +// +// // 当构建的 ClientToken 具有此权限时,所需要执行的方法 +// @Override +// public void workClientToken(ClientTokenModel ct) { +// } +// +// // 当使用 RefreshToken 刷新 AccessToken 时,是否重新执行 workAccessToken 构建方法 +// // 在一些实时性较高的数据中需要指定为 true +// @Override +// public boolean refreshAccessTokenIsWork() { +// return true; +// } +// +//} \ 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/h5/SaOAuth2ServerH5Controller.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/h5/SaOAuth2ServerH5Controller.java new file mode 100644 index 000000000..73600d098 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/h5/SaOAuth2ServerH5Controller.java @@ -0,0 +1,97 @@ +package com.pj.oauth2.h5; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +import cn.dev33.satoken.oauth2.template.SaOAuth2Template; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口) + */ +@RestController +public class SaOAuth2ServerH5Controller { + + /** + * 获取最终授权重定向地址,形如:http://xxx.com/xxx?code=xxxxx + * + *

情况1:客户端未登录,返回 code=401,提示用户登录

+ *

情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认

+ *

情况3:已登录且请求的 scope 已确认授权,返回 code=200,redirect_uri=最终重定向 url 地址(携带code码参数)

+ * + * @return / + */ + @PostMapping("/oauth2/getRedirectUri") + public Object getRedirectUri() { + + // 获取变量 + SaRequest req = SaHolder.getRequest(); + SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); + SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); + SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); + String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type); + + // 1、先判断是否开启了指定的授权模式 + SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg); + + // 2、如果尚未登录, 则先去登录 + long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L); + if(loginId == 0L) { + return SaResult.get(401, "need login", null); + } + + // 3、构建请求 Model + RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId); + + // 4、开发者自定义的授权前置检查 + SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId); + + // 5、校验:重定向域名是否合法 + oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri); + + // 6、校验:此次申请的Scope,该Client是否已经签约 + oauth2Template.checkContractScope(ra.clientId, ra.scopes); + + // 7、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面 + boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes); + if(isNeedCarefulConfirm) { + SaClientModel cm = oauth2Template.checkClientModel(ra.clientId); + if( ! cm.getIsAutoConfirm()) { + // code=411,需要用户手动确认授权 + return SaResult.get(411, "need confirm", null); + } + } + + // 8、判断授权类型,重定向到不同地址 + // 如果是 授权码式,则:开始重定向授权,下放code + if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) { + CodeModel codeModel = dataGenerate.generateCode(ra); + String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); + return SaResult.ok().set("redirect_uri", redirectUri); + } + + // 如果是 隐藏式,则:开始重定向授权,下放 token + if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) { + AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null); + String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); + return SaResult.ok().set("redirect_uri", redirectUri); + } + + // 默认返回 + throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125); + } + +} 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 000000000..b97704fa6 --- /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 000000000..779f1a498 --- /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,78 @@ +package com.pj.satoken; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**").addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + // ... + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行 + .setBeforeAuth(obj -> { + SaHolder.getResponse() + + // ---------- 设置跨域响应头 ---------- + // 允许指定域访问跨域资源 + .setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "*") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + ; + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/Test2Controller.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/Test2Controller.java new file mode 100644 index 000000000..0a325f5f5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/Test2Controller.java @@ -0,0 +1,108 @@ +package com.pj.test; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.template.SaOAuth2Util; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * 测试 OAuth2 相关 token 增删查 + * + * @author click33 + * @since 2024/8/25 + */ +@RestController +@RequestMapping("/test") +public class Test2Controller { + + // 测试:查询全部 Access-Token --- http://localhost:8000/test/getAccessTokenValueList?clientId=1001&loginId=10001 + @RequestMapping("/getAccessTokenValueList") + public SaResult getAccessTokenValueList(String clientId, long loginId) { + List accessTokenValueList = SaOAuth2Util.getAccessTokenValueList(clientId, loginId); + return SaResult.data(accessTokenValueList); + } + + // 测试:查询全部 Access-Token, 带过期时间 --- http://localhost:8000/test/getAccessTokenIndexMap?clientId=1001&loginId=10001 + @RequestMapping("/getAccessTokenIndexMap") + public SaResult getAccessTokenIndexMap(String clientId, long loginId) { + Map accessTokenIndexMap = SaOAuth2Manager.getDao().getAccessTokenIndexMap_FromAdjustAfter(clientId, loginId); + return SaResult.data(accessTokenIndexMap); + } + + // 测试:回收指定 Access-Token --- http://localhost:8000/test/revokeAccessToken?access_token=xxxxxxxxxx + @RequestMapping("/revokeAccessToken") + public SaResult revokeAccessToken(String access_token) { + SaOAuth2Util.revokeAccessToken(access_token); + return SaResult.ok(); + } + + // 测试:回收全部 Access-Token --- http://localhost:8000/test/revokeAccessTokenByIndex?clientId=1001&loginId=10001 + @RequestMapping("/revokeAccessTokenByIndex") + public SaResult revokeAccessTokenByIndex(String clientId, long loginId) { + SaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId); + return SaResult.ok(); + } + + + // 测试:查询全部 Refresh-Token --- http://localhost:8000/test/getRefreshTokenValueList?clientId=1001&loginId=10001 + @RequestMapping("/getRefreshTokenValueList") + public SaResult getRefreshTokenValueList(String clientId, long loginId) { + List refreshTokenValueList = SaOAuth2Util.getRefreshTokenValueList(clientId, loginId); + return SaResult.data(refreshTokenValueList); + } + + // 测试:查询全部 Refresh-Token, 带过期时间 --- http://localhost:8000/test/getRefreshTokenIndexMap?clientId=1001&loginId=10001 + @RequestMapping("/getRefreshTokenIndexMap") + public SaResult getRefreshTokenIndexMap(String clientId, long loginId) { + Map refreshTokenIndexMap = SaOAuth2Manager.getDao().getRefreshTokenIndexMap_FromAdjustAfter(clientId, loginId); + return SaResult.data(refreshTokenIndexMap); + } + + // 测试:回收指定 Refresh-Token --- http://localhost:8000/test/revokeRefreshToken?refresh_token=xxxxxxxxxx + @RequestMapping("/revokeRefreshToken") + public SaResult revokeRefreshToken(String refresh_token) { + SaOAuth2Util.revokeRefreshToken(refresh_token); + return SaResult.ok(); + } + + // 测试:回收全部 Refresh-Token --- http://localhost:8000/test/revokeRefreshTokenByIndex?clientId=1001&loginId=10001 + @RequestMapping("/revokeRefreshTokenByIndex") + public SaResult revokeRefreshTokenByIndex(String clientId, long loginId) { + SaOAuth2Util.revokeRefreshTokenByIndex(clientId, loginId); + return SaResult.ok(); + } + + + // 测试:查询全部 Client-Token --- http://localhost:8000/test/getClientTokenValueList?clientId=1001 + @RequestMapping("/getClientTokenValueList") + public SaResult getClientTokenValueList(String clientId) { + List clientTokenValueList = SaOAuth2Util.getClientTokenValueList(clientId); + return SaResult.data(clientTokenValueList); + } + + // 测试:查询全部 Client-Token, 带过期时间 --- http://localhost:8000/test/getClientTokenIndexMap?clientId=1001&loginId=10001 + @RequestMapping("/getClientTokenIndexMap") + public SaResult getClientTokenIndexMap(String clientId, long loginId) { + Map rlientTokenIndexMap = SaOAuth2Manager.getDao().getClientTokenIndexMap_FromAdjustAfter(clientId, loginId); + return SaResult.data(rlientTokenIndexMap); + } + + // 测试:回收指定 Client-Token --- http://localhost:8000/test/revokeClientToken?client_token=xxxxxxxxxxx + @RequestMapping("/revokeClientToken") + public SaResult revokeClientToken(String client_token) { + SaOAuth2Util.revokeClientToken(client_token); + return SaResult.ok(); + } + + // 测试:回收全部 Client-Token --- http://localhost:8000/test/revokeClientTokenByIndex?clientId=1001 + @RequestMapping("/revokeClientTokenByIndex") + public SaResult revokeClientTokenByIndex(String clientId) { + SaOAuth2Util.revokeClientTokenByIndex(clientId); + 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/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 000000000..8adc82dcf --- /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 new file mode 100644 index 000000000..5b2e9ebb8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml @@ -0,0 +1,51 @@ +server: + port: 8000 + +# sa-token配置 +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 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + # password: + # 连接超时时间(毫秒) + timeout: 1000ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + diff --git a/sa-token-demo/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 similarity index 83% rename from sa-token-demo/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html rename to sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html index 7ede1714d..49e801081 100644 --- a/sa-token-demo/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 @@ -24,7 +24,7 @@

Sa-OAuth2-认证中心-确认授权页


- + + + + + + diff --git a/sa-token-demo/sa-token-demo-quick-login-sb3/pom.xml b/sa-token-demo/sa-token-demo-quick-login-sb3/pom.xml new file mode 100644 index 000000000..355be2dfe --- /dev/null +++ b/sa-token-demo/sa-token-demo-quick-login-sb3/pom.xml @@ -0,0 +1,69 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-quick-login-sb3 + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 3.4.3 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-quick-login + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaQuicikStartup.java b/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaQuicikStartup.java new file mode 100644 index 000000000..ceaae6493 --- /dev/null +++ b/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaQuicikStartup.java @@ -0,0 +1,52 @@ +package com.pj; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.quick.SaQuickManager; + +/** + * springboot启动之后 + * @author click33 + * + */ +@Component +public class SaQuicikStartup implements CommandLineRunner { + + @Value("${spring.application.name:sa-quick}") + private String applicationName; + + @Value("${server.port:8080}") + private String port; + + @Value("${server.servlet.context-path:}") + private String path; + +// @Value("${spring.profiles.active:}") +// private String active; + + @Override + public void run(String... args) throws Exception { + String str = "\n------------- " + applicationName + " 启动成功 (" + getNow() + ") -------------\n" + + " - home: " + "http://localhost:" + port + path + "\n" + + " - name: " + SaQuickManager.getConfig().getName() + "\n"+ + " - pwd : " + SaQuickManager.getConfig().getPwd() + "\n"; + System.out.println(str); + } + + + + /** + * 返回系统当前时间的YYYY-MM-dd hh:mm:ss 字符串格式 + */ + private static String getNow(){ + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + } + + +} + diff --git a/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaTokenQuickSb3DemoApplication.java b/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaTokenQuickSb3DemoApplication.java new file mode 100644 index 000000000..e62c3b344 --- /dev/null +++ b/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaTokenQuickSb3DemoApplication.java @@ -0,0 +1,13 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaTokenQuickSb3DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenQuickSb3DemoApplication.class, args); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..b7a4d57a4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,27 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaTokenConsts; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +public class TestController { + + // 浏览器访问测试: http://localhost:8081 + @RequestMapping({"/"}) + public String index() { + String str = "
" +// + "

Welcome to the system

" + + "

资源页 (登录后才可进入本页面)

" + + "
" + + "

Sa-Token " + SaTokenConsts.VERSION_NO + "

"; + return str; + } + +} diff --git a/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/resources/application.yml new file mode 100644 index 000000000..8b111db3a --- /dev/null +++ b/sa-token-demo/sa-token-demo-quick-login-sb3/src/main/resources/application.yml @@ -0,0 +1,31 @@ +# 端口 +server: + port: 8081 + +# Sa-Token-Quick-Login 配置 +sa: + # 登录账号 + name: sa + # 登录密码 + pwd: 123456 + # 是否自动随机生成账号密码 (此项为true时, name与pwd失效) + auto: false + # 是否开启全局认证(关闭后将不再强行拦截) + auth: true + # 登录页标题 + title: Sa-Token 登录 + # 是否显示底部版权信息 + copr: true + # 指定拦截路径 + # include: /** + # 指定排除路径 + # exclude: /1.jpg + # 将本地磁盘的某个路径作为静态资源开放 + # dir: file:E:\static + + +# 静态文件路径映射 +spring: + resources: + static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/, ${sa.dir:} + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-quick-login/.gitignore b/sa-token-demo/sa-token-demo-quick-login/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-quick-login/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file 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 546564967..914c7d489 100644 --- a/sa-token-demo/sa-token-demo-quick-login/pom.xml +++ b/sa-token-demo/sa-token-demo-quick-login/pom.xml @@ -10,12 +10,14 @@ org.springframework.boot spring-boot-starter-parent - 2.0.0.RELEASE + 2.5.14 + + - + - 1.24.0 + 1.45.0 @@ -26,18 +28,18 @@ spring-boot-starter-web - + cn.dev33 sa-token-spring-boot-starter - ${sa-token-version} + ${sa-token.version} cn.dev33 sa-token-quick-login - ${sa-token-version} + ${sa-token.version} diff --git a/sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/SaQuicikStartup.java b/sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/SaQuicikStartup.java index 0bd613466..ceaae6493 100644 --- a/sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/SaQuicikStartup.java +++ b/sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/SaQuicikStartup.java @@ -11,7 +11,7 @@ /** * springboot启动之后 - * @author kong + * @author click33 * */ @Component diff --git a/sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/test/TestController.java index 29fd21baa..b7a4d57a4 100644 --- a/sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/test/TestController.java +++ b/sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/test/TestController.java @@ -7,7 +7,7 @@ /** * 测试专用Controller - * @author kong + * @author click33 * */ @RestController @@ -21,7 +21,7 @@ public String index() { + "

资源页 (登录后才可进入本页面)

" + "
" + "

Sa-Token " + SaTokenConsts.VERSION_NO + "

"; - return str; + return str; } } diff --git a/sa-token-demo/sa-token-demo-quick-login/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-quick-login/src/main/resources/application.yml index da4605017..8b111db3a 100644 --- a/sa-token-demo/sa-token-demo-quick-login/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-quick-login/src/main/resources/application.yml @@ -19,7 +19,7 @@ sa: # 指定拦截路径 # include: /** # 指定排除路径 - # exclude: /sss,/fff + # exclude: /1.jpg # 将本地磁盘的某个路径作为静态资源开放 # dir: file:E:\static diff --git a/sa-token-demo/sa-token-demo-remember-me/page_project/.gitignore b/sa-token-demo/sa-token-demo-remember-me/page_project/.gitignore new file mode 100644 index 000000000..cc5b432e5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/page_project/.gitignore @@ -0,0 +1,23 @@ +# vite创建项目时自动生成的git忽略配置文件 +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/sa-token-demo/sa-token-demo-remember-me/page_project/README.md b/sa-token-demo/sa-token-demo-remember-me/page_project/README.md new file mode 100644 index 000000000..6be215631 --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/page_project/README.md @@ -0,0 +1,11 @@ +# Vue 3 + Vite + +[Node下载地址](https://nodejs.org/zh-cn/) + +安装最新版本Node环境, 然后执行如下命令开启开发服务: +``` +npm install +npm run dev +``` + +[cookie/sessionstorage/localstorage三者的区别](https://blog.csdn.net/weixin_45541388/article/details/125367823) diff --git a/sa-token-demo/sa-token-demo-remember-me/page_project/index.html b/sa-token-demo/sa-token-demo-remember-me/page_project/index.html new file mode 100644 index 000000000..6bb3cf29f --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/page_project/index.html @@ -0,0 +1,12 @@ + + + + + + 记住我模式Demo页面 + + +
+ + + diff --git a/sa-token-demo/sa-token-demo-remember-me/page_project/package.json b/sa-token-demo/sa-token-demo-remember-me/page_project/package.json new file mode 100644 index 000000000..aab213337 --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/page_project/package.json @@ -0,0 +1,22 @@ +{ + "name": "page_project", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.3.4", + "element-plus": "^2.2.33", + "qs": "^6.11.0", + "vue": "^3.2.45", + "vue-axios": "^3.5.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.0.0", + "vite": "^4.1.0" + } +} diff --git a/sa-token-demo/sa-token-demo-remember-me/page_project/src/App.vue b/sa-token-demo/sa-token-demo-remember-me/page_project/src/App.vue new file mode 100644 index 000000000..a39e2dd05 --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/page_project/src/App.vue @@ -0,0 +1,160 @@ + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-remember-me/page_project/src/main.js b/sa-token-demo/sa-token-demo-remember-me/page_project/src/main.js new file mode 100644 index 000000000..6f63545ff --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/page_project/src/main.js @@ -0,0 +1,20 @@ +import { createApp } from 'vue' +import App from './App.vue' +import axios from 'axios' // 请求发送接收工具 +import VueAxios from 'vue-axios' // vue封装axios +import qs from 'qs' // axios请求参数类型封装 +import ElementPlus from 'element-plus' // elementUI for vue3 +import 'element-plus/dist/index.css' // 加载elementUI样式 +import zhCn from 'element-plus/es/locale/lang/zh-cn' // 引入中文本地化组件 + + +const app = createApp(App) + +// vue组件内通过 this.$f() 来调用 +app.config.globalProperties.$f = (params) => { + return qs.stringify(params) +} + +app.use(VueAxios, axios) +.use(ElementPlus, { locale: zhCn }) +.mount('#app') diff --git a/sa-token-demo/sa-token-demo-remember-me/page_project/vite.config.js b/sa-token-demo/sa-token-demo-remember-me/page_project/vite.config.js new file mode 100644 index 000000000..f4ad736d3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/page_project/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// 开启代理服务 +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + server: { + port: 5173, + host: true, + proxy: { + '^/back/.*$': { + target: 'http://localhost:80' + } + } + } +}) 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 new file mode 100644 index 000000000..0a5ddca6a --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + cn.dev33 + sa-token-demo-remember-me-server + 1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + 1.45.0 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + org.apache.commons + commons-pool2 + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/RememberMeApplication.java b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/RememberMeApplication.java new file mode 100644 index 000000000..ab7757bc9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/RememberMeApplication.java @@ -0,0 +1,11 @@ +package cc.sa_token; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class RememberMeApplication { + public static void main(String[] args) { + SpringApplication.run(RememberMeApplication.class, args); + } +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/controller/UserLoginController.java b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/controller/UserLoginController.java new file mode 100644 index 000000000..fcfd7fe15 --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/controller/UserLoginController.java @@ -0,0 +1,37 @@ +package cc.sa_token.controller; + +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/back/user") +public class UserLoginController { + + @RequestMapping("/login") + public SaResult doLogin(String name, String pwd, Boolean remember) { + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001, remember); + SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); + return SaResult.ok() + .set("tokenName", tokenInfo.getTokenName()) + .set("tokenValue", tokenInfo.getTokenValue()); + } else { + return SaResult.error("登录失败"); + } + } + + @RequestMapping("/state") + public SaResult checkNowLoginState() { + return SaResult.ok().setData(StpUtil.isLogin()); + } + + @RequestMapping("/logout") + public SaResult doLogout() { + StpUtil.logout(); + return SaResult.ok().setData(StpUtil.isLogin()); + } + +} diff --git a/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/resources/application.yml new file mode 100644 index 000000000..98937634e --- /dev/null +++ b/sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 80 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-solon-redisson/pom.xml b/sa-token-demo/sa-token-demo-solon-redisson/pom.xml new file mode 100644 index 000000000..ec3a52710 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-solon-redisson + 0.0.1-SNAPSHOT + + + + org.noear + solon-parent + 3.2.1 + + + + + + 1.45.0 + UTF-8 + UTF-8 + + + + + + + org.noear + solon-web + + + + org.noear + solon.logging.simple + + + + org.noear + redisson-solon-plugin + + + + + cn.dev33 + sa-token-solon-plugin + ${sa-token.version} + + + + + cn.dev33 + sa-token-redisson + ${sa-token.version} + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + -parameters + 1.8 + 1.8 + UTF-8 + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/SaTokenDemoApp.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/SaTokenDemoApp.java new file mode 100644 index 000000000..8bc72dfa2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/SaTokenDemoApp.java @@ -0,0 +1,20 @@ +package com.pj; + + +import cn.dev33.satoken.SaManager; +import org.noear.solon.Solon; +import org.noear.solon.annotation.SolonMain; + +/** + * sa-token整合 solon 示例 + * @author noear + * + */ +@SolonMain +public class SaTokenDemoApp { + + public static void main(String[] args) { + Solon.start(SaTokenDemoApp.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSlf4j.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSlf4j.java new file mode 100644 index 000000000..41f47c65d --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSlf4j.java @@ -0,0 +1,51 @@ +package com.pj.satoken; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.log.SaLog; +import cn.dev33.satoken.log.SaLogForConsole; +import cn.dev33.satoken.util.StrFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 将 Sa-Token log 信息转接到 slf4j 接口 + * + * @author noear 2022/11/14 created + */ +//@Component +public class SaLogForSlf4j extends SaLogForConsole implements SaLog { + static final Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); + + /** + * 打印日志到控制台 + * + * @param level 日志等级 + * @param str 字符串 + * @param args 参数列表 + */ + public void println(int level, String str, Object... args) { + SaTokenConfig config = SaManager.getConfig(); + + if (config.getIsLog() && level >= config.getLogLevelInt()) { + switch (level) { + case trace: + log.trace(LOG_PREFIX + StrFormatter.format(str, args)); + break; + case debug: + log.debug(LOG_PREFIX + StrFormatter.format(str, args)); + break; + case info: + log.info(LOG_PREFIX + StrFormatter.format(str, args)); + break; + case warn: + log.warn(LOG_PREFIX + StrFormatter.format(str, args)); + break; + case error: + case fatal: + log.error(LOG_PREFIX + StrFormatter.format(str, args)); + break; + } + } + } +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSolon.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSolon.java new file mode 100644 index 000000000..01ee2354c --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSolon.java @@ -0,0 +1,51 @@ +package com.pj.satoken; + +import org.noear.solon.core.util.LogUtil; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.log.SaLog; +import cn.dev33.satoken.log.SaLogForConsole; +import cn.dev33.satoken.util.StrFormatter; + +/** + * 将 Sa-Token log 信息转接到 Solon + * + * @author click33 + * @since 2022-11-2 + */ +//@Component +public class SaLogForSolon extends SaLogForConsole implements SaLog { + + /** + * 打印日志到控制台 + * + * @param level 日志等级 + * @param str 字符串 + * @param args 参数列表 + */ + public void println(int level, String str, Object... args) { + SaTokenConfig config = SaManager.getConfig(); + + if (config.getIsLog() && level >= config.getLogLevelInt()) { + switch (level) { + case trace: + LogUtil.global().trace(LOG_PREFIX + StrFormatter.format(str, args)); + break; + case debug: + LogUtil.global().debug(LOG_PREFIX + StrFormatter.format(str, args)); + break; + case info: + LogUtil.global().info(LOG_PREFIX + StrFormatter.format(str, args)); + break; + case warn: + LogUtil.global().warn(LOG_PREFIX + StrFormatter.format(str, args)); + break; + case error: + case fatal: + LogUtil.global().error(LOG_PREFIX + StrFormatter.format(str, args)); + break; + } + } + } +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..fe686cf08 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,78 @@ +package com.pj.satoken; + + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisson; +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 org.noear.solon.annotation.Inject; +import org.redisson.api.RedissonClient; +import org.redisson.solon.RedissonSupplier; + + +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + * @author noear + */ +@Configuration +public class SaTokenConfigure { + + /** + * 注册 [sa-token全局过滤器] + */ + @Bean(index = -100) + public SaTokenInterceptor tokenPathFilter() { + return new SaTokenInterceptor() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**").addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(r -> { + // System.out.println("---------- sa全局认证"); + + // SaRouter.match("/test/test", () -> new Object()); + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + return AjaxJson.getError(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) + .setBeforeAuth(r -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-Frame-Options", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + ; + }); + } + + /** + * 构造 RedissonClient + * */ + @Bean + public RedissonClient saTokenDaoInit(@Inject("${sa-token-dao}") RedissonSupplier supplier) { + return supplier.get(); + } + + /** + * 构建 SaTokenDao + * */ + @Bean + public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) { + return new SaTokenDaoForRedisson(redissonClient); + } +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..5833b67d5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + + +import cn.dev33.satoken.stp.StpInterface; +import org.noear.solon.annotation.Component; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被 solon 扫描,即可完成 sa-token 的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/GlobalExceptionFilter.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/GlobalExceptionFilter.java new file mode 100644 index 000000000..1daeec163 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/GlobalExceptionFilter.java @@ -0,0 +1,46 @@ +package com.pj.test; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.exception.*; + +import org.noear.solon.annotation.Component; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Filter; +import org.noear.solon.core.handle.FilterChain; + + +/** + * 全局异常处理 + * + * @author noear + */ +@Component +public class GlobalExceptionFilter implements Filter { + @Override + public void doFilter(Context ctx, FilterChain chain) throws Throwable { + try { + chain.doFilter(ctx); + } catch (SaTokenException e) { + // 不同异常返回不同状态码 + AjaxJson aj = null; + if (e instanceof NotLoginException) { // 如果是未登录异常 + NotLoginException ee = (NotLoginException) e; + aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); + } else if (e instanceof NotRoleException) { // 如果是角色异常 + NotRoleException ee = (NotRoleException) e; + aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); + } else if (e instanceof NotPermissionException) { // 如果是权限异常 + NotPermissionException ee = (NotPermissionException) e; + aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); + } else if (e instanceof DisableServiceException) { // 如果是被封禁异常 + DisableServiceException ee = (DisableServiceException) e; + aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封"); + } else { // 普通异常, 输出:500 + 异常信息 + aj = AjaxJson.getError(e.getMessage()); + } + + ctx.render(aj); + } + } +} diff --git a/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/SSOController.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/SSOController.java similarity index 97% rename from sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/SSOController.java rename to sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/SSOController.java index 7bf77a2fb..8cc552870 100644 --- a/sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/SSOController.java +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/SSOController.java @@ -10,7 +10,7 @@ /** * 测试: 同域单点登录 - * @author kong + * @author click33 * @author noear */ @Controller diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/StressTestController.java new file mode 100644 index 000000000..cb2038121 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/StressTestController.java @@ -0,0 +1,61 @@ +package com.pj.test; + +import java.util.ArrayList; +import java.util.List; + +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; + +import com.pj.util.AjaxJson; +import com.pj.util.Ttime; + +import cn.dev33.satoken.stp.StpUtil; + +/** + * 压力测试 + * @author click33 + * @author noear + */ +@Controller +@Mapping("/s-test/") +public class StressTestController { + + + // 测试 浏览器访问: http://localhost:8081/s-test/login + // 测试前,请先将 is-read-cookie 配置为 false + @Mapping("login") + public AjaxJson login() { +// StpUtil.getTokenSession().logout(); +// StpUtil.logoutByLoginId(10001); + + int count = 10; // 循环多少轮 + int loginCount = 10000; // 每轮循环多少次 + + // 循环10次 取平均时间 + List list = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + System.out.println("\n---------------------第" + i + "轮---------------------"); + Ttime t = new Ttime().start(); + // 每次登录的次数 + for (int j = 1; j <= loginCount; j++) { + StpUtil.login("1000" + j, "PC-" + j); + if(j % 1000 == 0) { + System.out.println("已登录:" + j); + } + } + t.end(); + list.add((t.returnMs() + 0.0) / 1000); + System.out.println("第" + i + "轮" + "用时:" + t.toString()); + } +// System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); + + System.out.println("\n---------------------测试结果---------------------"); + System.out.println(list.size() + "次测试: " + list); + double ss = 0; + for (int i = 0; i < list.size(); i++) { + ss += list.get(i); + } + System.out.println("平均用时: " + ss / list.size()); + return AjaxJson.getSuccess(); + } +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..30f066945 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,252 @@ +package com.pj.test; + +import java.util.Date; +import java.util.List; + +import com.pj.util.AjaxJson; +import com.pj.util.Ttime; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; +import cn.dev33.satoken.session.SaSessionCustomUtil; +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpUtil; +import org.noear.snack.ONode; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; +import org.noear.solon.annotation.Param; + +/** + * 测试专用Controller + * @author click33 + * @author noear + */ +@Controller +@Mapping("/test/") +public class TestController { + + + // 测试登录接口, 浏览器访问: http://localhost:8081/test/login + @Mapping("login") + public AjaxJson login(@Param(defaultValue="10001") String id) { + System.out.println("======================= 进入方法,测试登录接口 ========================= "); + System.out.println("当前会话的token:" + StpUtil.getTokenValue()); + System.out.println("当前是否登录:" + StpUtil.isLogin()); + System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull()); + + StpUtil.login(id); // 在当前会话登录此账号 + System.out.println("登录成功"); + System.out.println("当前是否登录:" + StpUtil.isLogin()); + System.out.println("当前登录账号:" + StpUtil.getLoginId()); +// System.out.println("当前登录账号并转为int:" + StpUtil.getLoginIdAsInt()); + System.out.println("当前登录设备:" + StpUtil.getLoginDevice()); +// System.out.println("当前token信息:" + StpUtil.getTokenInfo()); + + return AjaxJson.getSuccess(); + } + + // 测试退出登录 , 浏览器访问: http://localhost:8081/test/logout + @Mapping("logout") + public AjaxJson logout() { + StpUtil.logout(); +// StpUtil.logoutByLoginId(10001); + return AjaxJson.getSuccess(); + } + + // 测试角色接口, 浏览器访问: http://localhost:8081/test/testRole + @Mapping("testRole") + public AjaxJson testRole() { + System.out.println("======================= 进入方法,测试角色接口 ========================= "); + + System.out.println("是否具有角色标识 user " + StpUtil.hasRole("user")); + System.out.println("是否具有角色标识 admin " + StpUtil.hasRole("admin")); + + System.out.println("没有admin权限就抛出异常"); + StpUtil.checkRole("admin"); + + System.out.println("在【admin、user】中只要拥有一个就不会抛出异常"); + StpUtil.checkRoleOr("admin", "user"); + + System.out.println("在【admin、user】中必须全部拥有才不会抛出异常"); + StpUtil.checkRoleAnd("admin", "user"); + + System.out.println("角色测试通过"); + + return AjaxJson.getSuccess(); + } + + // 测试权限接口, 浏览器访问: http://localhost:8081/test/testJur + @Mapping("testJur") + public AjaxJson testJur() { + System.out.println("======================= 进入方法,测试权限接口 ========================= "); + + System.out.println("是否具有权限101" + StpUtil.hasPermission("101")); + System.out.println("是否具有权限user-add" + StpUtil.hasPermission("user-add")); + System.out.println("是否具有权限article-get" + StpUtil.hasPermission("article-get")); + + System.out.println("没有user-add权限就抛出异常"); + StpUtil.checkPermission("user-add"); + + System.out.println("在【101、102】中只要拥有一个就不会抛出异常"); + StpUtil.checkPermissionOr("101", "102"); + + System.out.println("在【101、102】中必须全部拥有才不会抛出异常"); + StpUtil.checkPermissionAnd("101", "102"); + + System.out.println("权限测试通过"); + + return AjaxJson.getSuccess(); + } + + // 测试会话session接口, 浏览器访问: http://localhost:8081/test/session + @Mapping("session") + public AjaxJson session() { + System.out.println("======================= 进入方法,测试会话session接口 ========================= "); + System.out.println("当前是否登录:" + StpUtil.isLogin()); + System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); + System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); + System.out.println("测试取值name:" + StpUtil.getSession().get("name")); + StpUtil.getSession().set("name", new Date()); // 写入一个值 + System.out.println("测试取值name:" + StpUtil.getSession().get("name")); + System.out.println( ONode.stringify(StpUtil.getSession())); + return AjaxJson.getSuccess(); + } + + // 测试自定义session接口, 浏览器访问: http://localhost:8081/test/session2 + @Mapping("session2") + public AjaxJson session2() { + System.out.println("======================= 进入方法,测试自定义session接口 ========================= "); + // 自定义session就是无需登录也可以使用 的session :比如拿用户的手机号当做 key, 来获取 session + System.out.println("自定义 session的id为:" + SaSessionCustomUtil.getSessionById("1895544896").getId()); + System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); + SaSessionCustomUtil.getSessionById("1895544896").set("name", "张三"); // 写入值 + System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); + System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); + return AjaxJson.getSuccess(); + } + + // ---------- + // 测试token专属session, 浏览器访问: http://localhost:8081/test/getTokenSession + @Mapping("getTokenSession") + public AjaxJson getTokenSession() { + System.out.println("======================= 进入方法,测试会话session接口 ========================= "); + System.out.println("当前是否登录:" + StpUtil.isLogin()); + System.out.println("当前token专属session: " + StpUtil.getTokenSession().getId()); + + System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name")); + StpUtil.getTokenSession().set("name", "张三"); // 写入一个值 + System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name")); + + return AjaxJson.getSuccess(); + } + + // 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo + @Mapping("tokenInfo") + public AjaxJson tokenInfo() { + System.out.println("======================= 进入方法,打印当前token信息 ========================= "); + SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); + System.out.println(tokenInfo); + return AjaxJson.getSuccessData(tokenInfo); + } + + // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atCheck + @SaCheckLogin // 注解式鉴权:当前会话必须登录才能通过 + @SaCheckRole("super-admin") // 注解式鉴权:当前会话必须具有指定角色标识才能通过 + @SaCheckPermission("user-add") // 注解式鉴权:当前会话必须具有指定权限才能通过 + @Mapping("atCheck") + public AjaxJson atCheck() { + System.out.println("======================= 进入方法,测试注解鉴权接口 ========================= "); + System.out.println("只有通过注解鉴权,才能进入此方法"); +// StpUtil.checkActiveTimeout(); +// StpUtil.updateLastActiveToNow(); + return AjaxJson.getSuccess(); + } + + // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atJurOr + @Mapping("atJurOr") + @SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) // 注解式鉴权:只要具有其中一个权限即可通过校验 + public AjaxJson atJurOr() { + return AjaxJson.getSuccessData("用户信息"); + } + + // [活动时间] 续签: http://localhost:8081/test/rene + @Mapping("rene") + public AjaxJson rene() { + StpUtil.checkActiveTimeout(); + StpUtil.updateLastActiveToNow(); + return AjaxJson.getSuccess("续签成功"); + } + + // 测试踢人下线 浏览器访问: http://localhost:8081/test/kickOut + @Mapping("kickOut") + public AjaxJson kickOut() { + // 先登录上 + StpUtil.login(10001); + // 踢下线 + StpUtil.kickout(10001); + // 再尝试获取 + StpUtil.getLoginId(); + // 返回 + return AjaxJson.getSuccess(); + } + + // 测试登录接口, 按照设备类型登录, 浏览器访问: http://localhost:8081/test/login2 + @Mapping("login2") + public AjaxJson login2(@Param(defaultValue="10001") String id, @Param(defaultValue="PC") String device) { + StpUtil.login(id, device); + return AjaxJson.getSuccess(); + } + + // 测试身份临时切换: http://localhost:8081/test/switchTo + @Mapping("switchTo") + public AjaxJson switchTo() { + System.out.println("当前会话身份:" + StpUtil.getLoginIdDefaultNull()); + System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); + StpUtil.switchTo(10044, () -> { + System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); + System.out.println("当前会话身份已被切换为:" + StpUtil.getLoginId()); + }); + System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); + return AjaxJson.getSuccess(); + } + + // 测试会话治理 浏览器访问: http://localhost:8081/test/search + @Mapping("search") + public AjaxJson search() { + System.out.println("--------------"); + Ttime t = new Ttime().start(); + List tokenValue = StpUtil.searchTokenValue("8feb8265f773", 0, 10, true); + for (String v : tokenValue) { +// SaSession session = StpUtil.getSessionBySessionId(sid); + System.out.println(v); + } + System.out.println("用时:" + t.end().toString()); + return AjaxJson.getSuccess(); + } + + // 测试指定设备类型登录 浏览器访问: http://localhost:8081/test/loginByDevice + @Mapping("loginByDevice") + public AjaxJson loginByDevice() { + System.out.println("--------------"); + StpUtil.login(10001, "PC"); + return AjaxJson.getSuccessData("登录成功"); + } + + + // 测试 浏览器访问: http://localhost:8081/test/test + @Mapping("test") + public AjaxJson test() { + System.out.println("进来了"); + return AjaxJson.getSuccess("访问成功"); + } + + // 测试 浏览器访问: http://localhost:8081/test/test2 + @Mapping("test2") + public AjaxJson test2() { + return AjaxJson.getSuccess(); + } + + +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/UserController.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/UserController.java new file mode 100644 index 000000000..939d5c4d2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/UserController.java @@ -0,0 +1,33 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; + +/** + * 登录测试 + * @author click33 + * @author noear + */ +@Controller +@Mapping("/user/") +public class UserController { + + // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 + @Mapping("doLogin") + public String doLogin(String username, String password) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(username) && "123456".equals(password)) { + StpUtil.login(10001); + return "登录成功"; + } + return "登录失败"; + } + + // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin + @Mapping("isLogin") + public String isLogin(String username, String password) { + return "当前会话是否登录:" + StpUtil.isLogin(); + } + +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/AjaxJson.java new file mode 100644 index 000000000..768d05784 --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.util; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/Ttime.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/Ttime.java new file mode 100644 index 000000000..dec4f1cbe --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/Ttime.java @@ -0,0 +1,63 @@ +package com.pj.util; + + +/** + * 用于测试用时 + * @author click33 + * + */ +public class Ttime { + + private long start=0; //开始时间 + private long end=0; //结束时间 + + public static Ttime t = new Ttime(); //static快捷使用 + + /** + * 开始计时 + * @return + */ + public Ttime start() { + start=System.currentTimeMillis(); + return this; + } + + + /** + * 结束计时 + */ + public Ttime end() { + end=System.currentTimeMillis(); + return this; + } + + + /** + * 返回所用毫秒数 + */ + public long returnMs() { + return end-start; + } + + /** + * 格式化输出结果 + */ + public void outTime() { + System.out.println(this.toString()); + } + + /** + * 结束并格式化输出结果 + */ + public void endOutTime() { + this.end().outTime(); + } + + @Override + public String toString() { + return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s + } + + + +} diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/resources/app.yml b/sa-token-demo/sa-token-demo-solon-redisson/src/main/resources/app.yml new file mode 100644 index 000000000..f954a632d --- /dev/null +++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/resources/app.yml @@ -0,0 +1,32 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + + + +sa-token-dao: + config: | + singleServerConfig: + password: "123456" + address: "redis://localhost:6379" + database: 0 + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-solon/pom.xml b/sa-token-demo/sa-token-demo-solon/pom.xml index 41f39ab6d..377986610 100644 --- a/sa-token-demo/sa-token-demo-solon/pom.xml +++ b/sa-token-demo/sa-token-demo-solon/pom.xml @@ -6,54 +6,57 @@ sa-token-demo-solon 0.0.1-SNAPSHOT - - + + + org.noear + solon-parent + 3.2.1 + + + + - 1.24.0 + 17 + 17 + 17 + 1.45.0 + UTF-8 + UTF-8 - + org.noear solon-web - 1.5.1 + + + + org.noear + solon.logging.simple - + cn.dev33 sa-token-solon-plugin - ${sa-token-version} + ${sa-token.version} - + - - - - - - - - - - + sa-token-redisx + ${sa-token.version} + + + + + cn.dev33 + sa-token-snack3 + ${sa-token.version} + + + org.springframework.boot + spring-boot-starter-parent + + + + 2.1.18.RELEASE + + + + + + 1.45.0 + com.pj.SaTokenApplication + 1.8 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + cn.hutool + hutool-all + 5.8.36 + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + com.fasterxml.jackson.core + jackson-core + 2.17.3 + + + com.fasterxml.jackson.core + jackson-annotations + 2.17.3 + + + com.fasterxml.jackson.core + jackson-databind + 2.17.3 + + + + + + + + + + + + + + src/main/java + + **/*.xml + + + + src/main/resources + + **/*.* + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + lib/ + ${java.run.main.class} + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + package + + copy-dependencies + + + + ${project.build.directory}/lib + + + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/SaTokenApplication.java b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/SaTokenApplication.java new file mode 100644 index 000000000..a6ddba2e3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/SaTokenApplication.java @@ -0,0 +1,22 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +/** + * Sa-Token 测试 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); +// System.out.println(StpUtil.getSessionByLoginId(10001)); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..649bdf06b --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,28 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 返回给前端 + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..2ad7f7682 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,87 @@ +package com.pj.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // 输出 API 请求日志,方便调试代码 + // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行 + .setBeforeAuth(obj -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + + // ---------- 设置跨域响应头 ---------- + // 允许指定域访问跨域资源 + .setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..bf7650ec3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,71 @@ +package com.pj.test; + +import cn.dev33.satoken.session.SaTerminalInfo; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + StpUtil.getTokenSession(); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 校验登录 ---- http://localhost:8081/acc/checkLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + StpUtil.checkLogin(); + return SaResult.ok(); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 查询账号登录设备信息 ---- http://localhost:8081/acc/terminalInfo + @RequestMapping("terminalInfo") + public SaResult terminalInfo() { + System.out.println("账号 10001 登录设备信息:"); + List terminalList = StpUtil.getTerminalListByLoginId(10001); + for (SaTerminalInfo ter : terminalList) { + System.out.println("登录index=" + ter.getIndex() + ", 设备type=" + ter.getDeviceType() + ", token=" + ter.getTokenValue() + ", 登录time=" + ter.getCreateTime()); + } + return SaResult.data(terminalList); + } + + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.login(10001, SaLoginParameter.create().setIsConcurrent(false)); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-low-version/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/resources/application.yml new file mode 100644 index 000000000..95e98258f --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-low-version/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot-redis/pom.xml b/sa-token-demo/sa-token-demo-springboot-redis/pom.xml new file mode 100644 index 000000000..ad232aa60 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/pom.xml @@ -0,0 +1,72 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-springboot-redis + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/SaTokenDemoApplication.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/SaTokenDemoApplication.java new file mode 100644 index 000000000..094163242 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/SaTokenDemoApplication.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token整合SpringBoot 示例,整合redis + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenDemoApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..1d6ebb1cd --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,57 @@ +package com.pj.current; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.exception.DisableServiceException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 不同异常返回不同状态码 + AjaxJson aj = null; + if (e instanceof NotLoginException) { // 如果是未登录异常 + NotLoginException ee = (NotLoginException) e; + aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); + } + else if(e instanceof NotRoleException) { // 如果是角色异常 + NotRoleException ee = (NotRoleException) e; + aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); + } + else if(e instanceof NotPermissionException) { // 如果是权限异常 + NotPermissionException ee = (NotPermissionException) e; + aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); + } + else if(e instanceof DisableServiceException) { // 如果是被封禁异常 + DisableServiceException ee = (DisableServiceException) e; + aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); + } + else { // 普通异常, 输出:500 + 异常信息 + aj = AjaxJson.getError(e.getMessage()); + } + + // 返回给前端 + return aj; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..81ff22e8e --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,27 @@ +package com.pj.current; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 处理 404 + * @author click33 + */ +@RestController +public class NotFoundHandle implements ErrorController { + + @RequestMapping("/error") + public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setStatus(200); + return SaResult.get(404, "not found", null); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..9dd25cd95 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,69 @@ +package com.pj.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行 (BeforeAuth不受 includeList 与 excludeList 的限制,所有请求都会进入) + .setBeforeAuth(r -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + ; + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..b6cc79f71 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/AtController.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/AtController.java new file mode 100644 index 000000000..092381e04 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/AtController.java @@ -0,0 +1,80 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.annotation.SaCheckHttpBasic; +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; + +/** + * 注解鉴权测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/at/") +public class AtController { + + // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin + @SaCheckLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + return SaResult.ok(); + } + + // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission + @SaCheckPermission("user-add") + @RequestMapping("checkPermission") + public SaResult checkPermission() { + return SaResult.ok(); + } + + // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd + @SaCheckPermission({"user-add", "user-delete", "user-update"}) + @RequestMapping("checkPermissionAnd") + public SaResult checkPermissionAnd() { + return SaResult.ok(); + } + + // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr + @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) + @RequestMapping("checkPermissionOr") + public SaResult checkPermissionOr() { + return SaResult.ok(); + } + + // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole + @SaCheckRole("admin") + @RequestMapping("checkRole") + public SaResult checkRole() { + return SaResult.ok(); + } + + // 完成二级认证 ---- http://localhost:8081/at/openSafe + @RequestMapping("openSafe") + public SaResult openSafe() { + StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 + return SaResult.ok(); + } + + // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe + @SaCheckSafe + @RequestMapping("checkSafe") + public SaResult checkSafe() { + return SaResult.ok(); + } + + // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic + @SaCheckHttpBasic(account = "sa:123456") + @RequestMapping("checkBasic") + public SaResult checkBasic() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..34eea9a19 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/StressTestController.java new file mode 100644 index 000000000..b42731093 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/StressTestController.java @@ -0,0 +1,61 @@ +package com.pj.test; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.pj.util.Ttime; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 压力测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/s-test/") +public class StressTestController { + + // 测试 浏览器访问: http://localhost:8081/s-test/login + // 测试前,请先将 is-read-cookie 配置为 false + @RequestMapping("login") + public SaResult login() { +// StpUtil.getTokenSession().logout(); +// StpUtil.logoutByLoginId(10001); + + int count = 10; // 循环多少轮 + int loginCount = 10000; // 每轮循环多少次 + + // 循环10次 取平均时间 + List list = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + System.out.println("\n---------------------第" + i + "轮---------------------"); + Ttime t = new Ttime().start(); + // 每次登录的次数 + for (int j = 1; j <= loginCount; j++) { + StpUtil.login("1000" + j, "PC-" + j); + if(j % 1000 == 0) { + System.out.println("已登录:" + j); + } + } + t.end(); + list.add((t.returnMs() + 0.0) / 1000); + System.out.println("第" + i + "轮" + "用时:" + t.toString()); + } +// System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); + + System.out.println("\n---------------------测试结果---------------------"); + System.out.println(list.size() + "次测试: " + list); + double ss = 0; + for (int i = 0; i < list.size(); i++) { + ss += list.get(i); + } + System.out.println("平均用时: " + ss / list.size()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..9838063e8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,30 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + // 测试 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("------------进来了"); + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8081/test/test2 + @RequestMapping("test2") + public SaResult test2() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/AjaxJson.java new file mode 100644 index 000000000..768d05784 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.util; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/Ttime.java b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/Ttime.java new file mode 100644 index 000000000..dec4f1cbe --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/Ttime.java @@ -0,0 +1,63 @@ +package com.pj.util; + + +/** + * 用于测试用时 + * @author click33 + * + */ +public class Ttime { + + private long start=0; //开始时间 + private long end=0; //结束时间 + + public static Ttime t = new Ttime(); //static快捷使用 + + /** + * 开始计时 + * @return + */ + public Ttime start() { + start=System.currentTimeMillis(); + return this; + } + + + /** + * 结束计时 + */ + public Ttime end() { + end=System.currentTimeMillis(); + return this; + } + + + /** + * 返回所用毫秒数 + */ + public long returnMs() { + return end-start; + } + + /** + * 格式化输出结果 + */ + public void outTime() { + System.out.println(this.toString()); + } + + /** + * 结束并格式化输出结果 + */ + public void endOutTime() { + this.end().outTime(); + } + + @Override + public String toString() { + return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s + } + + + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redis/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-springboot-redis/src/main/resources/application.yml new file mode 100644 index 000000000..1889895e5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redis/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/pom.xml b/sa-token-demo/sa-token-demo-springboot-redisson/pom.xml new file mode 100644 index 000000000..092e53da0 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redisson/pom.xml @@ -0,0 +1,59 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-springboot-redisson + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redisson-spring-boot-starter + ${sa-token.version} + + + + io.netty + netty-transport-native-unix-common + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/SaTokenDemoApplication.java b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/SaTokenDemoApplication.java new file mode 100644 index 000000000..e9357ae48 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/SaTokenDemoApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +/** + * Sa-Token 整合 Redisson 示例 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenDemoApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..346289a2b --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,52 @@ +package com.pj.current; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +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.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import cn.dev33.satoken.util.SaResult; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 不同异常返回不同状态码 + if (e instanceof NotLoginException) { // 如果是未登录异常 + NotLoginException ee = (NotLoginException) e; + return SaResult.notLogin().setMsg(ee.getMessage()); + } + else if(e instanceof NotRoleException) { // 如果是角色异常 + NotRoleException ee = (NotRoleException) e; + return SaResult.notPermission().setMsg("无此角色:" + ee.getRole()); + } + else if(e instanceof NotPermissionException) { // 如果是权限异常 + NotPermissionException ee = (NotPermissionException) e; + return SaResult.notPermission().setMsg("无此权限:" + ee.getPermission()); + } + else if(e instanceof DisableServiceException) { // 如果是被封禁异常 + DisableServiceException ee = (DisableServiceException) e; + return SaResult.error("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); + } + else { // 普通异常, 输出:500 + 异常信息 + return SaResult.error(e.getMessage()); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonConfig.java b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonConfig.java new file mode 100644 index 000000000..3e71f5b43 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonConfig.java @@ -0,0 +1,50 @@ +//package com.pj.redisson; +// +//import org.redisson.config.SingleServerConfig; +//import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.context.properties.EnableConfigurationProperties; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +// +///** +// * redisson 配置 +// * +// * @author 疯狂的狮子Li +// */ +//@Configuration +//@EnableConfigurationProperties(RedissonProperties.class) +//public class RedissonConfig { +// +// @Autowired +// private RedissonProperties redissonProperties; +// +// /** +// * 自定义Redisson配置注入器 被RedissonAutoConfiguration调用执行 +// * 具体参考 {@link org.redisson.spring.starter.RedissonAutoConfiguration} +// *

+// * 使用自定义配置类手动注入配置数据 +// * 也可根据redisson官网使用properties文件配置 +// */ +// @Bean +// public RedissonAutoConfigurationCustomizer redissonCustomizer() { +// return config -> { +// config.setThreads(redissonProperties.getThreads()); +// config.setNettyThreads(redissonProperties.getNettyThreads()); +// SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig(); +// if (singleServerConfig != null) { +// // 使用单机模式 +// config.useSingleServer() +// .setTimeout(singleServerConfig.getTimeout()) +// .setClientName(singleServerConfig.getClientName()) +// .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout()) +// .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize()) +// .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize()) +// .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize()); +// } +// +// }; +// } +// +// +//} diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonProperties.java b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonProperties.java new file mode 100644 index 000000000..6d02c9b03 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonProperties.java @@ -0,0 +1,56 @@ +//package com.pj.redisson; +// +//import org.redisson.config.SingleServerConfig; +//import org.springframework.boot.context.properties.ConfigurationProperties; +//import org.springframework.boot.context.properties.NestedConfigurationProperty; +//import org.springframework.stereotype.Component; +// +///** +// * Redisson 配置属性 +// * +// * @author 疯狂的狮子Li +// */ +//@Component +//@ConfigurationProperties(prefix = "redisson") +//public class RedissonProperties { +// +// /** +// * 线程池数量,默认值 = 当前处理核数量 * 2 +// */ +// private int threads; +// +// /** +// * Netty线程池数量,默认值 = 当前处理核数量 * 2 +// */ +// private int nettyThreads; +// +// /** +// * 单机服务配置 +// */ +// @NestedConfigurationProperty +// private SingleServerConfig singleServerConfig; +// +// public int getThreads() { +// return threads; +// } +// +// public void setThreads(int threads) { +// this.threads = threads; +// } +// +// public int getNettyThreads() { +// return nettyThreads; +// } +// +// public void setNettyThreads(int nettyThreads) { +// this.nettyThreads = nettyThreads; +// } +// +// public SingleServerConfig getSingleServerConfig() { +// return singleServerConfig; +// } +// +// public void setSingleServerConfig(SingleServerConfig singleServerConfig) { +// this.singleServerConfig = singleServerConfig; +// } +//} diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..dc3b4bc55 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,57 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 读写 SaSession ---- http://localhost:8081/acc/session + @RequestMapping("session") + public SaResult session() { + System.out.println(StpUtil.getSession().get("name")); + StpUtil.getSession().set("name", "zhangsan"); + System.out.println(StpUtil.getSession().get("name")); + return SaResult.ok("Session 写入成功"); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot-redisson/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/resources/application.yml new file mode 100644 index 000000000..02d2ec815 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot-redisson/src/main/resources/application.yml @@ -0,0 +1,58 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 1 + # Redis服务器地址 + host: localhost + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码 为空需注释掉 + # password: + # 连接超时时间 + timeout: 10s + +#redisson: +# # 线程池数量 +# threads: 8 +# # Netty线程池数量 +# nettyThreads: 32 +# # 单节点配置 +# singleServerConfig: +# # 客户端名称 +# clientName: test +# # 最小空闲连接数 +# connectionMinimumIdleSize: 8 +# # 连接池大小 +# connectionPoolSize: 32 +# # 连接空闲超时,单位:毫秒 +# idleConnectionTimeout: 10000 +# # 命令等待超时,单位:毫秒 +# timeout: 3000 +# # 如果尝试在此限制之内发送成功,则开始启用 timeout 计时。 +# retryAttempts: 3 +# # 命令重试发送时间间隔,单位:毫秒 +# retryInterval: 1500 +# # 发布和订阅连接池大小 +# subscriptionConnectionPoolSize: 50 diff --git a/sa-token-demo/sa-token-demo-springboot/.gitignore b/sa-token-demo/sa-token-demo-springboot/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-springboot/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot/pom.xml b/sa-token-demo/sa-token-demo-springboot/pom.xml index 9bd1e4ef0..787ad0117 100644 --- a/sa-token-demo/sa-token-demo-springboot/pom.xml +++ b/sa-token-demo/sa-token-demo-springboot/pom.xml @@ -10,14 +10,14 @@ org.springframework.boot spring-boot-starter-parent - 2.0.0.RELEASE + 2.5.14 - + - 1.24.0 + 1.45.0 @@ -32,38 +32,18 @@ spring-boot-starter-aop - + cn.dev33 sa-token-spring-boot-starter - ${sa-token-version} + ${sa-token.version} - - - - - - - - - @@ -73,13 +53,6 @@ true - - - diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/SaTokenDemoApplication.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/SaTokenDemoApplication.java index 3da66eadf..195754ec8 100644 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/SaTokenDemoApplication.java +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/SaTokenDemoApplication.java @@ -7,14 +7,14 @@ /** * Sa-Token整合SpringBoot 示例 - * @author kong + * @author click33 * */ @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) { - SpringApplication.run(SaTokenDemoApplication.class, args); + SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..e0c9f42d8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,59 @@ +package com.pj.current; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.exception.DisableServiceException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; + +/** + * 全局异常处理 + */ +@ControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ResponseBody + @ExceptionHandler + public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 不同异常返回不同状态码 + AjaxJson aj = null; + if (e instanceof NotLoginException) { // 如果是未登录异常 + NotLoginException ee = (NotLoginException) e; + aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); + } + else if(e instanceof NotRoleException) { // 如果是角色异常 + NotRoleException ee = (NotRoleException) e; + aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); + } + else if(e instanceof NotPermissionException) { // 如果是权限异常 + NotPermissionException ee = (NotPermissionException) e; + aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); + } + else if(e instanceof DisableServiceException) { // 如果是被封禁异常 + DisableServiceException ee = (DisableServiceException) e; + aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封"); + } + else { // 普通异常, 输出:500 + 异常信息 + aj = AjaxJson.getError(e.getMessage()); + } + + // 返回给前端 + return aj; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..81ff22e8e --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,27 @@ +package com.pj.current; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 处理 404 + * @author click33 + */ +@RestController +public class NotFoundHandle implements ErrorController { + + @RequestMapping("/error") + public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setStatus(200); + return SaResult.get(404, "not found", null); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/SaTokenConfigure.java index b2360c402..8feeb06fa 100644 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/SaTokenConfigure.java +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -9,28 +9,28 @@ import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; -import cn.dev33.satoken.interceptor.SaAnnotationInterceptor; +import cn.dev33.satoken.interceptor.SaInterceptor; /** * [Sa-Token 权限认证] 配置类 - * @author kong + * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { - + /** - * 注册sa-token的拦截器,打开注解式鉴权功能 + * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { - // 注册注解拦截器 - registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**").excludePathPatterns(""); + // 注册 Sa-Token 拦截器打开注解鉴权功能 + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** - * 注册 [sa-token全局过滤器] + * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { @@ -40,10 +40,9 @@ public SaServletFilter getSaServletFilter() { .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 - .setAuth(r -> { - // System.out.println("---------- sa全局认证"); - - // SaRouter.match("/test/test", () -> new Object()); + .setAuth(obj -> { + // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + }) // 异常处理函数:每次认证函数发生异常时执行此函数 @@ -52,7 +51,7 @@ public SaServletFilter getSaServletFilter() { return AjaxJson.getError(e.getMessage()); }) - // 前置函数:在每次认证函数之前执行 + // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() @@ -69,5 +68,4 @@ public SaServletFilter getSaServletFilter() { ; } - } diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/MySaTokenAction.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/MySaTokenAction.java deleted file mode 100644 index 069abc81a..000000000 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/MySaTokenAction.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.pj.satoken.at; - -import java.lang.reflect.AnnotatedElement; - -import org.springframework.core.annotation.AnnotatedElementUtils; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.action.SaTokenActionDefaultImpl; -import cn.dev33.satoken.annotation.SaCheckLogin; -import cn.dev33.satoken.annotation.SaCheckPermission; -import cn.dev33.satoken.annotation.SaCheckRole; -import cn.dev33.satoken.annotation.SaCheckSafe; - -/** - * 继承Sa-Token行为Bean默认实现, 重写部分逻辑 - */ -//@Component -public class MySaTokenAction extends SaTokenActionDefaultImpl { - - /** - * 重写Sa-Token的注解处理器,加强注解合并功能 - * @param target see note - */ - @Override - protected void validateAnnotation(AnnotatedElement target) { - - // 校验 @SaCheckLogin 注解 - if(AnnotatedElementUtils.isAnnotated(target, SaCheckLogin.class)) { - SaCheckLogin at = AnnotatedElementUtils.getMergedAnnotation(target, SaCheckLogin.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckRole 注解 - if(AnnotatedElementUtils.isAnnotated(target, SaCheckRole.class)) { - SaCheckRole at = AnnotatedElementUtils.getMergedAnnotation(target, SaCheckRole.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckPermission 注解 - if(AnnotatedElementUtils.isAnnotated(target, SaCheckPermission.class)) { - SaCheckPermission at = AnnotatedElementUtils.getMergedAnnotation(target, SaCheckPermission.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckSafe 注解 - if(AnnotatedElementUtils.isAnnotated(target, SaCheckSafe.class)) { - SaCheckSafe at = AnnotatedElementUtils.getMergedAnnotation(target, SaCheckSafe.class); - SaManager.getStpLogic(null).checkByAnnotation(at); - } - } - -} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckLogin.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckLogin.java deleted file mode 100644 index 740148983..000000000 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckLogin.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.pj.satoken.at; - -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; - -/** - * 登录认证(User版):只有登录之后才能进入该方法 - *

可标注在函数、类上(效果等同于标注在此类的所有方法上) - * @author kong - * - */ -@SaCheckLogin(type = StpUserUtil.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.TYPE}) -public @interface SaUserCheckLogin { - -} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckPermission.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckPermission.java deleted file mode 100644 index 38312116f..000000000 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckPermission.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.pj.satoken.at; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.core.annotation.AliasFor; - -import cn.dev33.satoken.annotation.SaCheckPermission; -import cn.dev33.satoken.annotation.SaMode; - -/** - * 权限认证(User版):必须具有指定权限才能进入该方法 - *

可标注在函数、类上(效果等同于标注在此类的所有方法上) - * @author kong - * - */ -@SaCheckPermission(type = StpUserUtil.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.TYPE}) -public @interface SaUserCheckPermission { - - /** - * 需要校验的权限码 - * @return 需要校验的权限码 - */ - @AliasFor(annotation = SaCheckPermission.class) - String [] value() default {}; - - /** - * 验证模式:AND | OR,默认AND - * @return 验证模式 - */ - @AliasFor(annotation = SaCheckPermission.class) - SaMode mode() default SaMode.AND; - -} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckRole.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckRole.java deleted file mode 100644 index c9b44f324..000000000 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/SaUserCheckRole.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.pj.satoken.at; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.core.annotation.AliasFor; - -import cn.dev33.satoken.annotation.SaCheckRole; -import cn.dev33.satoken.annotation.SaMode; - -/** - * 角色认证(User版):必须具有指定角色标识才能进入该方法 - *

可标注在函数、类上(效果等同于标注在此类的所有方法上) - * @author kong - * - */ -@SaCheckRole(type = StpUserUtil.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.TYPE}) -public @interface SaUserCheckRole { - - /** - * 需要校验的角色标识 - * @return 需要校验的角色标识 - */ - @AliasFor(annotation = SaCheckRole.class) - String [] value() default {}; - - /** - * 验证模式:AND | OR,默认AND - * @return 验证模式 - */ - @AliasFor(annotation = SaCheckRole.class) - SaMode mode() default SaMode.AND; - -} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/StpUserUtil.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/StpUserUtil.java deleted file mode 100644 index d98b11b5c..000000000 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/StpUserUtil.java +++ /dev/null @@ -1,697 +0,0 @@ -package com.pj.satoken.at; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import cn.dev33.satoken.fun.SaFunction; -import cn.dev33.satoken.session.SaSession; -import cn.dev33.satoken.stp.SaLoginModel; -import cn.dev33.satoken.stp.SaTokenInfo; -import cn.dev33.satoken.stp.StpLogic; - -/** - * Sa-Token 权限验证工具类 (User版) - * @author kong - */ -@Component -public class StpUserUtil { - - /** - * 账号类型标识 - */ - public static final String TYPE = "user"; - - /** - * 底层的 StpLogic 对象 - */ - public static StpLogic stpLogic = new StpLogic(TYPE); - - /** - * 获取当前 StpLogic 的账号类型 - * @return See Note - */ - public static String getLoginType(){ - return stpLogic.getLoginType(); - } - - - // =================== 获取token 相关 =================== - - /** - * 返回token名称 - * @return 此StpLogic的token名称 - */ - public static String getTokenName() { - return stpLogic.getTokenName(); - } - - /** - * 在当前会话写入当前TokenValue - * @param tokenValue token值 - * @param cookieTimeout Cookie存活时间(秒) - */ - public static void setTokenValue(String tokenValue, int cookieTimeout){ - stpLogic.setTokenValue(tokenValue, cookieTimeout); - } - - /** - * 获取当前TokenValue - * @return 当前tokenValue - */ - public static String getTokenValue() { - return stpLogic.getTokenValue(); - } - - /** - * 获取当前会话的Token信息 - * @return token信息 - */ - public static SaTokenInfo getTokenInfo() { - return stpLogic.getTokenInfo(); - } - - - // =================== 登录相关操作 =================== - - /** - * 会话登录 - * @param id 账号id,建议的类型:(long | int | String) - */ - public static void login(Object id) { - stpLogic.login(id); - } - - /** - * 会话登录,并指定登录设备 - * @param id 账号id,建议的类型:(long | int | String) - * @param device 设备标识 - */ - public static void login(Object id, String device) { - stpLogic.login(id, device); - } - - /** - * 会话登录,并指定是否 [记住我] - * @param id 账号id,建议的类型:(long | int | String) - * @param isLastingCookie 是否为持久Cookie - */ - public static void login(Object id, boolean isLastingCookie) { - stpLogic.login(id, isLastingCookie); - } - - /** - * 会话登录,并指定所有登录参数Model - * @param id 登录id,建议的类型:(long | int | String) - * @param loginModel 此次登录的参数Model - */ - public static void login(Object id, SaLoginModel loginModel) { - stpLogic.login(id, loginModel); - } - - /** - * 会话注销 - */ - public static void logout() { - stpLogic.logout(); - } - - /** - * 会话注销,根据指定Token - * @param tokenValue 指定token - */ - public static void logoutByTokenValue(String tokenValue) { - stpLogic.logoutByTokenValue(tokenValue); - } - - /** - * 会话注销,根据账号id (踢人下线) - *

当对方再次访问系统时,会抛出NotLoginException异常,场景值=-2 - * @param loginId 账号id - */ - public static void logoutByLoginId(Object loginId) { - stpLogic.logoutByLoginId(loginId); - } - - /** - * 会话注销,根据账号id & 设备标识 (踢人下线) - *

当对方再次访问系统时,会抛出NotLoginException异常,场景值=-2 - * @param loginId 账号id - * @param device 设备标识 - */ - public static void logoutByLoginId(Object loginId, String device) { - stpLogic.logoutByLoginId(loginId, device); - } - - // 查询相关 - - /** - * 当前会话是否已经登录 - * @return 是否已登录 - */ - public static boolean isLogin() { - return stpLogic.isLogin(); - } - - /** - * 检验当前会话是否已经登录,如未登录,则抛出异常 - */ - public static void checkLogin() { - stpLogic.checkLogin(); - } - - /** - * 获取当前会话账号id, 如果未登录,则抛出异常 - * @return 账号id - */ - public static Object getLoginId() { - return stpLogic.getLoginId(); - } - - /** - * 获取当前会话账号id, 如果未登录,则返回默认值 - * @param 返回类型 - * @param defaultValue 默认值 - * @return 登录id - */ - public static T getLoginId(T defaultValue) { - return stpLogic.getLoginId(defaultValue); - } - - /** - * 获取当前会话账号id, 如果未登录,则返回null - * @return 账号id - */ - public static Object getLoginIdDefaultNull() { - return stpLogic.getLoginIdDefaultNull(); - } - - /** - * 获取当前会话账号id, 并转换为String类型 - * @return 账号id - */ - public static String getLoginIdAsString() { - return stpLogic.getLoginIdAsString(); - } - - /** - * 获取当前会话账号id, 并转换为int类型 - * @return 账号id - */ - public static int getLoginIdAsInt() { - return stpLogic.getLoginIdAsInt(); - } - - /** - * 获取当前会话账号id, 并转换为long类型 - * @return 账号id - */ - public static long getLoginIdAsLong() { - return stpLogic.getLoginIdAsLong(); - } - - /** - * 获取指定Token对应的账号id,如果未登录,则返回 null - * @param tokenValue token - * @return 账号id - */ - public static Object getLoginIdByToken(String tokenValue) { - return stpLogic.getLoginIdByToken(tokenValue); - } - - - // =================== session相关 =================== - - /** - * 获取指定账号id的Session, 如果Session尚未创建,isCreate=是否新建并返回 - * @param loginId 账号id - * @param isCreate 是否新建 - * @return Session对象 - */ - public static SaSession getSessionByLoginId(Object loginId, boolean isCreate) { - return stpLogic.getSessionByLoginId(loginId, isCreate); - } - - /** - * 获取指定key的Session, 如果Session尚未创建,则返回null - * @param sessionId SessionId - * @return Session对象 - */ - public static SaSession getSessionBySessionId(String sessionId) { - return stpLogic.getSessionBySessionId(sessionId); - } - - /** - * 获取指定账号id的Session,如果Session尚未创建,则新建并返回 - * @param loginId 账号id - * @return Session对象 - */ - public static SaSession getSessionByLoginId(Object loginId) { - return stpLogic.getSessionByLoginId(loginId); - } - - /** - * 获取当前会话的Session, 如果Session尚未创建,isCreate=是否新建并返回 - * @param isCreate 是否新建 - * @return Session对象 - */ - public static SaSession getSession(boolean isCreate) { - return stpLogic.getSession(isCreate); - } - - /** - * 获取当前会话的Session,如果Session尚未创建,则新建并返回 - * @return Session对象 - */ - public static SaSession getSession() { - return stpLogic.getSession(); - } - - - // =================== token专属session =================== - - /** - * 获取指定Token-Session,如果Session尚未创建,则新建并返回 - * @param tokenValue Token值 - * @return Session对象 - */ - public static SaSession getTokenSessionByToken(String tokenValue) { - return stpLogic.getTokenSessionByToken(tokenValue); - } - - /** - * 获取当前Token-Session,如果Session尚未创建,则新建并返回 - * @return Session对象 - */ - public static SaSession getTokenSession() { - return stpLogic.getTokenSession(); - } - - - // =================== [临时过期] 验证相关 =================== - - /** - * 检查当前token 是否已经[临时过期],如果已经过期则抛出异常 - */ - public static void checkActivityTimeout() { - stpLogic.checkActivityTimeout(); - } - - /** - * 续签当前token:(将 [最后操作时间] 更新为当前时间戳) - *

请注意: 即时token已经 [临时过期] 也可续签成功, - * 如果此场景下需要提示续签失败,可在此之前调用 checkActivityTimeout() 强制检查是否过期即可

- */ - public static void updateLastActivityToNow() { - stpLogic.updateLastActivityToNow(); - } - - - // =================== 过期时间相关 =================== - - /** - * 获取当前登录者的token剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ - public static long getTokenTimeout() { - return stpLogic.getTokenTimeout(); - } - - /** - * 获取当前登录者的Session剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ - public static long getSessionTimeout() { - return stpLogic.getSessionTimeout(); - } - - /** - * 获取当前token的专属Session剩余有效时间 (单位: 秒) - * @return token剩余有效时间 - */ - public static long getTokenSessionTimeout() { - return stpLogic.getTokenSessionTimeout(); - } - - /** - * 获取当前token[临时过期]剩余有效时间 (单位: 秒) - * @return token[临时过期]剩余有效时间 - */ - public static long getTokenActivityTimeout() { - return stpLogic.getTokenActivityTimeout(); - } - - - - // =================== 角色验证操作 =================== - - /** - * 指定账号id是否含有角色标识, 返回true或false - * @param loginId 账号id - * @param role 角色标识 - * @return 是否含有指定角色标识 - */ - public static boolean hasRole(Object loginId, String role) { - return stpLogic.hasRole(loginId, role); - } - - /** - * 当前账号是否含有指定角色标识, 返回true或false - * @param role 角色标识 - * @return 是否含有指定角色标识 - */ - public static boolean hasRole(String role) { - return stpLogic.hasRole(role); - } - - /** - * 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException - * @param role 角色标识 - */ - public static void checkRole(String role) { - stpLogic.checkRole(role); - } - - /** - * 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] - * @param roleArray 角色标识数组 - */ - public static void checkRoleAnd(String... roleArray){ - stpLogic.checkRoleAnd(roleArray); - } - - /** - * 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] - * @param roleArray 角色标识数组 - */ - public static void checkRoleOr(String... roleArray){ - stpLogic.checkRoleOr(roleArray); - } - - - // =================== 权限验证操作 =================== - - /** - * 指定账号id是否含有指定权限, 返回true或false - * @param loginId 账号id - * @param permission 权限码 - * @return 是否含有指定权限 - */ - public static boolean hasPermission(Object loginId, String permission) { - return stpLogic.hasPermission(loginId, permission); - } - - /** - * 当前账号是否含有指定权限, 返回true或false - * @param permission 权限码 - * @return 是否含有指定权限 - */ - public static boolean hasPermission(String permission) { - return stpLogic.hasPermission(permission); - } - - /** - * 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException - * @param permission 权限码 - */ - public static void checkPermission(String permission) { - stpLogic.checkPermission(permission); - } - - /** - * 当前账号是否含有指定权限 [指定多个,必须全部验证通过] - * @param permissionArray 权限码数组 - */ - public static void checkPermissionAnd(String... permissionArray) { - stpLogic.checkPermissionAnd(permissionArray); - } - - /** - * 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] - * @param permissionArray 权限码数组 - */ - public static void checkPermissionOr(String... permissionArray) { - stpLogic.checkPermissionOr(permissionArray); - } - - - // =================== id 反查token 相关操作 =================== - - /** - * 获取指定账号id的tokenValue - *

在配置为允许并发登录时,此方法只会返回队列的最后一个token, - * 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId - * @param loginId 账号id - * @return token值 - */ - public static String getTokenValueByLoginId(Object loginId) { - return stpLogic.getTokenValueByLoginId(loginId); - } - - /** - * 获取指定账号id指定设备端的tokenValue - *

在配置为允许并发登录时,此方法只会返回队列的最后一个token, - * 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId - * @param loginId 账号id - * @param device 设备标识 - * @return token值 - */ - public static String getTokenValueByLoginId(Object loginId, String device) { - return stpLogic.getTokenValueByLoginId(loginId, device); - } - - /** - * 获取指定账号id的tokenValue集合 - * @param loginId 账号id - * @return 此loginId的所有相关token - */ - public static List getTokenValueListByLoginId(Object loginId) { - return stpLogic.getTokenValueListByLoginId(loginId); - } - - /** - * 获取指定账号id指定设备端的tokenValue 集合 - * @param loginId 账号id - * @param device 设备标识 - * @return 此loginId的所有相关token - */ - public static List getTokenValueListByLoginId(Object loginId, String device) { - return stpLogic.getTokenValueListByLoginId(loginId, device); - } - - /** - * 返回当前会话的登录设备 - * @return 当前令牌的登录设备 - */ - public static String getLoginDevice() { - return stpLogic.getLoginDevice(); - } - - - // =================== 会话管理 =================== - - /** - * 根据条件查询Token - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return token集合 - */ - public static List searchTokenValue(String keyword, int start, int size) { - return stpLogic.searchTokenValue(keyword, start, size); - } - - /** - * 根据条件查询SessionId - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return sessionId集合 - */ - public static List searchSessionId(String keyword, int start, int size) { - return stpLogic.searchSessionId(keyword, start, size); - } - - /** - * 根据条件查询Token专属Session的Id - * @param keyword 关键字 - * @param start 开始处索引 (-1代表查询所有) - * @param size 获取数量 - * @return sessionId集合 - */ - public static List searchTokenSessionId(String keyword, int start, int size) { - return stpLogic.searchTokenSessionId(keyword, start, size); - } - - - // ------------------- 账号封禁 ------------------- - - /** - * 封禁指定账号 - *

此方法不会直接将此账号id踢下线,而是在对方再次登录时抛出`DisableLoginException`异常 - * @param loginId 指定账号id - * @param disableTime 封禁时间, 单位: 秒 (-1=永久封禁) - */ - public static void disable(Object loginId, long disableTime) { - stpLogic.disable(loginId, disableTime); - } - - /** - * 指定账号是否已被封禁 (true=已被封禁, false=未被封禁) - * @param loginId 账号id - * @return see note - */ - public static boolean isDisable(Object loginId) { - return stpLogic.isDisable(loginId); - } - - /** - * 获取指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) - * @param loginId 账号id - * @return see note - */ - public static long getDisableTime(Object loginId) { - return stpLogic.getDisableTime(loginId); - } - - /** - * 解封指定账号 - * @param loginId 账号id - */ - public static void untieDisable(Object loginId) { - stpLogic.untieDisable(loginId); - } - - - // =================== 身份切换 =================== - - /** - * 临时切换身份为指定账号id - * @param loginId 指定loginId - */ - public static void switchTo(Object loginId) { - stpLogic.switchTo(loginId); - } - - /** - * 结束临时切换身份 - */ - public static void endSwitch() { - stpLogic.endSwitch(); - } - - /** - * 当前是否正处于[身份临时切换]中 - * @return 是否正处于[身份临时切换]中 - */ - public static boolean isSwitch() { - return stpLogic.isSwitch(); - } - - /** - * 在一个代码段里方法内,临时切换身份为指定账号id - * @param loginId 指定账号id - * @param function 要执行的方法 - */ - public static void switchTo(Object loginId, SaFunction function) { - stpLogic.switchTo(loginId, function); - } - - - // ------------------- 二级认证 ------------------- - - /** - * 在当前会话 开启二级认证 - * @param timeout 维持时间 (单位: 秒) - */ - public static void openSafe(long safeTime) { - stpLogic.openSafe(safeTime); - } - - /** - * 当前会话 是否处于二级认证时间内 - * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 - */ - public static boolean isSafe() { - return stpLogic.isSafe(); - } - - /** - * 检查当前会话是否已通过二级认证,如未通过则抛出异常 - */ - public static void checkSafe() { - stpLogic.checkSafe(); - } - - /** - * 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) - * @return - */ - public static long getSafeTime() { - return stpLogic.getSafeTime(); - } - - /** - * 在当前会话 结束二级认证 - */ - public static void closeSafe() { - stpLogic.closeSafe(); - } - - - // =================== 历史API,兼容旧版本 =================== - - /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.getLoginType() ,使用方式保持不变

- * 获取当前StpLogin的loginKey - * @return 当前StpLogin的loginKey - */ - @Deprecated - public static String getLoginKey(){ - return stpLogic.getLoginType(); - } - - /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.login() ,使用方式保持不变

- * 在当前会话上登录id - * @param loginId 登录id,建议的类型:(long | int | String) - */ - @Deprecated - public static void setLoginId(Object loginId) { - stpLogic.login(loginId); - } - - /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.login() ,使用方式保持不变

- * 在当前会话上登录id, 并指定登录设备 - * @param loginId 登录id,建议的类型:(long | int | String) - * @param device 设备标识 - */ - @Deprecated - public static void setLoginId(Object loginId, String device) { - stpLogic.login(loginId, device); - } - - /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.login() ,使用方式保持不变

- * 在当前会话上登录id, 并指定登录设备 - * @param loginId 登录id,建议的类型:(long | int | String) - * @param isLastingCookie 是否为持久Cookie - */ - @Deprecated - public static void setLoginId(Object loginId, boolean isLastingCookie) { - stpLogic.login(loginId, isLastingCookie); - } - - /** - *

本函数设计已过时,未来版本可能移除此函数,请及时更换为 StpUtil.login() ,使用方式保持不变

- * 在当前会话上登录id, 并指定所有登录参数Model - * @param loginId 登录id,建议的类型:(long | int | String) - * @param loginModel 此次登录的参数Model - */ - @Deprecated - public static void setLoginId(Object loginId, SaLoginModel loginModel) { - stpLogic.login(loginId, loginModel); - } - -} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/AtController.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/AtController.java new file mode 100644 index 000000000..092381e04 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/AtController.java @@ -0,0 +1,80 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.annotation.SaCheckHttpBasic; +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; + +/** + * 注解鉴权测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/at/") +public class AtController { + + // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin + @SaCheckLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + return SaResult.ok(); + } + + // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission + @SaCheckPermission("user-add") + @RequestMapping("checkPermission") + public SaResult checkPermission() { + return SaResult.ok(); + } + + // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd + @SaCheckPermission({"user-add", "user-delete", "user-update"}) + @RequestMapping("checkPermissionAnd") + public SaResult checkPermissionAnd() { + return SaResult.ok(); + } + + // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr + @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) + @RequestMapping("checkPermissionOr") + public SaResult checkPermissionOr() { + return SaResult.ok(); + } + + // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole + @SaCheckRole("admin") + @RequestMapping("checkRole") + public SaResult checkRole() { + return SaResult.ok(); + } + + // 完成二级认证 ---- http://localhost:8081/at/openSafe + @RequestMapping("openSafe") + public SaResult openSafe() { + StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 + return SaResult.ok(); + } + + // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe + @SaCheckSafe + @RequestMapping("checkSafe") + public SaResult checkSafe() { + return SaResult.ok(); + } + + // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic + @SaCheckHttpBasic(account = "sa:123456") + @RequestMapping("checkBasic") + public SaResult checkBasic() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/GlobalException.java deleted file mode 100644 index 2b08436fd..000000000 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/GlobalException.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.pj.test; - -import java.io.IOException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.ResponseBody; - -import com.pj.util.AjaxJson; - -import cn.dev33.satoken.exception.DisableLoginException; -import cn.dev33.satoken.exception.NotLoginException; -import cn.dev33.satoken.exception.NotPermissionException; -import cn.dev33.satoken.exception.NotRoleException; - -/** - * 全局异常处理 - */ -@ControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin") -public class GlobalException { - - // 在当前类每个方法进入之前触发的操作 - @ModelAttribute - public void get(HttpServletRequest request) throws IOException { - - } - - - // 全局异常拦截(拦截项目中的所有异常) - @ResponseBody - @ExceptionHandler - public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) - throws Exception { - - // 打印堆栈,以供调试 - System.out.println("全局异常---------------"); - e.printStackTrace(); - - // 不同异常返回不同状态码 - AjaxJson aj = null; - if (e instanceof NotLoginException) { // 如果是未登录异常 - NotLoginException ee = (NotLoginException) e; - aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); - } else if(e instanceof NotRoleException) { // 如果是角色异常 - NotRoleException ee = (NotRoleException) e; - aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); - } else if(e instanceof NotPermissionException) { // 如果是权限异常 - NotPermissionException ee = (NotPermissionException) e; - aj = AjaxJson.getNotJur("无此权限:" + ee.getCode()); - } else if(e instanceof DisableLoginException) { // 如果是被封禁异常 - DisableLoginException ee = (DisableLoginException) e; - aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封"); - } else { // 普通异常, 输出:500 + 异常信息 - aj = AjaxJson.getError(e.getMessage()); - } - - // 返回给前端 - return aj; - - // 输出到客户端 -// response.setContentType("application/json; charset=utf-8"); // http说明,我要返回JSON对象 -// response.getWriter().print(new ObjectMapper().writeValueAsString(aj)); - } - - - - // 全局异常拦截(拦截项目中的NotLoginException异常) -// @ExceptionHandler(NotLoginException.class) -// public AjaxJson handlerNotLoginException(NotLoginException nle, HttpServletRequest request, HttpServletResponse response) -// throws Exception { -// -// // 打印堆栈,以供调试 -// nle.printStackTrace(); -// -// // 判断场景值,定制化异常信息 -// String message = ""; -// if(nle.getType().equals(NotLoginException.NOT_TOKEN)) { -// message = "未提供token"; -// } -// else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) { -// message = "token无效"; -// } -// else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) { -// message = "token已过期"; -// } -// else if(nle.getType().equals(NotLoginException.BE_REPLACED)) { -// message = "token已被顶下线"; -// } -// else if(nle.getType().equals(NotLoginException.KICK_OUT)) { -// message = "token已被踢下线"; -// } -// else { -// message = "当前会话未登录"; -// } -// -// // 返回给前端 -// return AjaxJson.getError(message); -// } - - -} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..34eea9a19 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java index 5c0b3ce1a..4883e6eee 100644 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java @@ -13,7 +13,7 @@ /** * 压力测试 - * @author kong + * @author click33 * */ @RestController diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/TestController.java index 7da39137b..fe914cf04 100644 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/TestController.java +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/TestController.java @@ -22,7 +22,7 @@ /** * 测试专用Controller - * @author kong + * @author click33 * */ @RestController @@ -160,8 +160,8 @@ public AjaxJson tokenInfo() { public AjaxJson atCheck() { System.out.println("======================= 进入方法,测试注解鉴权接口 ========================= "); System.out.println("只有通过注解鉴权,才能进入此方法"); -// StpUtil.checkActivityTimeout(); -// StpUtil.updateLastActivityToNow(); +// StpUtil.checkActiveTimeout(); +// StpUtil.updateLastActiveToNow(); return AjaxJson.getSuccess(); } @@ -175,8 +175,8 @@ public AjaxJson atJurOr() { // [活动时间] 续签: http://localhost:8081/test/rene @RequestMapping("rene") public AjaxJson rene() { - StpUtil.checkActivityTimeout(); - StpUtil.updateLastActivityToNow(); + StpUtil.checkActiveTimeout(); + StpUtil.updateLastActiveToNow(); return AjaxJson.getSuccess("续签成功"); } @@ -186,7 +186,7 @@ public AjaxJson kickOut() { // 先登录上 StpUtil.login(10001); // 踢下线 - StpUtil.logoutByLoginId(10001); + StpUtil.kickout(10001); // 再尝试获取 StpUtil.getLoginId(); // 返回 @@ -218,7 +218,7 @@ public AjaxJson switchTo() { public AjaxJson search() { System.out.println("--------------"); Ttime t = new Ttime().start(); - List tokenValue = StpUtil.searchTokenValue("8feb8265f773", 0, 10); + List tokenValue = StpUtil.searchTokenValue("8feb8265f773", 0, 10, true); for (String v : tokenValue) { // SaSession session = StpUtil.getSessionBySessionId(sid); System.out.println(v); @@ -235,12 +235,11 @@ public AjaxJson loginByDevice() { return AjaxJson.getSuccessData("登录成功"); } - // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public AjaxJson test() { - System.out.println("进来了"); - return AjaxJson.getSuccess(); + System.out.println("------------进来了"); + return AjaxJson.getSuccess(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @@ -249,5 +248,4 @@ public AjaxJson test2() { return AjaxJson.getSuccess(); } - } diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/UserController.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/UserController.java deleted file mode 100644 index 7006ed29b..000000000 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/UserController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.pj.test; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import cn.dev33.satoken.stp.StpUtil; - -/** - * 登录测试 - * @author kong - * - */ -@RestController -@RequestMapping("/user/") -public class UserController { - - // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 - @RequestMapping("doLogin") - public String doLogin(String username, String password) { - // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 - if("zhang".equals(username) && "123456".equals(password)) { - StpUtil.login(10001); - return "登录成功"; - } - return "登录失败"; - } - - // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin - @RequestMapping("isLogin") - public String isLogin(String username, String password) { - return "当前会话是否登录:" + StpUtil.isLogin(); - } - -} diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/util/Ttime.java b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/util/Ttime.java index cb17dcaa3..dec4f1cbe 100644 --- a/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/util/Ttime.java +++ b/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/util/Ttime.java @@ -3,7 +3,7 @@ /** * 用于测试用时 - * @author kong + * @author click33 * */ public class Ttime { diff --git a/sa-token-demo/sa-token-demo-springboot/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-springboot/src/main/resources/application.yml index 5ac6109ec..dfa0066e2 100644 --- a/sa-token-demo/sa-token-demo-springboot/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-springboot/src/main/resources/application.yml @@ -1,24 +1,24 @@ # 端口 server: port: 8081 - -# sa-token配置 + +# sa-token 配置 sa-token: - # token名称 (同时也是cookie名称) + # token 名称 (同时也是 cookie 名称) token-name: satoken - # token有效期,单位s 默认30天, -1代表永不过期 + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 - # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 - activity-timeout: -1 - # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true - # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) - is-share: true - # token风格 + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 - is-log: false - + is-log: true + spring: # redis配置 redis: @@ -30,8 +30,8 @@ spring: port: 6379 # Redis服务器连接密码(默认为空) password: - # 连接超时时间(毫秒) - timeout: 10ms + # 连接超时时间 + timeout: 10s lettuce: pool: # 连接池最大连接数 diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/pom.xml b/sa-token-demo/sa-token-demo-springboot3-redis/pom.xml new file mode 100644 index 000000000..a7507d47f --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/pom.xml @@ -0,0 +1,71 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-springboot3-redis + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 3.5.11 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/SaTokenSpringBoot3Application.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/SaTokenSpringBoot3Application.java new file mode 100644 index 000000000..c10af67da --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/SaTokenSpringBoot3Application.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 SpringBoot3 示例,整合redis + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenSpringBoot3Application { + + public static void main(String[] args) { + SpringApplication.run(SaTokenSpringBoot3Application.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..7fb1c5630 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,56 @@ +package com.pj.current; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.exception.DisableServiceException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 不同异常返回不同状态码 + AjaxJson aj = null; + if (e instanceof NotLoginException) { // 如果是未登录异常 + NotLoginException ee = (NotLoginException) e; + aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); + } + else if(e instanceof NotRoleException) { // 如果是角色异常 + NotRoleException ee = (NotRoleException) e; + aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); + } + else if(e instanceof NotPermissionException) { // 如果是权限异常 + NotPermissionException ee = (NotPermissionException) e; + aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); + } + else if(e instanceof DisableServiceException) { // 如果是被封禁异常 + DisableServiceException ee = (DisableServiceException) e; + aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); + } + else { // 普通异常, 输出:500 + 异常信息 + aj = AjaxJson.getError(e.getMessage()); + } + + // 返回给前端 + return aj; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..6e744e4fb --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,26 @@ +package com.pj.current; + +import java.io.IOException; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 处理 404 + * @author click33 + */ +@RestController +public class NotFoundHandle implements ErrorController { + + @RequestMapping("/error") + public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setStatus(200); + return SaResult.get(404, "not found", null); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..bf339509c --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,71 @@ +package com.pj.satoken; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.util.SaResult; + + +/** + * [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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // System.out.println("---------- sa全局认证 " + SaHolder.getRequest().getRequestPath()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) + .setBeforeAuth(r -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + ; + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..b6cc79f71 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/AtController.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/AtController.java new file mode 100644 index 000000000..092381e04 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/AtController.java @@ -0,0 +1,80 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.annotation.SaCheckHttpBasic; +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; + +/** + * 注解鉴权测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/at/") +public class AtController { + + // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin + @SaCheckLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + return SaResult.ok(); + } + + // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission + @SaCheckPermission("user-add") + @RequestMapping("checkPermission") + public SaResult checkPermission() { + return SaResult.ok(); + } + + // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd + @SaCheckPermission({"user-add", "user-delete", "user-update"}) + @RequestMapping("checkPermissionAnd") + public SaResult checkPermissionAnd() { + return SaResult.ok(); + } + + // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr + @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) + @RequestMapping("checkPermissionOr") + public SaResult checkPermissionOr() { + return SaResult.ok(); + } + + // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole + @SaCheckRole("admin") + @RequestMapping("checkRole") + public SaResult checkRole() { + return SaResult.ok(); + } + + // 完成二级认证 ---- http://localhost:8081/at/openSafe + @RequestMapping("openSafe") + public SaResult openSafe() { + StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 + return SaResult.ok(); + } + + // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe + @SaCheckSafe + @RequestMapping("checkSafe") + public SaResult checkSafe() { + return SaResult.ok(); + } + + // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic + @SaCheckHttpBasic(account = "sa:123456") + @RequestMapping("checkBasic") + public SaResult checkBasic() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..34eea9a19 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/StressTestController.java new file mode 100644 index 000000000..b42731093 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/StressTestController.java @@ -0,0 +1,61 @@ +package com.pj.test; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.pj.util.Ttime; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 压力测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/s-test/") +public class StressTestController { + + // 测试 浏览器访问: http://localhost:8081/s-test/login + // 测试前,请先将 is-read-cookie 配置为 false + @RequestMapping("login") + public SaResult login() { +// StpUtil.getTokenSession().logout(); +// StpUtil.logoutByLoginId(10001); + + int count = 10; // 循环多少轮 + int loginCount = 10000; // 每轮循环多少次 + + // 循环10次 取平均时间 + List list = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + System.out.println("\n---------------------第" + i + "轮---------------------"); + Ttime t = new Ttime().start(); + // 每次登录的次数 + for (int j = 1; j <= loginCount; j++) { + StpUtil.login("1000" + j, "PC-" + j); + if(j % 1000 == 0) { + System.out.println("已登录:" + j); + } + } + t.end(); + list.add((t.returnMs() + 0.0) / 1000); + System.out.println("第" + i + "轮" + "用时:" + t.toString()); + } +// System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); + + System.out.println("\n---------------------测试结果---------------------"); + System.out.println(list.size() + "次测试: " + list); + double ss = 0; + for (int i = 0; i < list.size(); i++) { + ss += list.get(i); + } + System.out.println("平均用时: " + ss / list.size()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..46e2caaf9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,43 @@ +package com.pj.test; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + // 测试 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("------------进来了"); + System.out.println(SpringMVCUtil.getRequest()); + System.out.println(SaTokenContextJakartaServletUtil.getRequest()); + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8081/test/test2 + @RequestMapping("test2") + public SaResult test2() { + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8081/test/getRequestPath + @RequestMapping("getRequestPath") + public SaResult getRequestPath() { + System.out.println("-------------- 测试请求 path 获取"); + System.out.println("request.getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); + System.out.println("saRequest.getRequestPath() " + SaHolder.getRequest().getRequestPath()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/AjaxJson.java new file mode 100644 index 000000000..768d05784 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.util; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/Ttime.java b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/Ttime.java new file mode 100644 index 000000000..dec4f1cbe --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/Ttime.java @@ -0,0 +1,63 @@ +package com.pj.util; + + +/** + * 用于测试用时 + * @author click33 + * + */ +public class Ttime { + + private long start=0; //开始时间 + private long end=0; //结束时间 + + public static Ttime t = new Ttime(); //static快捷使用 + + /** + * 开始计时 + * @return + */ + public Ttime start() { + start=System.currentTimeMillis(); + return this; + } + + + /** + * 结束计时 + */ + public Ttime end() { + end=System.currentTimeMillis(); + return this; + } + + + /** + * 返回所用毫秒数 + */ + public long returnMs() { + return end-start; + } + + /** + * 格式化输出结果 + */ + public void outTime() { + System.out.println(this.toString()); + } + + /** + * 结束并格式化输出结果 + */ + public void endOutTime() { + this.end().outTime(); + } + + @Override + public String toString() { + return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s + } + + + +} diff --git a/sa-token-demo/sa-token-demo-springboot3-redis/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/resources/application.yml new file mode 100644 index 000000000..c8b24511d --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot3-redis/src/main/resources/application.yml @@ -0,0 +1,50 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/pom.xml b/sa-token-demo/sa-token-demo-springboot4-redis/pom.xml new file mode 100644 index 000000000..7a322a989 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/pom.xml @@ -0,0 +1,65 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-springboot4-redis + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-aspectj + + + + + cn.dev33 + sa-token-spring-boot4-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/SaTokenSpringBoot4Application.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/SaTokenSpringBoot4Application.java new file mode 100644 index 000000000..ed6d88351 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/SaTokenSpringBoot4Application.java @@ -0,0 +1,20 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token 整合 SpringBoot4 示例,整合 redis + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenSpringBoot4Application { + + public static void main(String[] args) { + SpringApplication.run(SaTokenSpringBoot4Application.class, args); + System.out.println("\n🎉 启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..7fb1c5630 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,56 @@ +package com.pj.current; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.exception.DisableServiceException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 不同异常返回不同状态码 + AjaxJson aj = null; + if (e instanceof NotLoginException) { // 如果是未登录异常 + NotLoginException ee = (NotLoginException) e; + aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); + } + else if(e instanceof NotRoleException) { // 如果是角色异常 + NotRoleException ee = (NotRoleException) e; + aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); + } + else if(e instanceof NotPermissionException) { // 如果是权限异常 + NotPermissionException ee = (NotPermissionException) e; + aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); + } + else if(e instanceof DisableServiceException) { // 如果是被封禁异常 + DisableServiceException ee = (DisableServiceException) e; + aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); + } + else { // 普通异常, 输出:500 + 异常信息 + aj = AjaxJson.getError(e.getMessage()); + } + + // 返回给前端 + return aj; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..125fa3075 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,26 @@ +//package com.pj.current; +// +//import java.io.IOException; +// +//import org.springframework.boot.web.servlet.error.ErrorController; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RestController; +// +//import cn.dev33.satoken.util.SaResult; +//import jakarta.servlet.http.HttpServletRequest; +//import jakarta.servlet.http.HttpServletResponse; +// +///** +// * 处理 404 +// * @author click33 +// */ +//@RestController +//public class NotFoundHandle implements ErrorController { +// +// @RequestMapping("/error") +// public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { +// response.setStatus(200); +// return SaResult.get(404, "not found", null); +// } +// +//} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..475492811 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,71 @@ +package com.pj.satoken; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.util.SaResult; + + +/** + * [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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // System.out.println("---------- sa全局认证 " + SaHolder.getRequest().getRequestPath()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) + .setBeforeAuth(r -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + ; + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..b6cc79f71 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/AtController.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/AtController.java new file mode 100644 index 000000000..14d2171fb --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/AtController.java @@ -0,0 +1,80 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.annotation.SaCheckHttpBasic; +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; + +/** + * 注解鉴权测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/at/") +public class AtController { + + // 登录认证,登录之后才可以进入方法 ---- http://localhost:8082/at/checkLogin + @SaCheckLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + return SaResult.ok(); + } + + // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8082/at/checkPermission + @SaCheckPermission("user-add") + @RequestMapping("checkPermission") + public SaResult checkPermission() { + return SaResult.ok(); + } + + // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8082/at/checkPermissionAnd + @SaCheckPermission({"user-add", "user-delete", "user-update"}) + @RequestMapping("checkPermissionAnd") + public SaResult checkPermissionAnd() { + return SaResult.ok(); + } + + // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8082/at/checkPermissionOr + @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) + @RequestMapping("checkPermissionOr") + public SaResult checkPermissionOr() { + return SaResult.ok(); + } + + // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8082/at/checkRole + @SaCheckRole("admin") + @RequestMapping("checkRole") + public SaResult checkRole() { + return SaResult.ok(); + } + + // 完成二级认证 ---- http://localhost:8082/at/openSafe + @RequestMapping("openSafe") + public SaResult openSafe() { + StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 + return SaResult.ok(); + } + + // 通过二级认证后才可以进入 ---- http://localhost:8082/at/checkSafe + @SaCheckSafe + @RequestMapping("checkSafe") + public SaResult checkSafe() { + return SaResult.ok(); + } + + // 通过Basic认证后才可以进入 ---- http://localhost:8082/at/checkBasic + @SaCheckHttpBasic(account = "sa:123456") + @RequestMapping("checkBasic") + public SaResult checkBasic() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/FaviconController.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/FaviconController.java new file mode 100644 index 000000000..e1d4683a8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/FaviconController.java @@ -0,0 +1,15 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class FaviconController { + + @RequestMapping("/favicon.ico") + public String favicon() { + return ""; + } + +} + diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..e04b06c55 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8082/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8082/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8082/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8082/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/StressTestController.java new file mode 100644 index 000000000..52820341c --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/StressTestController.java @@ -0,0 +1,61 @@ +package com.pj.test; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.pj.util.Ttime; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 压力测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/s-test/") +public class StressTestController { + + // 测试 浏览器访问: http://localhost:8082/s-test/login + // 测试前,请先将 is-read-cookie 配置为 false + @RequestMapping("login") + public SaResult login() { +// StpUtil.getTokenSession().logout(); +// StpUtil.logoutByLoginId(10001); + + int count = 10; // 循环多少轮 + int loginCount = 10000; // 每轮循环多少次 + + // 循环10次 取平均时间 + List list = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + System.out.println("\n---------------------第" + i + "轮---------------------"); + Ttime t = new Ttime().start(); + // 每次登录的次数 + for (int j = 1; j <= loginCount; j++) { + StpUtil.login("1000" + j, "PC-" + j); + if(j % 1000 == 0) { + System.out.println("已登录:" + j); + } + } + t.end(); + list.add((t.returnMs() + 0.0) / 1000); + System.out.println("第" + i + "轮" + "用时:" + t.toString()); + } +// System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); + + System.out.println("\n---------------------测试结果---------------------"); + System.out.println(list.size() + "次测试: " + list); + double ss = 0; + for (int i = 0; i < list.size(); i++) { + ss += list.get(i); + } + System.out.println("平均用时: " + ss / list.size()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..b0914ec37 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,43 @@ +package com.pj.test; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + // 测试 浏览器访问: http://localhost:8082/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("------------进来了"); + System.out.println(SpringMVCUtil.getRequest()); + System.out.println(SaTokenContextJakartaServletUtil.getRequest()); + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8082/test/test2 + @RequestMapping("test2") + public SaResult test2() { + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8082/test/getRequestPath + @RequestMapping("getRequestPath") + public SaResult getRequestPath() { + System.out.println("-------------- 测试请求 path 获取"); + System.out.println("request.getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); + System.out.println("saRequest.getRequestPath() " + SaHolder.getRequest().getRequestPath()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/AjaxJson.java new file mode 100644 index 000000000..768d05784 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.util; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/Ttime.java b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/Ttime.java new file mode 100644 index 000000000..dec4f1cbe --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/Ttime.java @@ -0,0 +1,63 @@ +package com.pj.util; + + +/** + * 用于测试用时 + * @author click33 + * + */ +public class Ttime { + + private long start=0; //开始时间 + private long end=0; //结束时间 + + public static Ttime t = new Ttime(); //static快捷使用 + + /** + * 开始计时 + * @return + */ + public Ttime start() { + start=System.currentTimeMillis(); + return this; + } + + + /** + * 结束计时 + */ + public Ttime end() { + end=System.currentTimeMillis(); + return this; + } + + + /** + * 返回所用毫秒数 + */ + public long returnMs() { + return end-start; + } + + /** + * 格式化输出结果 + */ + public void outTime() { + System.out.println(this.toString()); + } + + /** + * 结束并格式化输出结果 + */ + public void endOutTime() { + this.end().outTime(); + } + + @Override + public String toString() { + return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s + } + + + +} diff --git a/sa-token-demo/sa-token-demo-springboot4-redis/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/resources/application.yml new file mode 100644 index 000000000..04d0b3cf6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-springboot4-redis/src/main/resources/application.yml @@ -0,0 +1,50 @@ +# 端口 +server: + port: 8082 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +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 + + + + + diff --git a/sa-token-demo/sa-token-demo-sse/pom.xml b/sa-token-demo/sa-token-demo-sse/pom.xml new file mode 100644 index 000000000..9d43e3cdc --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/pom.xml @@ -0,0 +1,59 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sse + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + + + 1.45.0 + com.pj.SaTokenSseApplication + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + cn.hutool + hutool-all + 5.8.36 + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java new file mode 100644 index 000000000..753ff3845 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +/** + * Sa-Token 测试 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenSseApplication { + + // SSE 连接测试在线工具:https://toolshu.com/sse + + public static void main(String[] args) { + SpringApplication.run(SaTokenSseApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..750fc780d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,20 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..bc714c9e3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,52 @@ +package com.pj.satoken; + +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import org.springframework.context.annotation.Bean; +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) { + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); + } + + /** + * CORS 跨域处理 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..ebbc225a4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,43 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.pj.util.SseEmitterHolder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?uid=10001 + @RequestMapping("doLogin") + public SaResult doLogin(@RequestParam(defaultValue = "10001") long uid) { + StpUtil.login(uid); + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + if(StpUtil.isLogin()) { + long uid = StpUtil.getLoginIdAsLong(); + SseEmitterHolder.closeByUid(uid); + StpUtil.logout(); + } + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java new file mode 100644 index 000000000..f352fd73b --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java @@ -0,0 +1,29 @@ +package com.pj.test; + +import cn.dev33.satoken.util.SaResult; +import com.pj.util.SseEmitterHolder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * SSE 推送 + */ +@RestController +public class SseAdminController { + + // 推送消息 --- http://localhost:8081/sse/send?uid=10001&message=hello123 + @RequestMapping(value = "/sse/send") + public SaResult sendMessage(long uid, String message) { + SseEmitterHolder.sendMessageByUid(uid, message); + return SaResult.ok(); + } + + // 断开 --- http://localhost:8081/sse/close?uid=10001 + @RequestMapping(value = "/sse/close") + public SaResult close(long uid){ + SseEmitterHolder.closeByUid(uid); + return SaResult.ok(); + } + +} + diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java new file mode 100644 index 000000000..be52d225a --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java @@ -0,0 +1,23 @@ +package com.pj.test; + +import com.pj.util.SseEmitterHolder; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +/** + * SSE 连接 + */ +@RestController +public class SseController { + + + // 创建连接 --- http://localhost:8081/sse?satoken=d8a8e1c7-62a4-4656-8b54-cc14e6348ceb + @RequestMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter createSse(String satoken) { + return SseEmitterHolder.createSse(satoken); + } + +} + diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java new file mode 100644 index 000000000..291d30e39 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java @@ -0,0 +1,139 @@ +package com.pj.util; + +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * SSE 连接管理器 + * + * @author click33 + * @since 2025/4/11 + */ +public class SseEmitterHolder { + + public static final Map sseEmitterMap = new ConcurrentHashMap<>(); + + /** + * 创建客户端 + */ + public static SseEmitter createSse(String satoken) { + + Object loginId = StpUtil.getLoginIdByToken(satoken); + if(loginId == null) { + throw new NotLoginException("无效 token", StpUtil.TYPE, NotLoginException.INVALID_TOKEN); + } + long uid = SaFoxUtil.getValueByType(loginId, Long.class); + + // 默认 30 秒超时,设置为 0L 则永不超时 + SseEmitter sseEmitter = new SseEmitter(600 * 1000L); + sseEmitterMap.put(satoken, sseEmitter); + System.out.println("连接成功:satoken=" + satoken + ",uid=" + uid); + + // 完成后回调 + sseEmitter.onCompletion(() -> { + System.out.println("结束连接:satoken=" + satoken + ",uid=" + uid); + sseEmitterMap.remove(satoken); + }); + + //超时回调 + sseEmitter.onTimeout(() -> { + System.out.println("连接超时:satoken=" + satoken + ",uid=" + uid); + }); + + //异常回调 + sseEmitter.onError( e -> { +// try { + System.out.println("连接异常:satoken=" + satoken + ",uid=" + uid); + System.err.println(e.getMessage()); +// sseEmitter.send(SseEmitter.event() +// .id(String.valueOf(uid)) +// .name("发生异常!") +// .data("发生异常请重试!") +// .reconnectTime(3000)); +// sseEmitterMap.put(uid, sseEmitter); +// } catch (IOException ee) { +// ee.printStackTrace(); +// } + }); + try { + sseEmitter.send(SseEmitter.event().reconnectTime(5000)); + } catch (IOException e) { + e.printStackTrace(); + } + + return sseEmitter; + } + + /** + * 给指定 token 客户端发送消息 + * + */ + public static void sendMessageByToken(String satoken, String message) { + SseEmitter sseEmitter = sseEmitterMap.get(satoken); + if (sseEmitter == null) { + System.out.println("该 token 暂未建立连接:" + satoken); + return; + } + try { + sseEmitter.send(SseEmitter.event().reconnectTime(60 * 1000L).data(message)); + System.out.println("消息推送成功,token=" + satoken + ", message=" + message); + }catch (Exception e) { + e.printStackTrace(); +// sseEmitterMap.remove(uid); +// log.info("用户{},消息id:{},推送异常:{}", uid,messageId, e.getMessage()); +// sseEmitter.complete(); + } + } + + /** + * 给指定 用户 所有客户端发送消息 + * + */ + public static void sendMessageByUid(long uid, String message) { + List tokenList = StpUtil.getTokenValueListByLoginId(uid); + for (String token : tokenList) { + sendMessageByToken(token, message); + } + } + + /** + * 指定 token 断开连接 + * + */ + public static void closeByToken(String satoken) { + SseEmitter sseEmitter = sseEmitterMap.get(satoken); + if (sseEmitter == null) { + System.out.println("该 token 暂未建立连接:" + satoken); + return; + } + try { + sendMessageByToken(satoken, "连接已断开!"); + sseEmitter.complete(); + System.out.println("连接已断开,token=" + satoken); + }catch (Exception e) { + e.printStackTrace(); + // sseEmitterMap.remove(uid); + // log.info("用户{},消息id:{},推送异常:{}", uid,messageId, e.getMessage()); + // sseEmitter.complete(); + } + } + + /** + * 指定 uid 断开连接 + * + */ + public static void closeByUid(long uid) { + List tokenList = StpUtil.getTokenValueListByLoginId(uid); + for (String token : tokenList) { + closeByToken(token); + } + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml new file mode 100644 index 000000000..95e98258f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/README.md b/sa-token-demo/sa-token-demo-ssm/README.md new file mode 100644 index 000000000..e694b724b --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/README.md @@ -0,0 +1,10 @@ + +## SSM 架构集成 Sa-Token 示例 + +说是SSM,其实没有M,仅仅是给使用 SpringMVC 非 SpringBoot 的项目提供一个简单的 Sa-Token 集成示例。 + +直接运行项目即可,里面注释挺全的,也不必做过多说明了 +(其实就是我懒,光搭建起来这个架子就累瘫了,各种版本兼容问题报起错来大汗淋漓,推荐新项目能上 SpringBoot 就赶紧上吧,千万别在SSM上浪费生命)。 + +推荐 jdk8 + tomcat8。 + diff --git a/sa-token-demo/sa-token-demo-ssm/pom.xml b/sa-token-demo/sa-token-demo-ssm/pom.xml new file mode 100644 index 000000000..6ed37083d --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + cn.dev33 + sa-token-demo-ssm + war + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + 0.0.1-SNAPSHOT + + + + UTF-8 + UTF-8 + + 5.3.7 + 2.16.1 + 1.45.0 + + + + + + + javax.servlet + javax.servlet-api + 3.1.0 + + + + + org.springframework + spring-core + ${spring.version} + + + org.springframework + spring-web + ${spring.version} + + + org.springframework + spring-oxm + ${spring.version} + + + org.springframework + spring-tx + ${spring.version} + + + org.springframework + spring-webmvc + ${spring.version} + + + org.springframework + spring-context + ${spring.version} + + + org.springframework + spring-context-support + ${spring.version} + + + org.springframework + spring-aop + ${spring.version} + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + org.springframework.boot + spring-boot-starter-web + + + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + redis.clients + jedis + 3.8.0 + + + org.springframework.data + spring-data-redis + 2.3.9.RELEASE + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.11.2 + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..ee0e9b36b --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/AtController.java @@ -0,0 +1,74 @@ +package com.pj.controller; + +import cn.dev33.satoken.annotation.*; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 注解鉴权测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/at/") +public class AtController { + + // 登录认证,登录之后才可以进入方法 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkLogin + @SaCheckLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + return SaResult.ok(); + } + + // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermission + @SaCheckPermission("user-add") + @RequestMapping("checkPermission") + public SaResult checkPermission() { + return SaResult.ok(); + } + + // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermissionAnd + @SaCheckPermission({"user-add", "user-delete", "user-update"}) + @RequestMapping("checkPermissionAnd") + public SaResult checkPermissionAnd() { + return SaResult.ok(); + } + + // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermissionOr + @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) + @RequestMapping("checkPermissionOr") + public SaResult checkPermissionOr() { + return SaResult.ok(); + } + + // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkRole + @SaCheckRole("admin") + @RequestMapping("checkRole") + public SaResult checkRole() { + return SaResult.ok(); + } + + // 完成二级认证 ---- http://localhost:8080/sa_token_demo_ssm_war/at/openSafe + @RequestMapping("openSafe") + public SaResult openSafe() { + StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 + return SaResult.ok(); + } + + // 通过二级认证后才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkSafe + @SaCheckSafe + @RequestMapping("checkSafe") + public SaResult checkSafe() { + return SaResult.ok(); + } + + // 通过Basic认证后才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkBasic + @SaCheckHttpBasic(account = "sa:123456") + @RequestMapping("checkBasic") + public SaResult checkBasic() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/LoginController.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/LoginController.java new file mode 100644 index 000000000..c21384350 --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/LoginController.java @@ -0,0 +1,55 @@ +package com.pj.controller; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + System.out.println("-------- 12344"); + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 校验登录 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/checkLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + StpUtil.checkLogin(); + return SaResult.ok(); + } + + // 查询 Token 信息 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/PageController.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/PageController.java new file mode 100644 index 000000000..a996f76fe --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/PageController.java @@ -0,0 +1,38 @@ +package com.pj.controller; + +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * 页面访问测试 + * @author click33 + * @since 2024/4/14 + */ +@Controller +public class PageController { + + // http://localhost:8080/sa_token_demo_ssm_war/home + @RequestMapping("/home") + public String index() { + System.out.println("------- home页,所有游客可访问"); + return "home"; + } + + // http://localhost:8080/sa_token_demo_ssm_war/user + @RequestMapping("/user") + public String user() { + System.out.println("------- user页,登录后才能访问"); + StpUtil.checkLogin(); + return "user"; + } + + // http://localhost:8080/sa_token_demo_ssm_war/admin + @RequestMapping("/admin") + public String admin() { + System.out.println("------- admin页,具有admin角色才能访问"); + StpUtil.checkRole("admin"); + return "admin"; + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/TestController.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/TestController.java new file mode 100644 index 000000000..b20fec1ea --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/TestController.java @@ -0,0 +1,54 @@ +package com.pj.controller; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.stp.SaLoginConfig; +import cn.dev33.satoken.stp.StpUtil; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + // 测试登录 ---- http://localhost:8080/sa_token_demo_ssm_war/test/login + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue = "10001") long id) { + StpUtil.login(id, SaLoginConfig.setActiveTimeout(-1)); + return SaResult.ok("登录成功"); + } + + // 测试 浏览器访问: http://localhost:8080/sa_token_demo_ssm_war/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("------------进来了 " + SaFoxUtil.formatDate(new Date())); + // StpUtil.getLoginId(); + // 返回 + return SaResult.data(null); + } + + // 测试 浏览器访问: http://localhost:8080/sa_token_demo_ssm_war/test/test2 + @RequestMapping("test2") + public SaResult test2() { + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8080/sa_token_demo_ssm_war/getRequestPath + @RequestMapping("getRequestPath") + public SaResult getRequestPath() { + System.out.println("------------ 测试访问路径获取 "); +// System.out.println("SpringMVCUtil.getRequest().getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); + System.out.println("SaHolder.getRequest().getRequestPath() " + SaHolder.getRequest().getRequestPath()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..7ac105ccb --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,29 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { + + // 打印堆栈,以供调试 + System.err.println("全局异常---------------"); + e.printStackTrace(); + + // 返回给前端 + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..5d456fd97 --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,24 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 处理 404 + * @author click33 + */ +@RestController +public class NotFoundHandle implements ErrorController { + + @RequestMapping("/error") + public Object error(HttpServletRequest request, HttpServletResponse response) { + response.setStatus(200); + return SaResult.get(404, "not found", null); + } + +} diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/model/SysUser.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/model/SysUser.java new file mode 100644 index 000000000..b434edb3e --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/model/SysUser.java @@ -0,0 +1,83 @@ +package com.pj.model; + +/** + * User 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysUser { + + public SysUser() {} + + public SysUser(long id, String name, int age) { + super(); + this.id = id; + this.name = name; + this.age = age; + } + + + /** + * 用户id + */ + private long id; + + /** + * 用户名称 + */ + private String name; + + /** + * 用户年龄 + */ + private int age; + + /** + * @return id + */ + public long getId() { + return id; + } + + /** + * @param id 要设置的 id + */ + public void setId(long id) { + this.id = id; + } + + /** + * @return name + */ + public String getName() { + return name; + } + + /** + * @param name 要设置的 name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return age + */ + public int getAge() { + return age; + } + + /** + * @param age 要设置的 age + */ + public void setAge(int age) { + this.age = age; + } + + @Override + public String toString() { + return "SysUser [id=" + id + ", name=" + name + ", age=" + age + "]"; + } + +} diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaInterceptorImpl.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaInterceptorImpl.java new file mode 100644 index 000000000..5f961a9bf --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaInterceptorImpl.java @@ -0,0 +1,32 @@ +package com.pj.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.interceptor.SaInterceptor; + +/** + * 路由拦截鉴权测试 + * @author click33 + * @since 2024/4/15 + */ +public class SaInterceptorImpl extends SaInterceptor { + + public SaInterceptorImpl() { + super(hadnle->{ + System.out.println("-------------- SA 路由拦截鉴权,你访问的是:" + SaHolder.getRequest().getRequestPath()); + // System.out.println("你访问的是:" + SaHolder.getRequest().getRequestPath()); + // SaRouter.match("/test/test", r -> StpUtil.checkLogin()); + + // 根据路由划分模块,不同模块不同鉴权 +// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); +// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); +// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); +// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); +// SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); +// SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); + + // 更多写法参考:https://sa-token.com/doc.html#/use/route-check + + }); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaTokenBeanInjection.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaTokenBeanInjection.java new file mode 100644 index 000000000..d22a9b676 --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaTokenBeanInjection.java @@ -0,0 +1,57 @@ +package com.pj.satoken; + +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate; +import cn.dev33.satoken.json.SaJsonTemplateForJackson; +import cn.dev33.satoken.log.SaLog; +import cn.dev33.satoken.plugin.SaTokenPluginHolder; +import cn.dev33.satoken.spring.SaBeanInject; +import cn.dev33.satoken.spring.SaTokenContextForSpring; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +/** + * 手动注入 Sa-Token 所需要的组件 + * @author click33 + * @since 2024/4/15 + */ +public class SaTokenBeanInjection { + + public SaTokenBeanInjection( + SaLog log, + SaTokenConfig config, + @Autowired(required = false) SaTokenPluginHolder pluginHolder, + RedisConnectionFactory connectionFactory, + String routePrefix + ) { + System.out.println("---------------- 手动注入 Sa-Token 所需要的组件 start ----------------"); + + // 日志组件、配置信息 + SaBeanInject inject = new SaBeanInject(log, config, pluginHolder); + + // 基于 Spring 的上下文处理器 + inject.setSaTokenContext(new SaTokenContextForSpring()); + + // 基于 Jackson 的 json解析器 + inject.setSaJsonTemplate(new SaJsonTemplateForJackson()); + + // 基于 Jackson 序列化的 Redis 持久化组件 + SaTokenDaoForRedisTemplate saTokenDaoForRedisTemplate = new SaTokenDaoForRedisTemplate(); + saTokenDaoForRedisTemplate.init(connectionFactory); + inject.setSaTokenDao(saTokenDaoForRedisTemplate); + + // 权限和角色数据 + inject.setStpInterface(new StpInterfaceImpl()); + + // 项目路由前缀,方便路由拦截鉴权的 + ApplicationInfo.routePrefix = routePrefix; + // System.out.println(routePrefix); + + // 注入更多组件 .... + // inject.setXxx + + System.out.println("---------------- 手动注入 Sa-Token 所需要的组件 end ----------------"); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..45637719a --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,41 @@ +package com.pj.satoken; + +import cn.dev33.satoken.stp.StpInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义权限验证接口扩展 + */ +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-ssm/src/main/resources/application.yml new file mode 100644 index 000000000..d0e2718e4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/resources/applicationContext.xml b/sa-token-demo/sa-token-demo-ssm/src/main/resources/applicationContext.xml new file mode 100644 index 000000000..36145594a --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/resources/applicationContext.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-mvc.xml b/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-mvc.xml new file mode 100644 index 000000000..038332c0a --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-mvc.xml @@ -0,0 +1,59 @@ + + + + + + + + text/html;charset=UTF-8 + text/json;charset=UTF-8 + application/json;charset=UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-redis.xml b/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-redis.xml new file mode 100644 index 000000000..d2bac2cd0 --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-redis.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-sa-token.xml b/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-sa-token.xml new file mode 100644 index 000000000..306a54eaa --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-sa-token.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/admin.jsp b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/admin.jsp new file mode 100644 index 000000000..100a7e69c --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/admin.jsp @@ -0,0 +1,10 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Admin.jsp + + +

Admin.jsp

+

具有 admin 角色才可以访问

+ + diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/home.jsp b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/home.jsp new file mode 100644 index 000000000..8d3b705c4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/home.jsp @@ -0,0 +1,10 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Home.jsp + + +

Home.jsp

+

所有游客可访问

+ + diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/user.jsp b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/user.jsp new file mode 100644 index 000000000..81645ba1e --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/user.jsp @@ -0,0 +1,10 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + User.jsp + + +

User.jsp

+

登录后才可以访问

+ + diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/web.xml b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..1d316ffcf --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,50 @@ + + + yixiao2 + + + + contextConfigLocation + classpath:applicationContext.xml + + + org.springframework.web.context.ContextLoaderListener + + + + springmvc + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:spring-mvc.xml + + 1 + + + + springmvc + / + + + default + /static/* + + + + + 404 + /error + + + + + + index.jsp + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-ssm/src/main/webapp/index.jsp b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/index.jsp new file mode 100644 index 000000000..051d14463 --- /dev/null +++ b/sa-token-demo/sa-token-demo-ssm/src/main/webapp/index.jsp @@ -0,0 +1,10 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + 欢迎页 index.jsp + + +

欢迎页 index.jsp

+

这是个外置位的 jsp 页面,可以不经过 Controller 直接访问到

+ + 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 new file mode 100644 index 000000000..6a02e1e44 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso-server-solon + 0.0.1-SNAPSHOT + + + + org.noear + solon-parent + 3.2.1 + + + + + + 1.45.0 + + + + + + + org.noear + solon-web + ${solon.version} + + + + + cn.dev33 + sa-token-solon-plugin + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redisx + ${sa-token.version} + + + + + cn.dev33 + sa-token-snack3 + ${sa-token.version} + + + + + org.noear + snack3 + 3.2.133 + + + + + org.noear + solon.view.thymeleaf + ${solon.version} + + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + + + + + + + \ 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/SaConfig.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaConfig.java new file mode 100644 index 000000000..04017fc95 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaConfig.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisx; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * @author noear 2023/3/13 created + */ +@Configuration +public class SaConfig { + + /** + * 构建建 SaToken redis dao(如果不需要 redis;可以注释掉) + * */ + @Bean + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx saTokenDao) { + return saTokenDao; + } + +} 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 new file mode 100644 index 000000000..57e95726c --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaSsoServerApp.java @@ -0,0 +1,43 @@ +package com.pj; + + +import cn.dev33.satoken.sso.SaSsoManager; +import org.noear.solon.Solon; +import org.noear.solon.annotation.SolonMain; + +@SolonMain +public class SaSsoServerApp { + + public static void main(String[] args) { + Solon.start(SaSsoServerApp.class, args); + + System.out.println(); + System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------"); + System.out.println("配置信息:" + SaSsoManager.getServerConfig()); + System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth"); + System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); + System.out.println(); + } + + /* + * 类型序列化测试代码 + * + + System.out.println(SaManager.getSaJsonTemplate()); + + SaSsoClientInfo sci = new SaSsoClientInfo(); + sci.setClient("client1"); + + List list = new ArrayList<>(); + list.add(sci); + + StpUtil.getSessionByLoginId(10001).set("list", list); + + List list2 = (List)StpUtil.getSessionByLoginId(10001).get("list"); + for (SaSsoClientInfo info : list2) { + System.out.println(info); + } + + */ + +} \ 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/h5/H5Controller.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/H5Controller.java new file mode 100644 index 000000000..d5a0ce1c8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/H5Controller.java @@ -0,0 +1,57 @@ +package com.pj.h5; + + +import cn.dev33.satoken.sso.template.SaSsoUtil; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; + +/** + * 前后台分离架构下集成SSO所需的代码 (SSO-Server端) + *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

+ * @author click33 + * + */ +@Controller +public class H5Controller { + + /** + * 获取 redirectUrl + */ + @Mapping("/sso/getRedirectUrl") + public SaResult getRedirectUrl(String client, String redirect, String mode) { + // 未登录情况下,返回 code=401 + if(StpUtil.isLogin() == false) { + return SaResult.code(401); + } + // 已登录情况下,构建 redirectUrl + redirect = SaFoxUtil.decoderUrl(redirect); + if(SaSsoConsts.MODE_SIMPLE.equals(mode)) { + // 模式一 + SaSsoUtil.checkRedirectUrl(client, redirect); + return SaResult.data(redirect); + } else { + // 模式二或模式三 + String redirectUrl = SaSsoUtil.buildRedirectUrl(client, redirect, StpUtil.getLoginId(), StpUtil.getLoginDeviceId()); + return SaResult.data(redirectUrl); + } + } + +// /** +// * 控制当前类的异常 +// */ +// @Override +// public void render(Object data, Context ctx) throws Throwable { +// if (data instanceof Throwable) { +// Throwable e = (Throwable) data; +// e.printStackTrace(); +// ctx.render(SaResult.error(e.getMessage())); +// } else { +// ctx.render(data); +// } +// } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/SaTokenConfigure.java new file mode 100644 index 000000000..5bd7e5ed2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/SaTokenConfigure.java @@ -0,0 +1,40 @@ +package com.pj.h5; + +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 (解决跨域问题) + * + * @author click33 + */ +@Configuration +public class SaTokenConfigure { + + /** + * CORS 跨域处理策略 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java new file mode 100644 index 000000000..a10ec8179 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java @@ -0,0 +1,28 @@ +package com.pj.sso; + + +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.Component; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Filter; +import org.noear.solon.core.handle.FilterChain; + +/** + * 全局异常处理 + * @author click33 + * + */ +@Component +public class GlobalExceptionFilter implements Filter { + + @Override + public void doFilter(Context ctx, FilterChain chain) throws Throwable { + try { + chain.doFilter(ctx); + } catch (Exception e) { + e.printStackTrace(); + ctx.render(SaResult.error(e.getMessage())); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/HomeController.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/HomeController.java new file mode 100644 index 000000000..a3bc98d80 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/HomeController.java @@ -0,0 +1,36 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.stp.StpUtil; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; + +/** + * SSO 平台中心模式示例,跳连接进入子系统 + */ +@Controller +public class HomeController { + + // 平台化首页 + @Mapping("/home") + public Object index() { + // 如果未登录,则先去登录 + if(!StpUtil.isLogin()) { + return SaHolder.getResponse().redirect("/sso/auth"); + } + + // 拼接各个子系统的地址,格式形如:/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页} + String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/"; + String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/"; + String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/"; + + // 组织网页结构返回到前端 + String title = "

SSO 平台首页 (平台中心模式)

"; + String client1 = "

进入Client1系统

"; + String client2 = "

进入Client2系统

"; + String client3 = "

进入Client3系统

"; + + return title + client1 + client2 + client3; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoServerController.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoServerController.java new file mode 100644 index 000000000..244283372 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoServerController.java @@ -0,0 +1,71 @@ +package com.pj.sso; + + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; +import org.noear.solon.core.handle.ModelAndView; + +/** + * Sa-Token-SSO Server端 Controller + * @author click33 + * + */ +@Controller +@Configuration +public class SsoServerController { + + /** + * SSO-Server端:处理所有SSO相关请求 + * http://{host}:{port}/sso/auth -- 单点登录授权地址 + * http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd + * http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开) + */ + @Mapping("/sso/*") + public Object ssoRequest() { + return SaSsoServerProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Bean + private void configSso(SaSsoServerTemplate ssoServerTemplate) { + + // 配置:未登录时返回的View + ssoServerTemplate.strategy.notLoginView = () -> { + return new ModelAndView("sa-login.html"); + }; + + // 配置:登录处理函数 + ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { + // 此处仅做模拟登录,真实环境应该查询数据库进行登录 + if("sa".equals(name) && "123456".equals(pwd)) { + String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32)); + StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId)); + return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); + } + return SaResult.error("登录失败!"); + }; + + // 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口) + ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> { + System.out.println("收到消息:" + message); + + // 自定义返回结果(模拟) + return SaResult.ok() + .set("id", message.get("loginId")) + .set("name", "LinXiaoYu") + .set("sex", "女") + .set("age", 18); + }); + + } + +} diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/jquery.min.js b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/jquery.min.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/jquery.min.js rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/jquery.min.js diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/layer.js b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/layer.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/layer.js rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/layer.js diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/mobile/layer.js b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/mobile/layer.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/mobile/layer.js rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/mobile/layer.js diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/mobile/need/layer.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/mobile/need/layer.css diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/icon-ext.png similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/icon-ext.png diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/icon.png b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/icon.png similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/icon.png rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/icon.png diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/layer.css b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/layer.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/layer.css rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/layer.css diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/loading-0.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/loading-0.gif diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/loading-1.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/loading-1.gif diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/loading-2.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/loading-2.gif diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.css b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/login.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.css rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/login.css diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/login.js b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/login.js new file mode 100644 index 000000000..fb64ce92c --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/login.js @@ -0,0 +1,65 @@ +// sa +var sa = {}; + +// 打开loading +sa.loading = function(msg) { + layer.closeAll(); // 开始前先把所有弹窗关了 + return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); +}; + +// 隐藏loading +sa.hideLoading = function() { + layer.closeAll(); +}; + + +// ----------------------------------- 登录事件 ----------------------------------- + +$('.login-btn').click(function(){ + sa.loading("正在登录..."); + // 开始登录 + setTimeout(function() { + $.ajax({ + url: "sso/doLogin", + type: "post", + data: { + name: $('[name=name]').val(), + pwd: $('[name=pwd]').val() + }, + dataType: 'json', + success: function(res){ + console.log('返回数据:', res); + sa.hideLoading(); + if(res.code == 200) { + layer.msg('登录成功', {anim: 0, icon: 6 }); + setTimeout(function() { + location.reload(); + }, 800) + } else { + layer.msg(res.msg, {anim: 6, icon: 2 }); + } + }, + error: function(xhr, type, errorThrown){ + sa.hideLoading(); + if(xhr.status == 0){ + return layer.alert('无法连接到服务器,请检查网络'); + } + return layer.alert("异常:" + JSON.stringify(xhr)); + } + }); + }, 400); +}); + +// 绑定回车事件 +$('[name=name],[name=pwd]').bind('keypress', function(event){ + if(event.keyCode == "13") { + $('.login-btn').click(); + } +}); + +// 输入框获取焦点 +$("[name=name]").focus(); + +// 打印信息 +var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.com/"; +console.log(str); diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/templates/sa-login.html b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/view/sa-login.html similarity index 97% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/templates/sa-login.html rename to sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/view/sa-login.html index 82b50faa7..1d3ad0837 100644 --- a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/templates/sa-login.html +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/view/sa-login.html @@ -3,7 +3,7 @@ Sa-SSO-Server 认证中心-登录 - + 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 new file mode 100644 index 000000000..802823401 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/app.yml @@ -0,0 +1,60 @@ +# 端口 +server: + port: 9000 + +# Sa-Token 配置 +sa-token: + # 打印操作日志 + is-log: true + + # SSO 模式一配置 (非模式一不需要配置) +# cookie: +# # 配置 Cookie 作用域 +# domain: stp.com + + # SSO-Server 配置 + sso-server: + # Ticket有效期 (单位: 秒),默认五分钟 + ticket-timeout: 300 + # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 + home-route: /home + # 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) + allow-anon-client: true + # 所有允许的授权回调地址 (匿名 client 使用) + allow-url: "*" + # API 接口调用秘钥 (全局默认 + 匿名 client 使用) + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用列表:配置接入的应用信息 + clients: + # 应用 sso-client1:采用模式一对接 (同域、同Redis) + sso-client1: + client: sso-client1 + allowUrl: "*" + # 应用 sso-client2:采用模式二对接 (跨域、同Redis) + sso-client2: + client: sso-client2 + allowUrl: "*" + secretKey: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用 sso-client3:采用模式三对接 (跨域、跨Redis) + sso-client3: + # 应用名称 + client: sso-client3 + # 允许授权地址 + allowUrl: "*" + # 是否接收消息推送 + isPush: true + # 消息推送地址 + pushUrl: http://sa-sso-client1.com:9003/sso/pushC + # 接口调用秘钥 (如果不配置则使用全局默认秘钥) + secretKey: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +sa-token.dao: #名字可以随意取 + redis: + server: "localhost:6379" +# password: 123456 + db: 1 + maxTotal: 200 + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..66481ac57 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/pom.xml @@ -0,0 +1,68 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso1-client-solon + 0.0.1-SNAPSHOT + + + + org.noear + solon-parent + 3.2.1 + + + + + + 1.45.0 + + + + + + org.noear + solon-web + ${solon.version} + + + + + cn.dev33 + sa-token-solon-plugin + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redisx + ${sa-token.version} + + + + + cn.dev33 + sa-token-snack3 + ${sa-token.version} + + + + + org.noear + snack3 + 3.2.133 + + + + + + \ 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/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 new file mode 100644 index 000000000..6215ddb70 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaConfig.java @@ -0,0 +1,22 @@ +package com.pj; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisx; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * @author noear 2023/3/13 created + */ +@Configuration +public class SaConfig { + + /** + * 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) + * */ + @Bean + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx 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 new file mode 100644 index 000000000..cbab1afa5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaSso1ClientApp.java @@ -0,0 +1,30 @@ +package com.pj; + + +import cn.dev33.satoken.sso.SaSsoManager; +import org.noear.solon.Solon; +import org.noear.solon.annotation.SolonMain; + +/** + * SSO模式一,Client端 Demo + * @author click33 + * + */ +@SolonMain +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("---------------------- 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/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..d0d9bba5f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,49 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; +import org.noear.solon.annotation.Produces; +import org.noear.solon.boot.web.MimeType; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Render; + +/** + * Sa-Token-SSO Client端 Controller + * @author click33 + */ +@Controller +public class SsoClientController implements Render { + + // SSO-Client端:首页 + @Produces(MimeType.TEXT_HTML_VALUE) + @Mapping("/") + public String index() { + String url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), Context.current().queryString()) ); + SaSsoClientConfig cfg = SaSsoManager.getClientConfig(); + + String str = "

Sa-Token SSO-Client 应用端 (模式一)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单浏览器注销 - " + + "全端注销 " + + "

"; + return str; + } + + // 全局异常拦截并转换 + @Override + public void render(Object data, Context ctx) throws Throwable { + if(data instanceof Exception){ + data = SaResult.error(((Exception)data).getMessage()); + } + ctx.render(data); + } + +} 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 new file mode 100644 index 000000000..0ce72151e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/resources/app.yml @@ -0,0 +1,28 @@ +# 端口 +server: + port: 9001 + +# Sa-Token 配置 +sa-token: + # 打印操作日志 + is-log: true + + # SSO-相关配置 + sso-client: + # client 标识 + client: sso-client1 + # SSO-Server端 - 主机地址 + server-url: http://sso.stp.com:9000 + +# 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) +sa-token.dao: #名字可以随意取 + redis: + server: "localhost:6379" +# password: 123456 + db: 1 + maxTotal: 200 + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..9c9358d5f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/pom.xml @@ -0,0 +1,81 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso2-client-solon + 0.0.1-SNAPSHOT + + + + org.noear + solon-parent + 3.2.1 + + + + + + 1.45.0 + + + + + + + + org.noear + solon-web + ${solon.version} + + + + + cn.dev33 + sa-token-solon-plugin + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redisx + ${sa-token.version} + + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + + + + + cn.dev33 + sa-token-snack3 + ${sa-token.version} + + + + + org.noear + snack3 + 3.2.133 + + + + + + + \ 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/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 new file mode 100644 index 000000000..07b1ef50a --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaConfig.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisx; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * @author noear 2023/3/13 created + */ +@Configuration +public class SaConfig { + + /** + * 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) + * */ + @Bean + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx saTokenDao) { + return saTokenDao; + } + +} 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 new file mode 100644 index 000000000..501fd8a5a --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaSso2ClientApp.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.sso.SaSsoManager; +import org.noear.solon.Solon; +import org.noear.solon.annotation.SolonMain; + +@SolonMain +public class SaSso2ClientApp { + + public static void main(String[] args) { + Solon.start(SaSso2ClientApp.class, args); + + 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 new file mode 100644 index 000000000..9ca8f1db2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/H5Controller.java @@ -0,0 +1,45 @@ +package com.pj.h5; + +import cn.dev33.satoken.sso.model.SaCheckTicketResult; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; + +/** + * 前后台分离架构下集成SSO所需的代码 (SSO-Client端) + *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

+ * @author click33 + * + */ +@Controller +public class H5Controller { + + // 判断当前是否登录 + @Mapping("/sso/isLogin") + public Object isLogin() { + return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); + } + + // 返回SSO认证中心登录地址 + @Mapping("/sso/getSsoAuthUrl") + public SaResult getSsoAuthUrl(String clientLoginUrl) { + String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); + return SaResult.data(serverAuthUrl); + } + + // 根据 ticket 进行登录 + @Mapping("/sso/doLoginByTicket") + public SaResult doLoginByTicket(String ticket) { + SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); + StpUtil.login(ctr.loginId, new SaLoginParameter() + .setTimeout(ctr.remainTokenTimeout) + .setDeviceId(ctr.deviceId) + ); + 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/java/com/pj/h5/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java new file mode 100644 index 000000000..5bd7e5ed2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java @@ -0,0 +1,40 @@ +package com.pj.h5; + +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 (解决跨域问题) + * + * @author click33 + */ +@Configuration +public class SaTokenConfigure { + + /** + * CORS 跨域处理策略 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java new file mode 100644 index 000000000..a10ec8179 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java @@ -0,0 +1,28 @@ +package com.pj.sso; + + +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.Component; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Filter; +import org.noear.solon.core.handle.FilterChain; + +/** + * 全局异常处理 + * @author click33 + * + */ +@Component +public class GlobalExceptionFilter implements Filter { + + @Override + public void doFilter(Context ctx, FilterChain chain) throws Throwable { + try { + chain.doFilter(ctx); + } catch (Exception e) { + e.printStackTrace(); + ctx.render(SaResult.error(e.getMessage())); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..bec46241e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,83 @@ +package com.pj.sso; + + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.*; +import org.noear.solon.boot.web.MimeType; + +/** + * Sa-Token-SSO Client端 Controller + * @author click33 + */ +@Controller +@Configuration +public class SsoClientController { + + // 首页 + @Produces(MimeType.TEXT_HTML_VALUE) + @Mapping("/") + public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式二)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "单浏览器注销 - " + + "全端注销 - " + + "账号资料" + + "

"; + return str; + } + + /* + * SSO-Client端:处理所有SSO相关请求 + * http://{host}:{port}/sso/login -- Client 端登录地址 + * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) + * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 + */ + @Mapping("/sso/*") + public Object ssoRequest() { + return SaSsoClientProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Bean + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + } + + // 当前应用独自注销 (不退出其它应用) + @Mapping("/sso/logoutByAlone") + public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); + } + + // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 + @Mapping("/sso/myInfo") + public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 获取本地 loginId + Object loginId = StpUtil.getLoginId(); + + // 推送消息 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", loginId); + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; + } + +} 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 new file mode 100644 index 000000000..fbe940b02 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/resources/app.yml @@ -0,0 +1,32 @@ +# 端口 +server: + port: 9002 + +# sa-token配置 +sa-token: + # 打印操作日志 + is-log: true + + # SSO-相关配置 + sso-client: + # 应用标识 + client: sso-client2 + # SSO-Server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) + # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html + # API 接口调用秘钥 (单点注销时会用到) + secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) +sa-token.dao: #名字可以随意取 + redis: + server: "localhost:6379" +# password: 123456 + db: 1 + maxTotal: 200 + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..560cc6bbb --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/pom.xml @@ -0,0 +1,75 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso3-client-solon + 0.0.1-SNAPSHOT + + + + org.noear + solon-parent + 3.2.1 + + + + + + 1.45.0 + + + + + + org.noear + solon-web + ${solon.version} + + + + + cn.dev33 + sa-token-solon-plugin + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redisx + ${sa-token.version} + + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + + + + + cn.dev33 + sa-token-snack3 + ${sa-token.version} + + + + + org.noear + snack3 + 3.2.133 + + + + + + \ 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/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 new file mode 100644 index 000000000..511879e4e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaConfig.java @@ -0,0 +1,22 @@ +package com.pj; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisx; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * @author noear 2023/3/13 created + */ +@Configuration +public class SaConfig { + + /** + * 构建建 SaToken redis dao(如果不需要 redis;可以注释掉) + * */ + @Bean + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx 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 new file mode 100644 index 000000000..b152c52f6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaSso3ClientApp.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.sso.SaSsoManager; +import org.noear.solon.Solon; +import org.noear.solon.annotation.SolonMain; + +@SolonMain +public class SaSso3ClientApp { + + public static void main(String[] args) { + Solon.start(SaSso3ClientApp.class, args); + + 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/h5/H5Controller.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/H5Controller.java new file mode 100644 index 000000000..9ca8f1db2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/H5Controller.java @@ -0,0 +1,45 @@ +package com.pj.h5; + +import cn.dev33.satoken.sso.model.SaCheckTicketResult; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Mapping; + +/** + * 前后台分离架构下集成SSO所需的代码 (SSO-Client端) + *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

+ * @author click33 + * + */ +@Controller +public class H5Controller { + + // 判断当前是否登录 + @Mapping("/sso/isLogin") + public Object isLogin() { + return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); + } + + // 返回SSO认证中心登录地址 + @Mapping("/sso/getSsoAuthUrl") + public SaResult getSsoAuthUrl(String clientLoginUrl) { + String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); + return SaResult.data(serverAuthUrl); + } + + // 根据 ticket 进行登录 + @Mapping("/sso/doLoginByTicket") + public SaResult doLoginByTicket(String ticket) { + SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); + StpUtil.login(ctr.loginId, new SaLoginParameter() + .setTimeout(ctr.remainTokenTimeout) + .setDeviceId(ctr.deviceId) + ); + return SaResult.data(StpUtil.getTokenValue()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java new file mode 100644 index 000000000..5bd7e5ed2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java @@ -0,0 +1,40 @@ +package com.pj.h5; + +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 (解决跨域问题) + * + * @author click33 + */ +@Configuration +public class SaTokenConfigure { + + /** + * CORS 跨域处理策略 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java new file mode 100644 index 000000000..a10ec8179 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java @@ -0,0 +1,28 @@ +package com.pj.sso; + + +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.Component; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Filter; +import org.noear.solon.core.handle.FilterChain; + +/** + * 全局异常处理 + * @author click33 + * + */ +@Component +public class GlobalExceptionFilter implements Filter { + + @Override + public void doFilter(Context ctx, FilterChain chain) throws Throwable { + try { + chain.doFilter(ctx); + } catch (Exception e) { + e.printStackTrace(); + ctx.render(SaResult.error(e.getMessage())); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..32756028c --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,82 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.noear.solon.annotation.*; +import org.noear.solon.boot.web.MimeType; + +/** + * Sa-Token-SSO Client端 Controller + * @author click33 + */ +@Controller +@Configuration +public class SsoClientController { + + // SSO-Client端:首页 + @Produces(MimeType.TEXT_HTML_VALUE) + @Mapping("/") + public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "单浏览器注销 - " + + "全端注销 - " + + "账号资料" + + "

"; + return str; + } + + /* + * SSO-Client端:处理所有SSO相关请求 + * http://{host}:{port}/sso/login -- Client 端登录地址 + * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) + * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 + */ + @Mapping("/sso/*") + public Object ssoRequest() { + return SaSsoClientProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Bean + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + } + + // 当前应用独自注销 (不退出其它应用) + @Mapping("/sso/logoutByAlone") + public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); + } + + // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 + @Mapping("/sso/myInfo") + public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 获取本地 loginId + Object loginId = StpUtil.getLoginId(); + + // 推送消息 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", loginId); + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; + } + +} 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 new file mode 100644 index 000000000..670239241 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/resources/app.yml @@ -0,0 +1,31 @@ +# 端口 +server: + port: 9003 + +# sa-token配置 +sa-token: + # 打印操作日志 + is-log: true + + # sso-client 相关配置 + sso-client: + # 应用标识 + client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) + # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html + # 使用 Http 请求校验 ticket (模式三) + is-http: true + # API 接口调用秘钥 + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# 配置 Sa-Token Dao(此处与SSO-Server端连接不同的Redis) +sa-token.dao: #名字可以随意取 + redis: + server: "localhost:6379" +# password: 123456 + db: 4 + maxTotal: 200 + + \ No newline at end of file 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 new file mode 100644 index 000000000..1ce210476 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/index.html @@ -0,0 +1,67 @@ + + + + + Sa-Token-SSO-Client端-测试页(前后端分离版-原生h5) + + +

Sa-Token SSO-Client 应用端(前后端分离版-原生h5)

+

当前是否登录:

+

+ 登录 - + 单应用注销 - + 单浏览器注销 - + 全端注销 - + 账号资料 +

+ + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-common.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-common.js new file mode 100644 index 000000000..8dcb0a2b5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-common.js @@ -0,0 +1,60 @@ +// 服务器接口主机地址 +// var baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 +var baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 + +// 封装一下Ajax +function ajax(path, data, successFn, errorFn) { + console.log('发起请求:', baseUrl + path, JSON.stringify(data)); + fetch(baseUrl + path, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + 'satoken': localStorage.getItem('satoken') + }, + body: serializeToQueryString(data), + }) + .then(response => response.json()) + .then(res => { + console.log('返回数据:', res); + if(res.code === 500) { + return alert(res.msg); + } + successFn(res); + }) + .catch(error => { + console.error('请求失败:', error); + return alert("异常:" + JSON.stringify(error)); + }); +} + +// ------------ 工具方法 --------------- + +// 从url中查询到指定名称的参数值 +function getParam(name, defaultValue) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + if (pair[0] == name) { + return pair[1]; + } + } + return (defaultValue == undefined ? null : defaultValue); +} + +// 将 json 对象序列化为kv字符串,形如:name=Joh&age=30&active=true +function serializeToQueryString(obj) { + return Object.entries(obj) + .filter(([_, value]) => value != null) // 过滤 null 和 undefined + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); +} + +// 向指定标签里 set 内容 +function setHtml(select, html) { + const dom = document.querySelector('.is-login'); + if(dom) { + dom.innerHTML = html; + } +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-login.html b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-login.html new file mode 100644 index 000000000..8ac77614c --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-login.html @@ -0,0 +1,45 @@ + + + + + Sa-Token-SSO-Client端-登录中转页页 + + + + + + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/.gitignore b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/.gitignore new file mode 100644 index 000000000..11f5d7142 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/.gitignore @@ -0,0 +1,22 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/README.md b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/README.md new file mode 100644 index 000000000..d5de7da63 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/README.md @@ -0,0 +1,20 @@ +# sa-token-demo-sso-client-vue2 +Sa-Token SSO-Client 应用端(前后端分离版-Vue2) + +在线文档:[https://sa-token.com/](https://sa-token.com/) + +## 运行 +先安装依赖 +``` bat +npm install --registry=https://registry.npm.taobao.org +``` + +运行 +``` bat +npm run serve +``` + +打包 +``` bat +npm run build +``` diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/babel.config.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/babel.config.js new file mode 100644 index 000000000..e9558405f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/jsconfig.json b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/jsconfig.json new file mode 100644 index 000000000..4aafc5f6e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/jsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "baseUrl": "./", + "moduleResolution": "node", + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + } +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/package-lock.json b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/package-lock.json new file mode 100644 index 000000000..fcf6af584 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/package-lock.json @@ -0,0 +1,19558 @@ +{ + "name": "hello-world", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "hello-world", + "version": "0.1.0", + "dependencies": { + "axios": "^1.1.3", + "core-js": "^3.6.5", + "vue": "^2.6.11", + "vue-router": "^3.6.5" + }, + "devDependencies": { + "@babel/core": "^7.12.16", + "@babel/eslint-parser": "^7.12.16", + "@vue/cli-plugin-babel": "~5.0.0", + "@vue/cli-plugin-eslint": "~5.0.0", + "@vue/cli-service": "~5.0.0", + "eslint": "^7.32.0", + "eslint-plugin-vue": "^8.0.3" + } + }, + "node_modules/@achrinza/node-ipc": { + "version": "9.2.6", + "resolved": "https://registry.npmmirror.com/@achrinza/node-ipc/-/node-ipc-9.2.6.tgz", + "integrity": "sha512-ULSIYPy4ZPM301dfCxRz0l2GJjOwIo/PqmWonIu1bLml7UmnVQmH+juJcoyXp6E8gIRRNAjGYftJnNQlfy4vPg==", + "dev": true, + "dependencies": { + "@node-ipc/js-queue": "2.0.3", + "event-pubsub": "4.3.0", + "js-message": "1.0.7" + }, + "engines": { + "node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/eslint-parser/-/eslint-parser-7.22.5.tgz", + "integrity": "sha512-C69RWYNYtrgIRE5CmTd77ZiLDXqgBipahJc/jHP3sLcAGj6AJzxNIuKNpVnICqbyK7X3pFUfEvL++rvtbQpZkQ==", + "dev": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz", + "integrity": "sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", + "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", + "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", + "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", + "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", + "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.22.5.tgz", + "integrity": "sha512-h8hlezQ4dl6ixodgXkH8lUfcD7x+WAuIqPUjwGoItynrXOAv4a4Tci1zA/qjzQjjcl0v3QpLdc2LM6ZACQuY7A==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/plugin-syntax-decorators": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.5.tgz", + "integrity": "sha512-avpUOBS7IU6al8MmF1XpAyj9QYeLPuSDJI5D4pVMSMdL7xQokKqJPYQC67RCT0aCTashUXPiGwMJ0DEXXCEmMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz", + "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz", + "integrity": "sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", + "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.5.tgz", + "integrity": "sha512-bg4Wxd1FWeFx3daHFTWk1pkSWK/AyQuiyAoeZAOkAOUBjnZPH6KT7eMxouV47tQ6hl6ax2zyAWBdWZXbrvXlaw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.22.5.tgz", + "integrity": "sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.5", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.5", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "core-js-compat": "^3.30.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmmirror.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@node-ipc/js-queue": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@node-ipc/js-queue/-/js-queue-2.0.3.tgz", + "integrity": "sha512-fL1wpr8hhD5gT2dA1qifeVaoDFlQR5es8tFuKqjHX+kdOtdNHnxkVZbtIrR2rxnMFvehkjaZRNV2H/gPXlb0hw==", + "dev": true, + "dependencies": { + "easy-stack": "1.0.1" + }, + "engines": { + "node": ">=1.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, + "node_modules/@soda/friendly-errors-webpack-plugin": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz", + "integrity": "sha512-h2ooWqP8XuFqTXT+NyAFbrArzfQA7R6HTezADrvD9Re8fxMLTPPniLdqVTdDaO0eIoLaAwKT+d6w+5GeTk7Vbg==", + "dev": true, + "dependencies": { + "chalk": "^3.0.0", + "error-stack-parser": "^2.0.6", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@soda/get-current-script": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@soda/get-current-script/-/get-current-script-1.0.2.tgz", + "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", + "dev": true + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmmirror.com/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.40.2", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.40.2.tgz", + "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmmirror.com/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.3.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", + "dev": true + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "dev": true, + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmmirror.com/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vue/babel-helper-vue-jsx-merge-props": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", + "integrity": "sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==", + "dev": true + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz", + "integrity": "sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==", + "dev": true + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz", + "integrity": "sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "@vue/babel-helper-vue-transform-on": "^1.0.2", + "camelcase": "^6.0.0", + "html-tags": "^3.1.0", + "svg-tags": "^1.0.0" + } + }, + "node_modules/@vue/babel-plugin-transform-vue-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.4.0.tgz", + "integrity": "sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "html-tags": "^2.0.0", + "lodash.kebabcase": "^4.1.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-plugin-transform-vue-jsx/node_modules/html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vue/babel-preset-app": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/babel-preset-app/-/babel-preset-app-5.0.8.tgz", + "integrity": "sha512-yl+5qhpjd8e1G4cMXfORkkBlvtPCIgmRf3IYCWYDKIQ7m+PPa5iTm4feiNmCMD6yGqQWMhhK/7M3oWGL9boKwg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.16", + "@babel/helper-compilation-targets": "^7.12.16", + "@babel/helper-module-imports": "^7.12.13", + "@babel/plugin-proposal-class-properties": "^7.12.13", + "@babel/plugin-proposal-decorators": "^7.12.13", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/plugin-transform-runtime": "^7.12.15", + "@babel/preset-env": "^7.12.16", + "@babel/runtime": "^7.12.13", + "@vue/babel-plugin-jsx": "^1.0.3", + "@vue/babel-preset-jsx": "^1.1.2", + "babel-plugin-dynamic-import-node": "^2.3.3", + "core-js": "^3.8.3", + "core-js-compat": "^3.8.3", + "semver": "^7.3.4" + }, + "peerDependencies": { + "@babel/core": "*", + "core-js": "^3", + "vue": "^2 || ^3.2.13" + }, + "peerDependenciesMeta": { + "core-js": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/babel-preset-app/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vue/babel-preset-app/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vue/babel-preset-app/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@vue/babel-preset-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.4.0.tgz", + "integrity": "sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==", + "dev": true, + "dependencies": { + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "@vue/babel-sugar-composition-api-inject-h": "^1.4.0", + "@vue/babel-sugar-composition-api-render-instance": "^1.4.0", + "@vue/babel-sugar-functional-vue": "^1.4.0", + "@vue/babel-sugar-inject-h": "^1.4.0", + "@vue/babel-sugar-v-model": "^1.4.0", + "@vue/babel-sugar-v-on": "^1.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "vue": "*" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/babel-sugar-composition-api-inject-h": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.4.0.tgz", + "integrity": "sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-composition-api-render-instance": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.4.0.tgz", + "integrity": "sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-functional-vue": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.4.0.tgz", + "integrity": "sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-inject-h": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.4.0.tgz", + "integrity": "sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-v-model": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.4.0.tgz", + "integrity": "sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "camelcase": "^5.0.0", + "html-tags": "^2.0.0", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-v-model/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@vue/babel-sugar-v-model/node_modules/html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vue/babel-sugar-v-on": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.4.0.tgz", + "integrity": "sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "camelcase": "^5.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-v-on/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@vue/cli-overlay": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-overlay/-/cli-overlay-5.0.8.tgz", + "integrity": "sha512-KmtievE/B4kcXp6SuM2gzsnSd8WebkQpg3XaB6GmFh1BJGRqa1UiW9up7L/Q67uOdTigHxr5Ar2lZms4RcDjwQ==", + "dev": true + }, + "node_modules/@vue/cli-plugin-babel": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-plugin-babel/-/cli-plugin-babel-5.0.8.tgz", + "integrity": "sha512-a4qqkml3FAJ3auqB2kN2EMPocb/iu0ykeELwed+9B1c1nQ1HKgslKMHMPavYx3Cd/QAx2mBD4hwKBqZXEI/CsQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.16", + "@vue/babel-preset-app": "^5.0.8", + "@vue/cli-shared-utils": "^5.0.8", + "babel-loader": "^8.2.2", + "thread-loader": "^3.0.0", + "webpack": "^5.54.0" + }, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "node_modules/@vue/cli-plugin-eslint": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-5.0.8.tgz", + "integrity": "sha512-d11+I5ONYaAPW1KyZj9GlrV/E6HZePq5L5eAF5GgoVdu6sxr6bDgEoxzhcS1Pk2eh8rn1MxG/FyyR+eCBj/CNg==", + "dev": true, + "dependencies": { + "@vue/cli-shared-utils": "^5.0.8", + "eslint-webpack-plugin": "^3.1.0", + "globby": "^11.0.2", + "webpack": "^5.54.0", + "yorkie": "^2.0.0" + }, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0", + "eslint": ">=7.5.0" + } + }, + "node_modules/@vue/cli-plugin-router": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-plugin-router/-/cli-plugin-router-5.0.8.tgz", + "integrity": "sha512-Gmv4dsGdAsWPqVijz3Ux2OS2HkMrWi1ENj2cYL75nUeL+Xj5HEstSqdtfZ0b1q9NCce+BFB6QnHfTBXc/fCvMg==", + "dev": true, + "dependencies": { + "@vue/cli-shared-utils": "^5.0.8" + }, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "node_modules/@vue/cli-plugin-vuex": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-5.0.8.tgz", + "integrity": "sha512-HSYWPqrunRE5ZZs8kVwiY6oWcn95qf/OQabwLfprhdpFWAGtLStShjsGED2aDpSSeGAskQETrtR/5h7VqgIlBA==", + "dev": true, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "node_modules/@vue/cli-service": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-service/-/cli-service-5.0.8.tgz", + "integrity": "sha512-nV7tYQLe7YsTtzFrfOMIHc5N2hp5lHG2rpYr0aNja9rNljdgcPZLyQRb2YRivTHqTv7lI962UXFURcpStHgyFw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.12.16", + "@soda/friendly-errors-webpack-plugin": "^1.8.0", + "@soda/get-current-script": "^1.0.2", + "@types/minimist": "^1.2.0", + "@vue/cli-overlay": "^5.0.8", + "@vue/cli-plugin-router": "^5.0.8", + "@vue/cli-plugin-vuex": "^5.0.8", + "@vue/cli-shared-utils": "^5.0.8", + "@vue/component-compiler-utils": "^3.3.0", + "@vue/vue-loader-v15": "npm:vue-loader@^15.9.7", + "@vue/web-component-wrapper": "^1.3.0", + "acorn": "^8.0.5", + "acorn-walk": "^8.0.2", + "address": "^1.1.2", + "autoprefixer": "^10.2.4", + "browserslist": "^4.16.3", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "cli-highlight": "^2.1.10", + "clipboardy": "^2.3.0", + "cliui": "^7.0.4", + "copy-webpack-plugin": "^9.0.1", + "css-loader": "^6.5.0", + "css-minimizer-webpack-plugin": "^3.0.2", + "cssnano": "^5.0.0", + "debug": "^4.1.1", + "default-gateway": "^6.0.3", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "fs-extra": "^9.1.0", + "globby": "^11.0.2", + "hash-sum": "^2.0.0", + "html-webpack-plugin": "^5.1.0", + "is-file-esm": "^1.0.0", + "launch-editor-middleware": "^2.2.1", + "lodash.defaultsdeep": "^4.6.1", + "lodash.mapvalues": "^4.6.0", + "mini-css-extract-plugin": "^2.5.3", + "minimist": "^1.2.5", + "module-alias": "^2.2.2", + "portfinder": "^1.0.26", + "postcss": "^8.2.6", + "postcss-loader": "^6.1.1", + "progress-webpack-plugin": "^1.0.12", + "ssri": "^8.0.1", + "terser-webpack-plugin": "^5.1.1", + "thread-loader": "^3.0.0", + "vue-loader": "^17.0.0", + "vue-style-loader": "^4.1.3", + "webpack": "^5.54.0", + "webpack-bundle-analyzer": "^4.4.0", + "webpack-chain": "^6.5.1", + "webpack-dev-server": "^4.7.3", + "webpack-merge": "^5.7.3", + "webpack-virtual-modules": "^0.4.2", + "whatwg-fetch": "^3.6.2" + }, + "bin": { + "vue-cli-service": "bin/vue-cli-service.js" + }, + "engines": { + "node": "^12.0.0 || >= 14.0.0" + }, + "peerDependencies": { + "vue-template-compiler": "^2.0.0", + "webpack-sources": "*" + }, + "peerDependenciesMeta": { + "cache-loader": { + "optional": true + }, + "less-loader": { + "optional": true + }, + "pug-plain-loader": { + "optional": true + }, + "raw-loader": { + "optional": true + }, + "sass-loader": { + "optional": true + }, + "stylus-loader": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/@vue/cli-shared-utils": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz", + "integrity": "sha512-uK2YB7bBVuQhjOJF+O52P9yFMXeJVj7ozqJkwYE9PlMHL1LMHjtCYm4cSdOebuPzyP+/9p0BimM/OqxsevIopQ==", + "dev": true, + "dependencies": { + "@achrinza/node-ipc": "^9.2.5", + "chalk": "^4.1.2", + "execa": "^1.0.0", + "joi": "^17.4.0", + "launch-editor": "^2.2.1", + "lru-cache": "^6.0.0", + "node-fetch": "^2.6.7", + "open": "^8.0.2", + "ora": "^5.3.0", + "read-pkg": "^5.1.1", + "semver": "^7.3.4", + "strip-ansi": "^6.0.0" + } + }, + "node_modules/@vue/cli-shared-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@vue/cli-shared-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vue/cli-shared-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@vue/cli-shared-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@vue/cli-shared-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@vue/cli-shared-utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vue/cli-shared-utils/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vue/cli-shared-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@vue/cli-shared-utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@vue/compiler-sfc": { + "version": "2.7.14", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", + "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", + "dependencies": { + "@babel/parser": "^7.18.4", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/component-compiler-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", + "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", + "dev": true, + "dependencies": { + "consolidate": "^0.15.1", + "hash-sum": "^1.0.2", + "lru-cache": "^4.1.2", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.36", + "postcss-selector-parser": "^6.0.2", + "source-map": "~0.6.1", + "vue-template-es2015-compiler": "^1.9.0" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/@vue/component-compiler-utils/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/@vue/vue-loader-v15": { + "name": "vue-loader", + "version": "15.10.1", + "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.10.1.tgz", + "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", + "dev": true, + "dependencies": { + "@vue/component-compiler-utils": "^3.1.0", + "hash-sum": "^1.0.2", + "loader-utils": "^1.1.0", + "vue-hot-reload-api": "^2.3.0", + "vue-style-loader": "^4.1.0" + }, + "peerDependencies": { + "css-loader": "*", + "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "cache-loader": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/@vue/vue-loader-v15/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/@vue/web-component-wrapper": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz", + "integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmmirror.com/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", + "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.4.0", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", + "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.0", + "core-js-compat": "^3.30.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", + "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.9", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001505", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz", + "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==", + "dev": true + }, + "node_modules/case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "node_modules/clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmmirror.com/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cli-highlight/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "dev": true, + "dependencies": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmmirror.com/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmmirror.com/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", + "dev": true, + "dependencies": { + "bluebird": "^3.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/core-js": { + "version": "3.31.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.31.0.tgz", + "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==", + "hasInstallScript": true + }, + "node_modules/core-js-compat": { + "version": "3.31.0", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.31.0.tgz", + "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.5" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/css-declaration-sorter": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", + "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmmirror.com/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "dev": true, + "dependencies": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmmirror.com/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmmirror.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/default-gateway/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.434", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.434.tgz", + "integrity": "sha512-5Gvm09UZTQRaWrimRtWRO5rvaX6Kpk5WHAPKDa7A4Gj6NIPuJ8w8WNpnxCXdd+CJJt6RBU6tUw0KyULoW6XuHw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "8.7.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz", + "integrity": "sha512-28sbtm4l4cOzoO1LtzQPxfxhQABararUb1JtqusQqObJpWX2e/gmVyeYVfepizPFne0Q5cILkYGiBoV36L12Wg==", + "dev": true, + "dependencies": { + "eslint-utils": "^3.0.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.0.1", + "postcss-selector-parser": "^6.0.9", + "semver": "^7.3.5", + "vue-eslint-parser": "^8.0.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-plugin-vue/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "dev": true, + "dependencies": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/eslint-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/eslint-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmmirror.com/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.4.tgz", + "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmmirror.com/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/html-entities/-/html-entities-2.3.6.tgz", + "integrity": "sha512-9o0+dcpIw2/HxkNuYKxSJUF/MMRZQECK4GnF+oQOmJ83yCVHTWgCH5aOXxK5bozNRmM8wtgryjHD3uloPBDEGw==", + "dev": true + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", + "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmmirror.com/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmmirror.com/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "dependencies": { + "ci-info": "^1.5.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-file-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-file-esm/-/is-file-esm-1.0.0.tgz", + "integrity": "sha512-rZlaNKb4Mr8WlRu2A9XdeoKgnO5aA53XdPHgCKVyCrQ/rWi89RET1+bq37Ru46obaQXeiX4vmFIm1vks41hoSA==", + "dev": true, + "dependencies": { + "read-pkg-up": "^7.0.1" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/joi": { + "version": "17.9.2", + "resolved": "https://registry.npmmirror.com/joi/-/joi-17.9.2.tgz", + "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", + "dev": true, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "node_modules/launch-editor-middleware": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/launch-editor-middleware/-/launch-editor-middleware-2.6.0.tgz", + "integrity": "sha512-K2yxgljj5TdCeRN1lBtO3/J26+AIDDDw+04y6VAiZbWcTdBwsYN6RrZBnW5DN/QiSIdKNjKdATLUUluWWFYTIA==", + "dev": true, + "dependencies": { + "launch-editor": "^2.6.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmmirror.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-2.3.0.tgz", + "integrity": "sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^3.0.0", + "cli-cursor": "^2.0.0", + "wrap-ansi": "^3.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==", + "dev": true, + "dependencies": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmmirror.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/module-alias": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", + "dev": true + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmmirror.com/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmmirror.com/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmmirror.com/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmmirror.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-loader/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmmirror.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dev": true, + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dev": true, + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmmirror.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dev": true, + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/progress-webpack-plugin": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/progress-webpack-plugin/-/progress-webpack-plugin-1.0.16.tgz", + "integrity": "sha512-sdiHuuKOzELcBANHfrupYo+r99iPRyOnw15qX+rNlVUqXGfjXdH4IgxriKwG1kNJwVswKQHMdj1hYZMcb9jFaA==", + "dev": true, + "dependencies": { + "chalk": "^2.1.0", + "figures": "^2.0.0", + "log-update": "^2.3.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmmirror.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmmirror.com/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/table": { + "version": "6.8.1", + "resolved": "https://registry.npmmirror.com/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.18.1", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.18.1.tgz", + "integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thread-loader": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/thread-loader/-/thread-loader-3.0.4.tgz", + "integrity": "sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==", + "dev": true, + "dependencies": { + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.1.0", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" + } + }, + "node_modules/thread-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/thread-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.5.3.tgz", + "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vue": { + "version": "2.7.14", + "resolved": "https://registry.npmmirror.com/vue/-/vue-2.7.14.tgz", + "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", + "dependencies": { + "@vue/compiler-sfc": "2.7.14", + "csstype": "^3.1.0" + } + }, + "node_modules/vue-eslint-parser": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", + "integrity": "sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==", + "dev": true, + "dependencies": { + "debug": "^4.3.2", + "eslint-scope": "^7.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.0.0", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.5.2", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-eslint-parser/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/vue-hot-reload-api": { + "version": "2.3.4", + "resolved": "https://registry.npmmirror.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", + "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", + "dev": true + }, + "node_modules/vue-loader": { + "version": "17.2.2", + "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-17.2.2.tgz", + "integrity": "sha512-aqNvKJvnz2A/6VWeJZodAo8XLoAlVwBv+2Z6dama+LHsAF+P/xijQ+OfWrxIs0wcGSJduvdzvTuATzXbNKkpiw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "watchpack": "^2.4.0" + }, + "peerDependencies": { + "webpack": "^4.1.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/vue-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vue-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/vue-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/vue-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/vue-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vue-router": { + "version": "3.6.5", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-3.6.5.tgz", + "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==" + }, + "node_modules/vue-style-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz", + "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", + "dev": true, + "dependencies": { + "hash-sum": "^1.0.2", + "loader-utils": "^1.0.2" + } + }, + "node_modules/vue-style-loader/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/vue-template-es2015-compiler": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", + "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", + "dev": true + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmmirror.com/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/webpack": { + "version": "5.87.0", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.87.0.tgz", + "integrity": "sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz", + "integrity": "sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-chain": { + "version": "6.5.1", + "resolved": "https://registry.npmmirror.com/webpack-chain/-/webpack-chain-6.5.1.tgz", + "integrity": "sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==", + "dev": true, + "dependencies": { + "deepmerge": "^1.5.2", + "javascript-stringify": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmmirror.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.4.6", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz", + "integrity": "sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmmirror.com/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yorkie": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/yorkie/-/yorkie-2.0.0.tgz", + "integrity": "sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "execa": "^0.8.0", + "is-ci": "^1.0.10", + "normalize-path": "^1.0.0", + "strip-indent": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/yorkie/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/yorkie/node_modules/execa": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/execa/-/execa-0.8.0.tgz", + "integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==", + "dev": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/yorkie/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/yorkie/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/yorkie/node_modules/normalize-path": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-1.0.0.tgz", + "integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yorkie/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + }, + "dependencies": { + "@achrinza/node-ipc": { + "version": "9.2.6", + "resolved": "https://registry.npmmirror.com/@achrinza/node-ipc/-/node-ipc-9.2.6.tgz", + "integrity": "sha512-ULSIYPy4ZPM301dfCxRz0l2GJjOwIo/PqmWonIu1bLml7UmnVQmH+juJcoyXp6E8gIRRNAjGYftJnNQlfy4vPg==", + "dev": true, + "requires": { + "@node-ipc/js-queue": "2.0.3", + "event-pubsub": "4.3.0", + "js-message": "1.0.7" + } + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true + }, + "@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + } + }, + "@babel/eslint-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/eslint-parser/-/eslint-parser-7.22.5.tgz", + "integrity": "sha512-C69RWYNYtrgIRE5CmTd77ZiLDXqgBipahJc/jHP3sLcAGj6AJzxNIuKNpVnICqbyK7X3pFUfEvL++rvtbQpZkQ==", + "dev": true, + "requires": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + } + }, + "@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz", + "integrity": "sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", + "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.0" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", + "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", + "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-replace-supers": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", + "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", + "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==" + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.22.5.tgz", + "integrity": "sha512-h8hlezQ4dl6ixodgXkH8lUfcD7x+WAuIqPUjwGoItynrXOAv4a4Tci1zA/qjzQjjcl0v3QpLdc2LM6ZACQuY7A==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/plugin-syntax-decorators": "^7.22.5" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "requires": {} + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.5.tgz", + "integrity": "sha512-avpUOBS7IU6al8MmF1XpAyj9QYeLPuSDJI5D4pVMSMdL7xQokKqJPYQC67RCT0aCTashUXPiGwMJ0DEXXCEmMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz", + "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz", + "integrity": "sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", + "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.5.tgz", + "integrity": "sha512-bg4Wxd1FWeFx3daHFTWk1pkSWK/AyQuiyAoeZAOkAOUBjnZPH6KT7eMxouV47tQ6hl6ax2zyAWBdWZXbrvXlaw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "semver": "^6.3.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/preset-env": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.22.5.tgz", + "integrity": "sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.5", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.5", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "core-js-compat": "^3.30.2", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + } + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + } + } + }, + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmmirror.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "requires": { + "eslint-scope": "5.1.1" + } + }, + "@node-ipc/js-queue": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@node-ipc/js-queue/-/js-queue-2.0.3.tgz", + "integrity": "sha512-fL1wpr8hhD5gT2dA1qifeVaoDFlQR5es8tFuKqjHX+kdOtdNHnxkVZbtIrR2rxnMFvehkjaZRNV2H/gPXlb0hw==", + "dev": true, + "requires": { + "easy-stack": "1.0.1" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, + "@soda/friendly-errors-webpack-plugin": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz", + "integrity": "sha512-h2ooWqP8XuFqTXT+NyAFbrArzfQA7R6HTezADrvD9Re8fxMLTPPniLdqVTdDaO0eIoLaAwKT+d6w+5GeTk7Vbg==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "error-stack-parser": "^2.0.6", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@soda/get-current-script": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@soda/get-current-script/-/get-current-script-1.0.2.tgz", + "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", + "dev": true + }, + "@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmmirror.com/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/eslint": { + "version": "8.40.2", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.40.2.tgz", + "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmmirror.com/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true + }, + "@types/node": { + "version": "20.3.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", + "dev": true + }, + "@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmmirror.com/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@vue/babel-helper-vue-jsx-merge-props": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", + "integrity": "sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==", + "dev": true + }, + "@vue/babel-helper-vue-transform-on": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz", + "integrity": "sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==", + "dev": true + }, + "@vue/babel-plugin-jsx": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz", + "integrity": "sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "@vue/babel-helper-vue-transform-on": "^1.0.2", + "camelcase": "^6.0.0", + "html-tags": "^3.1.0", + "svg-tags": "^1.0.0" + } + }, + "@vue/babel-plugin-transform-vue-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.4.0.tgz", + "integrity": "sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "html-tags": "^2.0.0", + "lodash.kebabcase": "^4.1.1", + "svg-tags": "^1.0.0" + }, + "dependencies": { + "html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", + "dev": true + } + } + }, + "@vue/babel-preset-app": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/babel-preset-app/-/babel-preset-app-5.0.8.tgz", + "integrity": "sha512-yl+5qhpjd8e1G4cMXfORkkBlvtPCIgmRf3IYCWYDKIQ7m+PPa5iTm4feiNmCMD6yGqQWMhhK/7M3oWGL9boKwg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.16", + "@babel/helper-compilation-targets": "^7.12.16", + "@babel/helper-module-imports": "^7.12.13", + "@babel/plugin-proposal-class-properties": "^7.12.13", + "@babel/plugin-proposal-decorators": "^7.12.13", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/plugin-transform-runtime": "^7.12.15", + "@babel/preset-env": "^7.12.16", + "@babel/runtime": "^7.12.13", + "@vue/babel-plugin-jsx": "^1.0.3", + "@vue/babel-preset-jsx": "^1.1.2", + "babel-plugin-dynamic-import-node": "^2.3.3", + "core-js": "^3.8.3", + "core-js-compat": "^3.8.3", + "semver": "^7.3.4" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@vue/babel-preset-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.4.0.tgz", + "integrity": "sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==", + "dev": true, + "requires": { + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "@vue/babel-sugar-composition-api-inject-h": "^1.4.0", + "@vue/babel-sugar-composition-api-render-instance": "^1.4.0", + "@vue/babel-sugar-functional-vue": "^1.4.0", + "@vue/babel-sugar-inject-h": "^1.4.0", + "@vue/babel-sugar-v-model": "^1.4.0", + "@vue/babel-sugar-v-on": "^1.4.0" + } + }, + "@vue/babel-sugar-composition-api-inject-h": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.4.0.tgz", + "integrity": "sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-composition-api-render-instance": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.4.0.tgz", + "integrity": "sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-functional-vue": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.4.0.tgz", + "integrity": "sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-inject-h": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.4.0.tgz", + "integrity": "sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-v-model": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.4.0.tgz", + "integrity": "sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "camelcase": "^5.0.0", + "html-tags": "^2.0.0", + "svg-tags": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", + "dev": true + } + } + }, + "@vue/babel-sugar-v-on": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.4.0.tgz", + "integrity": "sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "camelcase": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "@vue/cli-overlay": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-overlay/-/cli-overlay-5.0.8.tgz", + "integrity": "sha512-KmtievE/B4kcXp6SuM2gzsnSd8WebkQpg3XaB6GmFh1BJGRqa1UiW9up7L/Q67uOdTigHxr5Ar2lZms4RcDjwQ==", + "dev": true + }, + "@vue/cli-plugin-babel": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-plugin-babel/-/cli-plugin-babel-5.0.8.tgz", + "integrity": "sha512-a4qqkml3FAJ3auqB2kN2EMPocb/iu0ykeELwed+9B1c1nQ1HKgslKMHMPavYx3Cd/QAx2mBD4hwKBqZXEI/CsQ==", + "dev": true, + "requires": { + "@babel/core": "^7.12.16", + "@vue/babel-preset-app": "^5.0.8", + "@vue/cli-shared-utils": "^5.0.8", + "babel-loader": "^8.2.2", + "thread-loader": "^3.0.0", + "webpack": "^5.54.0" + } + }, + "@vue/cli-plugin-eslint": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-5.0.8.tgz", + "integrity": "sha512-d11+I5ONYaAPW1KyZj9GlrV/E6HZePq5L5eAF5GgoVdu6sxr6bDgEoxzhcS1Pk2eh8rn1MxG/FyyR+eCBj/CNg==", + "dev": true, + "requires": { + "@vue/cli-shared-utils": "^5.0.8", + "eslint-webpack-plugin": "^3.1.0", + "globby": "^11.0.2", + "webpack": "^5.54.0", + "yorkie": "^2.0.0" + } + }, + "@vue/cli-plugin-router": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-plugin-router/-/cli-plugin-router-5.0.8.tgz", + "integrity": "sha512-Gmv4dsGdAsWPqVijz3Ux2OS2HkMrWi1ENj2cYL75nUeL+Xj5HEstSqdtfZ0b1q9NCce+BFB6QnHfTBXc/fCvMg==", + "dev": true, + "requires": { + "@vue/cli-shared-utils": "^5.0.8" + } + }, + "@vue/cli-plugin-vuex": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-5.0.8.tgz", + "integrity": "sha512-HSYWPqrunRE5ZZs8kVwiY6oWcn95qf/OQabwLfprhdpFWAGtLStShjsGED2aDpSSeGAskQETrtR/5h7VqgIlBA==", + "dev": true, + "requires": {} + }, + "@vue/cli-service": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-service/-/cli-service-5.0.8.tgz", + "integrity": "sha512-nV7tYQLe7YsTtzFrfOMIHc5N2hp5lHG2rpYr0aNja9rNljdgcPZLyQRb2YRivTHqTv7lI962UXFURcpStHgyFw==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.12.16", + "@soda/friendly-errors-webpack-plugin": "^1.8.0", + "@soda/get-current-script": "^1.0.2", + "@types/minimist": "^1.2.0", + "@vue/cli-overlay": "^5.0.8", + "@vue/cli-plugin-router": "^5.0.8", + "@vue/cli-plugin-vuex": "^5.0.8", + "@vue/cli-shared-utils": "^5.0.8", + "@vue/component-compiler-utils": "^3.3.0", + "@vue/vue-loader-v15": "npm:vue-loader@^15.9.7", + "@vue/web-component-wrapper": "^1.3.0", + "acorn": "^8.0.5", + "acorn-walk": "^8.0.2", + "address": "^1.1.2", + "autoprefixer": "^10.2.4", + "browserslist": "^4.16.3", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "cli-highlight": "^2.1.10", + "clipboardy": "^2.3.0", + "cliui": "^7.0.4", + "copy-webpack-plugin": "^9.0.1", + "css-loader": "^6.5.0", + "css-minimizer-webpack-plugin": "^3.0.2", + "cssnano": "^5.0.0", + "debug": "^4.1.1", + "default-gateway": "^6.0.3", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "fs-extra": "^9.1.0", + "globby": "^11.0.2", + "hash-sum": "^2.0.0", + "html-webpack-plugin": "^5.1.0", + "is-file-esm": "^1.0.0", + "launch-editor-middleware": "^2.2.1", + "lodash.defaultsdeep": "^4.6.1", + "lodash.mapvalues": "^4.6.0", + "mini-css-extract-plugin": "^2.5.3", + "minimist": "^1.2.5", + "module-alias": "^2.2.2", + "portfinder": "^1.0.26", + "postcss": "^8.2.6", + "postcss-loader": "^6.1.1", + "progress-webpack-plugin": "^1.0.12", + "ssri": "^8.0.1", + "terser-webpack-plugin": "^5.1.1", + "thread-loader": "^3.0.0", + "vue-loader": "^17.0.0", + "vue-style-loader": "^4.1.3", + "webpack": "^5.54.0", + "webpack-bundle-analyzer": "^4.4.0", + "webpack-chain": "^6.5.1", + "webpack-dev-server": "^4.7.3", + "webpack-merge": "^5.7.3", + "webpack-virtual-modules": "^0.4.2", + "whatwg-fetch": "^3.6.2" + } + }, + "@vue/cli-shared-utils": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz", + "integrity": "sha512-uK2YB7bBVuQhjOJF+O52P9yFMXeJVj7ozqJkwYE9PlMHL1LMHjtCYm4cSdOebuPzyP+/9p0BimM/OqxsevIopQ==", + "dev": true, + "requires": { + "@achrinza/node-ipc": "^9.2.5", + "chalk": "^4.1.2", + "execa": "^1.0.0", + "joi": "^17.4.0", + "launch-editor": "^2.2.1", + "lru-cache": "^6.0.0", + "node-fetch": "^2.6.7", + "open": "^8.0.2", + "ora": "^5.3.0", + "read-pkg": "^5.1.1", + "semver": "^7.3.4", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@vue/compiler-sfc": { + "version": "2.7.14", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", + "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", + "requires": { + "@babel/parser": "^7.18.4", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + } + }, + "@vue/component-compiler-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", + "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", + "dev": true, + "requires": { + "consolidate": "^0.15.1", + "hash-sum": "^1.0.2", + "lru-cache": "^4.1.2", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.36", + "postcss-selector-parser": "^6.0.2", + "prettier": "^1.18.2 || ^2.0.0", + "source-map": "~0.6.1", + "vue-template-es2015-compiler": "^1.9.0" + }, + "dependencies": { + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } + }, + "@vue/vue-loader-v15": { + "version": "npm:vue-loader@15.10.1", + "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.10.1.tgz", + "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", + "dev": true, + "requires": { + "@vue/component-compiler-utils": "^3.1.0", + "hash-sum": "^1.0.2", + "loader-utils": "^1.1.0", + "vue-hot-reload-api": "^2.3.0", + "vue-style-loader": "^4.1.0" + }, + "dependencies": { + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + } + } + }, + "@vue/web-component-wrapper": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz", + "integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "requires": {} + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "address": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arch": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmmirror.com/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "requires": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", + "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.4.0", + "semver": "^6.1.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", + "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.0", + "core-js-compat": "^3.30.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", + "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "requires": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.9", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001505", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz", + "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==", + "dev": true + }, + "case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmmirror.com/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true + }, + "clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "dev": true, + "requires": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "colord": { + "version": "2.9.3", + "resolved": "https://registry.npmmirror.com/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true + }, + "consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmmirror.com/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "dev": true, + "requires": { + "bluebird": "^3.1.1" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "copy-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==", + "dev": true, + "requires": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "core-js": { + "version": "3.31.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.31.0.tgz", + "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==" + }, + "core-js-compat": { + "version": "3.31.0", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.31.0.tgz", + "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==", + "dev": true, + "requires": { + "browserslist": "^4.21.5" + } + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "css-declaration-sorter": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", + "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", + "dev": true, + "requires": {} + }, + "css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmmirror.com/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "css-minimizer-webpack-plugin": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", + "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", + "dev": true, + "requires": { + "cssnano": "^5.0.6", + "jest-worker": "^27.0.2", + "postcss": "^8.3.5", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmmirror.com/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dev": true, + "requires": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + } + }, + "cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmmirror.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dev": true, + "requires": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + } + }, + "cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "dev": true, + "requires": {} + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "requires": { + "css-tree": "^1.1.2" + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true + }, + "default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "dns-packet": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "dev": true, + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.434", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.434.tgz", + "integrity": "sha512-5Gvm09UZTQRaWrimRtWRO5rvaX6Kpk5WHAPKDa7A4Gj6NIPuJ8w8WNpnxCXdd+CJJt6RBU6tUw0KyULoW6XuHw==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "requires": { + "stackframe": "^1.3.4" + } + }, + "es-module-lexer": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "eslint-plugin-vue": { + "version": "8.7.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz", + "integrity": "sha512-28sbtm4l4cOzoO1LtzQPxfxhQABararUb1JtqusQqObJpWX2e/gmVyeYVfepizPFne0Q5cILkYGiBoV36L12Wg==", + "dev": true, + "requires": { + "eslint-utils": "^3.0.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.0.1", + "postcss-selector-parser": "^6.0.9", + "semver": "^7.3.5", + "vue-eslint-parser": "^8.0.1" + }, + "dependencies": { + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "eslint-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "dev": true, + "requires": { + "@types/eslint": "^7.29.0 || ^8.4.1", + "jest-worker": "^28.0.2", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, + "event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmmirror.com/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-monkey": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.4.tgz", + "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "requires": { + "duplexer": "^0.1.2" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmmirror.com/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-entities": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/html-entities/-/html-entities-2.3.6.tgz", + "integrity": "sha512-9o0+dcpIw2/HxkNuYKxSJUF/MMRZQECK4GnF+oQOmJ83yCVHTWgCH5aOXxK5bozNRmM8wtgryjHD3uloPBDEGw==", + "dev": true + }, + "html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "requires": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + } + }, + "html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true + }, + "html-webpack-plugin": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", + "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", + "dev": true, + "requires": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + } + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmmirror.com/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmmirror.com/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "requires": {} + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + }, + "is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-file-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-file-esm/-/is-file-esm-1.0.0.tgz", + "integrity": "sha512-rZlaNKb4Mr8WlRu2A9XdeoKgnO5aA53XdPHgCKVyCrQ/rWi89RET1+bq37Ru46obaQXeiX4vmFIm1vks41hoSA==", + "dev": true, + "requires": { + "read-pkg-up": "^7.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "joi": { + "version": "17.9.2", + "resolved": "https://registry.npmmirror.com/joi/-/joi-17.9.2.tgz", + "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "klona": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true + }, + "launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dev": true, + "requires": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "launch-editor-middleware": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/launch-editor-middleware/-/launch-editor-middleware-2.6.0.tgz", + "integrity": "sha512-K2yxgljj5TdCeRN1lBtO3/J26+AIDDDw+04y6VAiZbWcTdBwsYN6RrZBnW5DN/QiSIdKNjKdATLUUluWWFYTIA==", + "dev": true, + "requires": { + "launch-editor": "^2.6.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmmirror.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "log-update": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-2.3.0.tgz", + "integrity": "sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "cli-cursor": "^2.0.0", + "wrap-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + } + } + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, + "memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "requires": { + "fs-monkey": "^1.0.4" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmmirror.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "dev": true, + "requires": { + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "module-alias": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", + "dev": true + }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmmirror.com/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "requires": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + } + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true + }, + "node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmmirror.com/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + }, + "dependencies": { + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + } + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "requires": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmmirror.com/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "dev": true, + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmmirror.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dev": true, + "requires": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "dev": true, + "requires": {} + }, + "postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "dev": true, + "requires": {} + }, + "postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "dev": true, + "requires": {} + }, + "postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "dev": true, + "requires": {} + }, + "postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dev": true, + "requires": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmmirror.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + } + }, + "postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dev": true, + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dev": true, + "requires": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmmirror.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dev": true, + "requires": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "dev": true, + "requires": {} + }, + "postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dev": true, + "requires": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dev": true, + "requires": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmmirror.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dev": true, + "requires": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dev": true, + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + } + }, + "postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "optional": true + }, + "pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "requires": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "progress-webpack-plugin": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/progress-webpack-plugin/-/progress-webpack-plugin-1.0.16.tgz", + "integrity": "sha512-sdiHuuKOzELcBANHfrupYo+r99iPRyOnw15qX+rNlVUqXGfjXdH4IgxriKwG1kNJwVswKQHMdj1hYZMcb9jFaA==", + "dev": true, + "requires": { + "chalk": "^2.1.0", + "figures": "^2.0.0", + "log-update": "^2.3.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + } + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + } + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmmirror.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true + }, + "renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "requires": { + "node-forge": "^1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmmirror.com/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dev": true, + "requires": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dev": true, + "requires": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + } + } + }, + "table": { + "version": "6.8.1", + "resolved": "https://registry.npmmirror.com/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "terser": { + "version": "5.18.1", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.18.1.tgz", + "integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "dependencies": { + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "thread-loader": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/thread-loader/-/thread-loader-3.0.4.tgz", + "integrity": "sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==", + "dev": true, + "requires": { + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.1.0", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "tslib": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.5.3.tgz", + "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, + "vue": { + "version": "2.7.14", + "resolved": "https://registry.npmmirror.com/vue/-/vue-2.7.14.tgz", + "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", + "requires": { + "@vue/compiler-sfc": "2.7.14", + "csstype": "^3.1.0" + } + }, + "vue-eslint-parser": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", + "integrity": "sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==", + "dev": true, + "requires": { + "debug": "^4.3.2", + "eslint-scope": "^7.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.0.0", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.5" + }, + "dependencies": { + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true + }, + "espree": { + "version": "9.5.2", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "vue-hot-reload-api": { + "version": "2.3.4", + "resolved": "https://registry.npmmirror.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", + "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", + "dev": true + }, + "vue-loader": { + "version": "17.2.2", + "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-17.2.2.tgz", + "integrity": "sha512-aqNvKJvnz2A/6VWeJZodAo8XLoAlVwBv+2Z6dama+LHsAF+P/xijQ+OfWrxIs0wcGSJduvdzvTuATzXbNKkpiw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "watchpack": "^2.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "vue-router": { + "version": "3.6.5", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-3.6.5.tgz", + "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==" + }, + "vue-style-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz", + "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", + "dev": true, + "requires": { + "hash-sum": "^1.0.2", + "loader-utils": "^1.0.2" + }, + "dependencies": { + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + } + } + }, + "vue-template-es2015-compiler": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", + "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", + "dev": true + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmmirror.com/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "webpack": { + "version": "5.87.0", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.87.0.tgz", + "integrity": "sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-bundle-analyzer": { + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz", + "integrity": "sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "webpack-chain": { + "version": "6.5.1", + "resolved": "https://registry.npmmirror.com/webpack-chain/-/webpack-chain-6.5.1.tgz", + "integrity": "sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==", + "dev": true, + "requires": { + "deepmerge": "^1.5.2", + "javascript-stringify": "^2.0.1" + } + }, + "webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } + }, + "webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmmirror.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "requires": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "requires": {} + } + } + }, + "webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "webpack-virtual-modules": { + "version": "0.4.6", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz", + "integrity": "sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==", + "dev": true + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmmirror.com/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + }, + "yorkie": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/yorkie/-/yorkie-2.0.0.tgz", + "integrity": "sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==", + "dev": true, + "requires": { + "execa": "^0.8.0", + "is-ci": "^1.0.10", + "normalize-path": "^1.0.0", + "strip-indent": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/execa/-/execa-0.8.0.tgz", + "integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "normalize-path": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-1.0.0.tgz", + "integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } + } + } +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/package.json b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/package.json new file mode 100644 index 000000000..4629083a1 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/package.json @@ -0,0 +1,45 @@ +{ + "name": "hello-world", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "axios": "^1.1.3", + "core-js": "^3.6.5", + "vue": "^2.6.11", + "vue-router": "^3.6.5" + }, + "devDependencies": { + "@babel/core": "^7.12.16", + "@babel/eslint-parser": "^7.12.16", + "@vue/cli-plugin-babel": "~5.0.0", + "@vue/cli-plugin-eslint": "~5.0.0", + "@vue/cli-service": "~5.0.0", + "eslint": "^7.32.0", + "eslint-plugin-vue": "^8.0.3" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "plugin:vue/vue3-essential", + "eslint:recommended" + ], + "parserOptions": { + "parser": "@babel/eslint-parser" + }, + "rules": {} + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead", + "not ie 11" + ] +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/public/favicon.ico b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/public/favicon.ico new file mode 100644 index 000000000..df36fcfb7 Binary files /dev/null and b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/public/favicon.ico differ diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/public/index.html b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/public/index.html new file mode 100644 index 000000000..58dff6403 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + Sa-Token SSO-Client 应用端(前后端分离版-Vue2) + + + +
+ + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/App.vue b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/App.vue new file mode 100644 index 000000000..c9b3a0e96 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/App.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/assets/logo.png b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/assets/logo.png new file mode 100644 index 000000000..f3d2503fc Binary files /dev/null and b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/assets/logo.png differ diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/main.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/main.js new file mode 100644 index 000000000..c4af74d77 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/main.js @@ -0,0 +1,11 @@ +import Vue from 'vue' +import App from './App.vue' + +Vue.config.productionTip = false + +import router from './router' + +new Vue({ + router, + render: h => h(App) +}).$mount('#app') diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/router/index.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/router/index.js new file mode 100644 index 000000000..a59af390d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/router/index.js @@ -0,0 +1,34 @@ +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +/** + * 路由表 + */ +export const routes = [ + // 首页 + { + name: 'index', + path: "/index", + component: () => import('../views/sso-index.vue') + }, + // SSO-登录页 + { + name: 'sso-login', + path: '/sso-login', + component: () => import('../views/sso-login.vue') + }, + + // 访问 / 时自动重定向到 /index + { + path: '/', + redirect: '/index' + } +] + +const router = new Router({ + routes: routes +}) + +export default router diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-common.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-common.js new file mode 100644 index 000000000..74da3dd02 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-common.js @@ -0,0 +1,43 @@ +import axios from 'axios' + +// sso-client 的后端服务地址 +// export const baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 +export const baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 + +// 封装一下 Ajax 方法 +export const ajax = function(path, data, successFn) { + console.log('发起请求:', baseUrl + path, JSON.stringify(data)); + axios({ + url: baseUrl + path, + method: 'post', + data: data, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "satoken": localStorage.getItem("satoken") + } + }). + then(function (response) { // 成功时执行 + const res = response.data; + console.log('返回数据:', res); + if(res.code === 500) { + return alert(res.msg); + } + successFn(res); + }). + catch(function (error) { + console.error('请求失败:', error); + return alert("异常:" + JSON.stringify(error)); + }) +} + +// 从url中查询到指定名称的参数值 +export const getParam = function(name, defaultValue){ + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-login.vue b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-login.vue new file mode 100644 index 000000000..096660566 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-login.vue @@ -0,0 +1,52 @@ + + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/vue.config.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/vue.config.js new file mode 100644 index 000000000..910e297e0 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/vue.config.js @@ -0,0 +1,4 @@ +const { defineConfig } = require('@vue/cli-service') +module.exports = defineConfig({ + transpileDependencies: true +}) diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/.gitignore b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/README.md b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/README.md new file mode 100644 index 000000000..d639d3d03 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/README.md @@ -0,0 +1,21 @@ +# sa-token-demo-sso-client-vue3 +Sa-Token SSO-Client 应用端(前后端分离版-Vue3) + +在线文档:[https://sa-token.com/](https://sa-token.com/) + + +## 运行 +先安装依赖 +``` bat +npm install --registry=https://registry.npm.taobao.org +``` + +运行 +``` bat +npm run dev +``` + +打包 +``` bat +npm run build +``` diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/index.html b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/index.html new file mode 100644 index 000000000..de926f9ff --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/index.html @@ -0,0 +1,13 @@ + + + + + + + Sa-Token SSO-Client 应用端(前后端分离版-Vue3) + + +
+ + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/package-lock.json b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/package-lock.json new file mode 100644 index 000000000..c45798ad6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/package-lock.json @@ -0,0 +1,1362 @@ +{ + "name": "sa-token-demo-sso-client-vue3", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "sa-token-demo-sso-client-vue3", + "version": "0.0.0", + "dependencies": { + "axios": "^1.1.3", + "vue": "^3.2.41", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^3.2.0", + "vite": "^3.2.7" + } + }, + "node_modules/@babel/parser": { + "version": "7.19.6", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.19.6.tgz", + "integrity": "sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.15.12.tgz", + "integrity": "sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz", + "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz", + "integrity": "sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^3.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.41.tgz", + "integrity": "sha512-oA4mH6SA78DT+96/nsi4p9DX97PHcNROxs51lYk7gb9Z4BPKQ3Mh+BLn6CQZBw857Iuhu28BfMSRHAlPvD4vlw==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/shared": "3.2.41", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz", + "integrity": "sha512-xe5TbbIsonjENxJsYRbDJvthzqxLNk+tb3d/c47zgREDa/PCp6/Y4gC/skM4H6PIuX5DAxm7fFJdbjjUH2QTMw==", + "dependencies": { + "@vue/compiler-core": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz", + "integrity": "sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.41", + "@vue/compiler-dom": "3.2.41", + "@vue/compiler-ssr": "3.2.41", + "@vue/reactivity-transform": "3.2.41", + "@vue/shared": "3.2.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7", + "postcss": "^8.1.10", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.41.tgz", + "integrity": "sha512-Y5wPiNIiaMz/sps8+DmhaKfDm1xgj6GrH99z4gq2LQenfVQcYXmHIOBcs5qPwl7jaW3SUQWjkAPKMfQemEQZwQ==", + "dependencies": { + "@vue/compiler-dom": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.4.5", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz", + "integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==" + }, + "node_modules/@vue/reactivity": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.41.tgz", + "integrity": "sha512-9JvCnlj8uc5xRiQGZ28MKGjuCoPhhTwcoAdv3o31+cfGgonwdPNuvqAXLhlzu4zwqavFEG5tvaoINQEfxz+l6g==", + "dependencies": { + "@vue/shared": "3.2.41" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.41.tgz", + "integrity": "sha512-mK5+BNMsL4hHi+IR3Ft/ho6Za+L3FA5j8WvreJ7XzHrqkPq8jtF/SMo7tuc9gHjLDwKZX1nP1JQOKo9IEAn54A==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.41", + "@vue/shared": "3.2.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.41.tgz", + "integrity": "sha512-0LBBRwqnI0p4FgIkO9q2aJBBTKDSjzhnxrxHYengkAF6dMOjeAIZFDADAlcf2h3GDALWnblbeprYYpItiulSVQ==", + "dependencies": { + "@vue/reactivity": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.41.tgz", + "integrity": "sha512-U7zYuR1NVIP8BL6jmOqmapRAHovEFp7CSw4pR2FacqewXNGqZaRfHoNLQsqQvVQ8yuZNZtxSZy0FFyC70YXPpA==", + "dependencies": { + "@vue/runtime-core": "3.2.41", + "@vue/shared": "3.2.41", + "csstype": "^2.6.8" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.41.tgz", + "integrity": "sha512-7YHLkfJdTlsZTV0ae5sPwl9Gn/EGr2hrlbcS/8naXm2CDpnKUwC68i1wGlrYAfIgYWL7vUZwk2GkYLQH5CvFig==", + "dependencies": { + "@vue/compiler-ssr": "3.2.41", + "@vue/shared": "3.2.41" + }, + "peerDependencies": { + "vue": "3.2.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.41.tgz", + "integrity": "sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.15.12.tgz", + "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.12", + "@esbuild/linux-loong64": "0.15.12", + "esbuild-android-64": "0.15.12", + "esbuild-android-arm64": "0.15.12", + "esbuild-darwin-64": "0.15.12", + "esbuild-darwin-arm64": "0.15.12", + "esbuild-freebsd-64": "0.15.12", + "esbuild-freebsd-arm64": "0.15.12", + "esbuild-linux-32": "0.15.12", + "esbuild-linux-64": "0.15.12", + "esbuild-linux-arm": "0.15.12", + "esbuild-linux-arm64": "0.15.12", + "esbuild-linux-mips64le": "0.15.12", + "esbuild-linux-ppc64le": "0.15.12", + "esbuild-linux-riscv64": "0.15.12", + "esbuild-linux-s390x": "0.15.12", + "esbuild-netbsd-64": "0.15.12", + "esbuild-openbsd-64": "0.15.12", + "esbuild-sunos-64": "0.15.12", + "esbuild-windows-32": "0.15.12", + "esbuild-windows-64": "0.15.12", + "esbuild-windows-arm64": "0.15.12" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz", + "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz", + "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz", + "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz", + "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz", + "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz", + "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz", + "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz", + "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz", + "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz", + "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz", + "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz", + "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz", + "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz", + "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz", + "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz", + "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz", + "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz", + "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz", + "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz", + "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/postcss": { + "version": "8.4.18", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.18.tgz", + "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/vite": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/vite/-/vite-3.2.7.tgz", + "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==", + "dev": true, + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.41.tgz", + "integrity": "sha512-uuuvnrDXEeZ9VUPljgHkqB5IaVO8SxhPpqF2eWOukVrBnRBx2THPSGQBnVRt0GrIG1gvCmFXMGbd7FqcT1ixNQ==", + "dependencies": { + "@vue/compiler-dom": "3.2.41", + "@vue/compiler-sfc": "3.2.41", + "@vue/runtime-dom": "3.2.41", + "@vue/server-renderer": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "node_modules/vue-router": { + "version": "4.1.6", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz", + "integrity": "sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==", + "dependencies": { + "@vue/devtools-api": "^6.4.5" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + } + }, + "dependencies": { + "@babel/parser": { + "version": "7.19.6", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.19.6.tgz", + "integrity": "sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA==" + }, + "@esbuild/android-arm": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.15.12.tgz", + "integrity": "sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz", + "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==", + "dev": true, + "optional": true + }, + "@vitejs/plugin-vue": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz", + "integrity": "sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==", + "dev": true, + "requires": {} + }, + "@vue/compiler-core": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.41.tgz", + "integrity": "sha512-oA4mH6SA78DT+96/nsi4p9DX97PHcNROxs51lYk7gb9Z4BPKQ3Mh+BLn6CQZBw857Iuhu28BfMSRHAlPvD4vlw==", + "requires": { + "@babel/parser": "^7.16.4", + "@vue/shared": "3.2.41", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "@vue/compiler-dom": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz", + "integrity": "sha512-xe5TbbIsonjENxJsYRbDJvthzqxLNk+tb3d/c47zgREDa/PCp6/Y4gC/skM4H6PIuX5DAxm7fFJdbjjUH2QTMw==", + "requires": { + "@vue/compiler-core": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "@vue/compiler-sfc": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz", + "integrity": "sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w==", + "requires": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.41", + "@vue/compiler-dom": "3.2.41", + "@vue/compiler-ssr": "3.2.41", + "@vue/reactivity-transform": "3.2.41", + "@vue/shared": "3.2.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7", + "postcss": "^8.1.10", + "source-map": "^0.6.1" + } + }, + "@vue/compiler-ssr": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.41.tgz", + "integrity": "sha512-Y5wPiNIiaMz/sps8+DmhaKfDm1xgj6GrH99z4gq2LQenfVQcYXmHIOBcs5qPwl7jaW3SUQWjkAPKMfQemEQZwQ==", + "requires": { + "@vue/compiler-dom": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "@vue/devtools-api": { + "version": "6.4.5", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz", + "integrity": "sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==" + }, + "@vue/reactivity": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.41.tgz", + "integrity": "sha512-9JvCnlj8uc5xRiQGZ28MKGjuCoPhhTwcoAdv3o31+cfGgonwdPNuvqAXLhlzu4zwqavFEG5tvaoINQEfxz+l6g==", + "requires": { + "@vue/shared": "3.2.41" + } + }, + "@vue/reactivity-transform": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.41.tgz", + "integrity": "sha512-mK5+BNMsL4hHi+IR3Ft/ho6Za+L3FA5j8WvreJ7XzHrqkPq8jtF/SMo7tuc9gHjLDwKZX1nP1JQOKo9IEAn54A==", + "requires": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.41", + "@vue/shared": "3.2.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7" + } + }, + "@vue/runtime-core": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.41.tgz", + "integrity": "sha512-0LBBRwqnI0p4FgIkO9q2aJBBTKDSjzhnxrxHYengkAF6dMOjeAIZFDADAlcf2h3GDALWnblbeprYYpItiulSVQ==", + "requires": { + "@vue/reactivity": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "@vue/runtime-dom": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.41.tgz", + "integrity": "sha512-U7zYuR1NVIP8BL6jmOqmapRAHovEFp7CSw4pR2FacqewXNGqZaRfHoNLQsqQvVQ8yuZNZtxSZy0FFyC70YXPpA==", + "requires": { + "@vue/runtime-core": "3.2.41", + "@vue/shared": "3.2.41", + "csstype": "^2.6.8" + } + }, + "@vue/server-renderer": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.41.tgz", + "integrity": "sha512-7YHLkfJdTlsZTV0ae5sPwl9Gn/EGr2hrlbcS/8naXm2CDpnKUwC68i1wGlrYAfIgYWL7vUZwk2GkYLQH5CvFig==", + "requires": { + "@vue/compiler-ssr": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "@vue/shared": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.41.tgz", + "integrity": "sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "esbuild": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.15.12.tgz", + "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.15.12", + "@esbuild/linux-loong64": "0.15.12", + "esbuild-android-64": "0.15.12", + "esbuild-android-arm64": "0.15.12", + "esbuild-darwin-64": "0.15.12", + "esbuild-darwin-arm64": "0.15.12", + "esbuild-freebsd-64": "0.15.12", + "esbuild-freebsd-arm64": "0.15.12", + "esbuild-linux-32": "0.15.12", + "esbuild-linux-64": "0.15.12", + "esbuild-linux-arm": "0.15.12", + "esbuild-linux-arm64": "0.15.12", + "esbuild-linux-mips64le": "0.15.12", + "esbuild-linux-ppc64le": "0.15.12", + "esbuild-linux-riscv64": "0.15.12", + "esbuild-linux-s390x": "0.15.12", + "esbuild-netbsd-64": "0.15.12", + "esbuild-openbsd-64": "0.15.12", + "esbuild-sunos-64": "0.15.12", + "esbuild-windows-32": "0.15.12", + "esbuild-windows-64": "0.15.12", + "esbuild-windows-arm64": "0.15.12" + } + }, + "esbuild-android-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz", + "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz", + "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz", + "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz", + "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz", + "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz", + "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz", + "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz", + "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz", + "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz", + "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz", + "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz", + "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz", + "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz", + "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz", + "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz", + "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz", + "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz", + "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz", + "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.12", + "resolved": "https://registry.npmmirror.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz", + "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==", + "dev": true, + "optional": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "postcss": { + "version": "8.4.18", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.18.tgz", + "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "vite": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/vite/-/vite-3.2.7.tgz", + "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==", + "dev": true, + "requires": { + "esbuild": "^0.15.9", + "fsevents": "~2.3.2", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + } + }, + "vue": { + "version": "3.2.41", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.41.tgz", + "integrity": "sha512-uuuvnrDXEeZ9VUPljgHkqB5IaVO8SxhPpqF2eWOukVrBnRBx2THPSGQBnVRt0GrIG1gvCmFXMGbd7FqcT1ixNQ==", + "requires": { + "@vue/compiler-dom": "3.2.41", + "@vue/compiler-sfc": "3.2.41", + "@vue/runtime-dom": "3.2.41", + "@vue/server-renderer": "3.2.41", + "@vue/shared": "3.2.41" + } + }, + "vue-router": { + "version": "4.1.6", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz", + "integrity": "sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==", + "requires": { + "@vue/devtools-api": "^6.4.5" + } + } + } +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/package.json b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/package.json new file mode 100644 index 000000000..557d53a9d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/package.json @@ -0,0 +1,20 @@ +{ + "name": "sa-token-demo-sso-client-vue3", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.1.3", + "vue": "^3.2.41", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^3.2.0", + "vite": "^3.2.7" + } +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/public/vite.svg b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/App.vue b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/App.vue new file mode 100644 index 000000000..511566217 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/App.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/assets/vue.svg b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/assets/vue.svg new file mode 100644 index 000000000..770e9d333 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/main.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/main.js new file mode 100644 index 000000000..660c82791 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/main.js @@ -0,0 +1,14 @@ +import { createApp } from 'vue' +import App from './App.vue' + +// createApp +const app = createApp(App); + +// 安装 vue-router +import router from './router'; +app.use(router); + + + +// 绑定dom +app.mount('#app'); diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/router/index.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/router/index.js new file mode 100644 index 000000000..dcea34c0e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/router/index.js @@ -0,0 +1,31 @@ +import { createRouter, createWebHistory } from 'vue-router'; + +/** + * 创建 vue-router 实例 + */ +const router = createRouter({ + history: createWebHistory(), + routes: [ + // 首页 + { + name: 'index', + path: "/index", + component: () => import('../views/sso-index.vue'), + }, + // SSO-登录页 + { + name: 'sso-login', + path: '/sso-login', + component: () => import('../views/sso-login.vue'), + }, + + // 访问 / 时自动重定向到 /index + { + path: "/", + redirect: '/index' + } + ], +}); + +// 导出 +export default router; diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-common.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-common.js new file mode 100644 index 000000000..74da3dd02 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-common.js @@ -0,0 +1,43 @@ +import axios from 'axios' + +// sso-client 的后端服务地址 +// export const baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 +export const baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 + +// 封装一下 Ajax 方法 +export const ajax = function(path, data, successFn) { + console.log('发起请求:', baseUrl + path, JSON.stringify(data)); + axios({ + url: baseUrl + path, + method: 'post', + data: data, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "satoken": localStorage.getItem("satoken") + } + }). + then(function (response) { // 成功时执行 + const res = response.data; + console.log('返回数据:', res); + if(res.code === 500) { + return alert(res.msg); + } + successFn(res); + }). + catch(function (error) { + console.error('请求失败:', error); + return alert("异常:" + JSON.stringify(error)); + }) +} + +// 从url中查询到指定名称的参数值 +export const getParam = function(name, defaultValue){ + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-login.vue b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-login.vue new file mode 100644 index 000000000..f8d7aea1d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-login.vue @@ -0,0 +1,50 @@ + + + + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/vite.config.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/vite.config.js new file mode 100644 index 000000000..315212d69 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()] +}) diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/common.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/common.js new file mode 100644 index 000000000..d0ff6617d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/common.js @@ -0,0 +1,40 @@ +// 服务端地址 +var baseUrl = "http://sa-sso-server.com:9000"; + +// sa +var sa = {}; + +// 打开loading +sa.loading = function(msg) { + layer.closeAll(); // 开始前先把所有弹窗关了 + return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'}); +}; + +// 隐藏loading +sa.hideLoading = function() { + layer.closeAll(); +}; + +// 封装一下Ajax +sa.ajax = function(url, data, successFn) { + $.ajax({ + url: baseUrl + url, + type: "post", + data: data, + dataType: 'json', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'satoken': localStorage.getItem('satoken') + }, + success: function(res){ + console.log('返回数据:', res); + successFn(res); + }, + error: function(xhr, type, errorThrown){ + if(xhr.status == 0){ + return alert('无法连接到服务器,请检查网络'); + } + return alert("异常:" + JSON.stringify(xhr)); + } + }); +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/home.html b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/home.html new file mode 100644 index 000000000..4e499ba6d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/home.html @@ -0,0 +1,40 @@ + + + + + SSO-Server 平台首页 + + +

SSO-Server 平台首页 (前后端分离模式) (平台中心模式)

+

+ 进入Client1系统 +

+

+ 进入Client2系统 +

+

+ 进入Client3系统 +

+ + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.css b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.css new file mode 100644 index 000000000..d23be7686 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.css @@ -0,0 +1,41 @@ +*{margin: 0; padding: 0;} +body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;} +::-webkit-input-placeholder{color: #ccc;} + +/* 视图盒子 */ +.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;} +/* 背景 EAEFF3 */ +.bg-1{height: 100%; background: #c0cCf4;} + +/* 内容盒子 */ +.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;} + +/* 登录盒子 */ +/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */ +.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;} +.login-box{display: flex; align-items: center; text-align: center;} + +/* 表单 */ +.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;} +.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;} +.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;} + +/* 输入框 */ +.from-item{border: 0px #000 solid; margin-bottom: 15px;} +.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;} +.s-input{font-size: 12px;} +.s-input:focus{border-color: #409eff} + +/* 登录按钮 */ +.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;} +.s-btn:hover{background-color: #50aEFF;} + +/* 重置按钮 */ +.reset-box{text-align: left; font-size: 12px;} +.reset-box a{text-decoration: none;} +.reset-box a:hover{text-decoration: underline;} + +/* loading框样式 */ +.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} +.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} +.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.html b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.html new file mode 100644 index 000000000..1f513b176 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.html @@ -0,0 +1,44 @@ + + + + Sa-SSO-Server 认证中心-登录 + + + + + +
+
+
+ +
+ +
+ This page is provided by Sa-Token-SSO +
+
+ + + + + + + + + diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.js new file mode 100644 index 000000000..5f16dc615 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.js @@ -0,0 +1,80 @@ + +// ----------------------------------- 相关事件 ----------------------------------- + +// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码 +var pData = { + client: getParam('client', ''), + redirect: getParam('redirect', ''), + mode: getParam('mode', '') +}; +// 提供 redirect 参数时,登录后往 redirect 跳转 +if(pData.redirect) { + sa.ajax("/sso/getRedirectUrl", pData, function(res) { + if(res.code == 200) { + // 已登录,并且redirect地址有效,开始跳转 + location.href = res.data; + } else if(res.code == 401) { + console.log('未登录'); + } else { + layer.alert(res.msg); + } + }) +} else { + // 未提供 redirect 参数时,登录后往 home 跳转 + sa.ajax("/sso/isLogin", {}, function(res) { + if(res.data) { + location.href = './home.html'; + } else { + console.log('未登录,请先登录...'); + } + }) +} + + +// 登录 +$('.login-btn').click(function(){ + sa.loading("正在登录..."); + // 开始登录 + var data = { + name: $('[name=name]').val(), + pwd: $('[name=pwd]').val() + }; + sa.ajax("/sso/doLogin", data, function(res) { + sa.hideLoading(); + if(res.code == 200) { + localStorage.setItem('satoken', res.data); + layer.msg('登录成功', {anim: 0, icon: 6 }); + setTimeout(function() { + location.reload(); + }, 800); + } else { + layer.msg(res.msg, {anim: 6, icon: 2 }); + } + }) +}); + + +// 绑定回车事件 +$('[name=name],[name=pwd]').bind('keypress', function(event){ + if(event.keyCode == "13") { + $('.login-btn').click(); + } +}); + +// 输入框获取焦点 +$("[name=name]").focus(); + +// 从url中查询到指定名称的参数值 +function getParam(name, defaultValue){ + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i + 4.0.0 + cn.dev33 + sa-token-demo-sso-server + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/SaSsoServerApplication.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/SaSsoServerApplication.java new file mode 100644 index 000000000..1df72812d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/SaSsoServerApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import cn.dev33.satoken.sso.SaSsoManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaSsoServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSsoServerApplication.class, args); + + System.out.println(); + System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------"); + System.out.println("配置信息:" + SaSsoManager.getServerConfig()); + System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth"); + System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); + System.out.println(); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/H5Controller.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/H5Controller.java new file mode 100644 index 000000000..ca1cde202 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/H5Controller.java @@ -0,0 +1,50 @@ +package com.pj.h5; + +import cn.dev33.satoken.sso.template.SaSsoServerUtil; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.StpUtil; +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; + +/** + * 前后台分离架构下集成SSO所需的代码 (SSO-Server端) + *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

+ * @author click33 + * + */ +@RestController +public class H5Controller { + + /** + * 返回当前是否已经登录 + */ + @RequestMapping("/sso/isLogin") + public SaResult isLogin() { + return SaResult.data(StpUtil.isLogin()); + } + + /** + * 获取 redirectUrl + */ + @RequestMapping("/sso/getRedirectUrl") + public SaResult getRedirectUrl(String client, String redirect, String mode) { + // 未登录情况下,返回 code=401 + if(StpUtil.isLogin() == false) { + return SaResult.code(401); + } + // 已登录情况下,构建 redirectUrl + redirect = SaFoxUtil.decoderUrl(redirect); + if(SaSsoConsts.MODE_SIMPLE.equals(mode)) { + // 模式一 + SaSsoServerUtil.checkRedirectUrl(client, redirect); + return SaResult.data(redirect); + } else { + // 模式二或模式三 + String redirectUrl = SaSsoServerUtil.buildRedirectUrl(client, redirect, StpUtil.getLoginId(), StpUtil.getTokenValue()); + return SaResult.data(redirectUrl); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/SaTokenConfigure.java new file mode 100644 index 000000000..7c0dbc914 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/SaTokenConfigure.java @@ -0,0 +1,40 @@ +package com.pj.h5; + +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 (解决跨域问题) + * + * @author click33 + */ +@Configuration +public class SaTokenConfigure { + + /** + * CORS 跨域处理策略 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java new file mode 100644 index 000000000..12e3ae848 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java @@ -0,0 +1,23 @@ +package com.pj.sso; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import cn.dev33.satoken.util.SaResult; + +/** + * 全局异常处理 + * @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-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/HomeController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/HomeController.java new file mode 100644 index 000000000..8ff6a307a --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/HomeController.java @@ -0,0 +1,36 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * SSO 平台中心模式示例,跳连接进入子系统 + */ +@RestController +public class HomeController { + + // 平台化首页 + @RequestMapping({"/", "/home"}) + public Object index() { + // 如果未登录,则先去登录 + if(!StpUtil.isLogin()) { + return SaHolder.getResponse().redirect("/sso/auth"); + } + + // 拼接各个子系统的地址,格式形如:/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页} + String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/"; + String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/"; + String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/"; + + // 组织网页结构返回到前端 + String title = "

SSO 平台首页 (平台中心模式)

"; + String client1 = "

进入Client1系统

"; + String client2 = "

进入Client2系统

"; + String client3 = "

进入Client3系统

"; + + return title + client1 + client2 + client3; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/SsoServerController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/SsoServerController.java new file mode 100644 index 000000000..d5cdaecb9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/SsoServerController.java @@ -0,0 +1,68 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +/** + * Sa-Token-SSO Server端 Controller + * @author click33 + * + */ +@RestController +public class SsoServerController { + + /** + * SSO-Server端:处理所有SSO相关请求 + * http://{host}:{port}/sso/auth -- 单点登录授权地址 + * http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd + * http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开) + */ + @RequestMapping("/sso/*") + public Object ssoRequest() { + return SaSsoServerProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoServerTemplate ssoServerTemplate) { + + // 配置:未登录时返回的View + ssoServerTemplate.strategy.notLoginView = () -> { + return new ModelAndView("sa-login.html"); + }; + + // 配置:登录处理函数 + ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { + // 此处仅做模拟登录,真实环境应该查询数据库进行登录 + if("sa".equals(name) && "123456".equals(pwd)) { + String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32)); + StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId)); + return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); + } + return SaResult.error("登录失败!"); + }; + + // 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口) + ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> { + System.out.println("收到消息:" + message); + + // 自定义返回结果(模拟) + return SaResult.ok() + .set("id", message.get("loginId")) + .set("name", "LinXiaoYu") + .set("sex", "女") + .set("age", 18); + }); + + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml new file mode 100644 index 000000000..5c5a15127 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml @@ -0,0 +1,105 @@ +# 端口 +server: + port: 9000 + +# Sa-Token 配置 +sa-token: + # 打印操作日志 + is-log: true + + # SSO 模式一配置 (非模式一不需要配置) +# cookie: +# # 配置 Cookie 作用域 +# domain: stp.com + + # SSO-Server 配置 + sso-server: + # Ticket有效期 (单位: 秒),默认五分钟 + ticket-timeout: 300 + # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 + home-route: /home + # 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) + allow-anon-client: true + # 所有允许的授权回调地址 (匿名 client 使用) + allow-url: "*" + # API 接口调用秘钥 (全局默认 + 匿名 client 使用) + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用列表:配置接入的应用信息 + clients: + # 应用 sso-client1:采用模式一对接 (同域、同Redis) + sso-client1: + client: sso-client1 + allow-url: "*" + # 应用 sso-client2:采用模式二对接 (跨域、同Redis) + sso-client2: + client: sso-client2 + allow-url: "*" + secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用 sso-client3:采用模式三对接 (跨域、跨Redis) + sso-client3: + # 应用名称 + client: sso-client3 + # 允许授权地址 + allow-url: "*" + # 是否接收消息推送 + is-push: true + # 消息推送地址 + push-url: http://sa-sso-client1.com:9003/sso/pushC + # 接口调用秘钥 (如果不配置则使用全局默认秘钥) + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用 sso-client3-nosdk:采用 NoSdk 模式对接 (不依赖 Sa-Token 客户端 SDK,手动实现协议) + sso-client3-nosdk: + # 应用名称 + client: sso-client3-nosdk + # 允许授权地址 + allow-url: "*" + # 是否接收消息推送 + is-push: true + # 消息推送地址 + push-url: http://sa-sso-client1.com:9004/sso/pushC + # 接口调用秘钥 (如果不配置则使用全局默认秘钥) + secret-key: SSO-C3-NoSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用 sso-client3-resdk:采用 ReSdk 模式对接 + sso-client3-resdk: + # 应用名称 + client: sso-client3-resdk + # 允许授权地址 + allow-url: "*" + # 是否接收消息推送 + is-push: true + # 消息推送地址 + push-url: http://sa-sso-client1.com:9005/sso/pushC + # 接口调用秘钥 (如果不配置则使用全局默认秘钥) + secret-key: SSO-C3-ReSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +spring: + # Redis配置 (SSO模式一和模式二使用 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 + +forest: + # 关闭 forest 请求日志打印 + log-enabled: false + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/jquery.min.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/jquery.min.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/jquery.min.js rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/jquery.min.js diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/layer.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/layer.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/layer.js rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/layer.js diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/layer.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/layer.js similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/layer.js rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/layer.js diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon-ext.png diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/icon.png b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon.png similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/icon.png rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/icon.png diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/layer.css b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/layer.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/layer.css rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/layer.css diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-0.gif diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-1.gif diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/loading-2.gif diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/login.css b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.css similarity index 100% rename from sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/login.css rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.css diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.js b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.js new file mode 100644 index 000000000..fb64ce92c --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.js @@ -0,0 +1,65 @@ +// sa +var sa = {}; + +// 打开loading +sa.loading = function(msg) { + layer.closeAll(); // 开始前先把所有弹窗关了 + return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); +}; + +// 隐藏loading +sa.hideLoading = function() { + layer.closeAll(); +}; + + +// ----------------------------------- 登录事件 ----------------------------------- + +$('.login-btn').click(function(){ + sa.loading("正在登录..."); + // 开始登录 + setTimeout(function() { + $.ajax({ + url: "sso/doLogin", + type: "post", + data: { + name: $('[name=name]').val(), + pwd: $('[name=pwd]').val() + }, + dataType: 'json', + success: function(res){ + console.log('返回数据:', res); + sa.hideLoading(); + if(res.code == 200) { + layer.msg('登录成功', {anim: 0, icon: 6 }); + setTimeout(function() { + location.reload(); + }, 800) + } else { + layer.msg(res.msg, {anim: 6, icon: 2 }); + } + }, + error: function(xhr, type, errorThrown){ + sa.hideLoading(); + if(xhr.status == 0){ + return layer.alert('无法连接到服务器,请检查网络'); + } + return layer.alert("异常:" + JSON.stringify(xhr)); + } + }); + }, 400); +}); + +// 绑定回车事件 +$('[name=name],[name=pwd]').bind('keypress', function(event){ + if(event.keyCode == "13") { + $('.login-btn').click(); + } +}); + +// 输入框获取焦点 +$("[name=name]").focus(); + +// 打印信息 +var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.com/"; +console.log(str); diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/templates/sa-login.html b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/templates/sa-login.html similarity index 100% rename from sa-token-demo/sa-token-demo-sso2-server/src/main/resources/templates/sa-login.html rename to sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/templates/sa-login.html diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/pom.xml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/pom.xml new file mode 100644 index 000000000..d57967dde --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/pom.xml @@ -0,0 +1,65 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso1-client + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-alone-redis + ${sa-token.version} + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/SaSso1ClientApplication.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/SaSso1ClientApplication.java new file mode 100644 index 000000000..a711b146e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/SaSso1ClientApplication.java @@ -0,0 +1,28 @@ +package com.pj; + +import cn.dev33.satoken.sso.SaSsoManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * SSO模式一,Client端 Demo + * @author click33 + * + */ +@SpringBootApplication +public class SaSso1ClientApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSso1ClientApplication.class, args); + + System.out.println(); + System.out.println("---------------------- 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/sa-token-demo-sso1-client/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..fedf762ba --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,45 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * Sa-Token-SSO Client端 Controller + * @author click33 + */ +@RestController +public class SsoClientController { + + // SSO-Client端:首页 + @RequestMapping("/") + public String index(HttpServletRequest request) { + String url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), request.getQueryString()) ); + SaSsoClientConfig cfg = SaSsoManager.getClientConfig(); + + String str = "

Sa-Token SSO-Client 应用端 (模式一)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单浏览器注销 - " + + "全端注销 " + + "

"; + return str; + } + + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/resources/application.yml new file mode 100644 index 000000000..ff40f8042 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/resources/application.yml @@ -0,0 +1,44 @@ +# 端口 +server: + port: 9001 + +# Sa-Token 配置 +sa-token: + # 打印操作日志 + is-log: true + + # SSO-相关配置 + sso-client: + # client 标识 + client: sso-client1 + # SSO-Server端主机地址 + server-url: http://sso.stp.com:9000 + + # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) + # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 + alone-redis: + # Redis数据库索引 + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/pom.xml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/pom.xml new file mode 100644 index 000000000..2d82c315f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/pom.xml @@ -0,0 +1,74 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso2-client + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-alone-redis + ${sa-token.version} + + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/SaSso2ClientApplication.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/SaSso2ClientApplication.java new file mode 100644 index 000000000..fca4a7b2e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/SaSso2ClientApplication.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.sso.SaSsoManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaSso2ClientApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSso2ClientApplication.class, args); + + System.out.println(); + System.out.println("---------------------- 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/sa-token-demo-sso2-client/src/main/java/com/pj/h5/H5Controller.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/H5Controller.java new file mode 100644 index 000000000..eb692567e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/H5Controller.java @@ -0,0 +1,45 @@ +package com.pj.h5; + +import cn.dev33.satoken.sso.model.SaCheckTicketResult; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 前后台分离架构下集成SSO所需的代码 (SSO-Client端) + *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

+ * @author click33 + * + */ +@RestController +public class H5Controller { + + // 判断当前是否登录 + @RequestMapping("/sso/isLogin") + public Object isLogin() { + return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); + } + + // 返回SSO认证中心登录地址 + @RequestMapping("/sso/getSsoAuthUrl") + public SaResult getSsoAuthUrl(String clientLoginUrl) { + String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); + return SaResult.data(serverAuthUrl); + } + + // 根据 ticket 进行登录 + @RequestMapping("/sso/doLoginByTicket") + public SaResult doLoginByTicket(String ticket) { + SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); + StpUtil.login(ctr.loginId, new SaLoginParameter() + .setTimeout(ctr.remainTokenTimeout) + .setDeviceId(ctr.deviceId) + ); + return SaResult.data(StpUtil.getTokenValue()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/SaTokenConfigure.java new file mode 100644 index 000000000..fb7492785 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/SaTokenConfigure.java @@ -0,0 +1,40 @@ +package com.pj.h5; + +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 + * + * @author click33 + */ +@Configuration +public class SaTokenConfigure { + + /** + * CORS 跨域处理策略 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java new file mode 100644 index 000000000..f4e55a31e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.pj.sso; + +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-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..7fa8bd697 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,81 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Sa-Token-SSO Client端 Controller + * @author click33 + */ +@RestController +public class SsoClientController { + + // 首页 + @RequestMapping("/") + public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式二)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "单浏览器注销 - " + + "全端注销 - " + + "账号资料" + + "

"; + return str; + } + + /* + * SSO-Client端:处理所有SSO相关请求 + * http://{host}:{port}/sso/login -- Client 端登录地址 + * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) + * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 + */ + @RequestMapping("/sso/*") + public Object ssoRequest() { + return SaSsoClientProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + } + + // 当前应用独自注销 (不退出其它应用) + @RequestMapping("/sso/logoutByAlone") + public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); + } + + // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 + @RequestMapping("/sso/myInfo") + public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 获取本地 loginId + Object loginId = StpUtil.getLoginId(); + + // 推送消息 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", loginId); + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/resources/application.yml new file mode 100644 index 000000000..8ee463a6d --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/resources/application.yml @@ -0,0 +1,47 @@ +# 端口 +server: + port: 9002 + +# sa-token配置 +sa-token: + # 打印操作日志 + is-log: true + + # SSO-相关配置 + sso-client: + # 应用标识 + client: sso-client2 + # SSO-Server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) + # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html + # API 接口调用秘钥 (单点注销时会用到) + secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + + # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) + # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 + alone-redis: + # Redis数据库索引 + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + +forest: + # 关闭 forest 请求日志打印 + log-enabled: false diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/pom.xml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/pom.xml new file mode 100644 index 000000000..cfc489a4c --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/pom.xml @@ -0,0 +1,68 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso3-client-anon + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/SaSso3ClientAnonApplication.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/SaSso3ClientAnonApplication.java new file mode 100644 index 000000000..10ef3122f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/SaSso3ClientAnonApplication.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.sso.SaSsoManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaSso3ClientAnonApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSso3ClientAnonApplication.class, args); + + System.out.println(); + System.out.println("---------------------- Sa-Token SSO 模式三 (匿名应用) Client 端启动成功 ----------------------"); + System.out.println("配置信息:" + SaSsoManager.getClientConfig()); + System.out.println("测试访问应用端一: http://sa-sso-client1.com:9006"); + System.out.println("测试访问应用端二: http://sa-sso-client2.com:9006"); + System.out.println("测试访问应用端三: http://sa-sso-client3.com:9006"); + 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/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/GlobalExceptionHandler.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/GlobalExceptionHandler.java new file mode 100644 index 000000000..f4e55a31e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.pj.sso; + +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-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..9bf4e4428 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,93 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Sa-Token-SSO Client端 Controller + * @author click33 + */ +@RestController +public class SsoClientController { + + // 首页 + @RequestMapping("/") + public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式三-匿名应用)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "单浏览器注销 - " + + "全端注销 - " + + "账号资料" + + "

"; + return str; + } + + /* + * SSO-Client端:处理所有SSO相关请求 + * http://{host}:{port}/sso/login -- Client 端登录地址 + * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) + * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 + */ + @RequestMapping("/sso/*") + public Object ssoRequest() { + return SaSsoClientProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + // 重写 loginId 与 centerId 转换策略函数,做到本地应用 userId 与认证中心 userId 的互相映射 + +// // 将 centerId 转换为 loginId 的函数 +// ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> { +// return "Stu" + centerId; +// }; +// // 将 loginId 转换为 centerId 的函数 +// ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> { +// return loginId.toString().substring(3); +// }; + } + + // 当前应用独自注销 (不退出其它应用) + @RequestMapping("/sso/logoutByAlone") + public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); + } + + // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 + @RequestMapping("/sso/myInfo") + public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 原写法:直接调用 StpUtil.getLoginId() 当做 centerId 来提交 + // Object centerId = StpUtil.getLoginId(); + + // 新写法:获取本地 loginId 对应的认证中心 centerId + Object centerId = SaSsoClientUtil.getSsoTemplate().strategy.convertLoginIdToCenterId.run(StpUtil.getLoginId()); + + // 推送消息 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", centerId); + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/resources/application.yml new file mode 100644 index 000000000..138dcd450 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/resources/application.yml @@ -0,0 +1,51 @@ +# 端口 +server: + port: 9006 + +# sa-token配置 +sa-token: + # 配置一个不同的 token-name,以避免在和模式三 demo 一起测试时发生数据覆盖 + token-name: satoken-client-anon + # sso-client 相关配置 + sso-client: + # client 标识 匿名应用就是指不配置 client 标识的应用 + # client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 使用 Http 请求校验ticket (模式三) + is-http: true + # 是否在登录时注册单点登录回调接口 (匿名应用想要参与单点注销必须打开这个) + reg-logout-call: true + # API 接口调用秘钥 + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +spring: + # 配置 Redis 连接 (此处与SSO-Server端连接不同的Redis) + redis: + # Redis数据库索引 + database: 6 + # 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 + +forest: + # 关闭 forest 请求日志打印 + log-enabled: false + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/pom.xml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/pom.xml new file mode 100644 index 000000000..237f332e3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso3-client-nosdk + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.dtflys.forest + forest-spring-boot-starter + 1.5.26 + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java new file mode 100644 index 000000000..a5d677987 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaSsoClientNoSdkApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSsoClientNoSdkApplication.class, args); + + System.out.println(); + System.out.println("---------------------- Sa-Token SSO 模式三 (NoSdk版) demo 启动成功 ----------------------"); + System.out.println("测试访问应用端一: http://sa-sso-client1.com:9004"); + System.out.println("测试访问应用端二: http://sa-sso-client2.com:9004"); + System.out.println("测试访问应用端三: http://sa-sso-client3.com:9004"); + 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/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..f9f329a54 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,190 @@ +package com.pj.sso; + +import com.pj.sso.util.AjaxJson; +import com.pj.sso.util.MyHttpSessionHolder; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * SSO Client端 Controller + * @author click33 + */ +@RestController +public class SsoClientController { + + // SSO-Client端:首页 + @RequestMapping("/") + public String index(HttpSession session) { + Object userId = session.getAttribute("userId"); + String str = "

Sa-Token SSO-Client 应用端 (模式三-NoSdk)

" + + "

当前会话是否登录:" + (userId != null) + " (" + userId + ")

" + + "

登录" + + " 注销" + + " 获取资料

"; + return str; + } + + // SSO-Client端:单点登录地址 + @RequestMapping("/sso/login") + public Object ssoLogin(String ticket, @RequestParam(defaultValue = "/") String back, + HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException { + + // 如果已经登录,则直接返回 + if (session.getAttribute("userId") != null) { + response.sendRedirect(back); + return null; + } + + /* + * 此时有两种情况: + * 情况1:ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心 + * 情况2:ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录 + */ + if (ticket == null) { + // ------- 情况 1 + // 当前 url,形如:http://sso-client.com/sso/login?back=xxx + String clientLoginUrl = request.getRequestURL().toString() + "?back=" + SsoRequestUtil.encodeUrl(back); + // 最终授权地址,形如:http://sso-server.com/sso/auth?client=xxx&redirect=http://sso-client.com/sso/login?back=xxx + String serverAuthUrl = SsoRequestUtil.authUrl + + "?client=" + SsoRequestUtil.clientId + + "&redirect=" + clientLoginUrl; + response.sendRedirect(serverAuthUrl); + return null; + + } else { + // ------- 情况 2 + // 构建 checkTicket 请求参数,以 ticket 查询 userId + Map params = new LinkedHashMap<>(); + params.put("msgType", "checkTicket"); + params.put("client", SsoRequestUtil.clientId); + params.put("ticket", ticket); + SsoSignUtil.addSignParams(params); + String pushUrl = SsoRequestUtil.buildUrl(SsoRequestUtil.pushSUrl, params); + AjaxJson result = SsoRequestUtil.request(pushUrl); + + // 200 代表校验成功 + if (result.getCode() == 200 && !SsoRequestUtil.isEmpty(result.getData())) { + // 登录上 + session.setAttribute("userId", result.getData()); + // 返回 back 地址 + response.sendRedirect(back); + return null; + } else { + // 将 sso-server 回应的消息作为异常抛出 + throw new RuntimeException(result.getMsg()); + } + } + } + + // SSO-Client端:单点注销地址 + @RequestMapping("/sso/logout") + public Object ssoLogout(@RequestParam(defaultValue = "/") String back, + HttpServletResponse response, HttpSession session) throws IOException { + + // 如果未登录,则无需注销 + if (session.getAttribute("userId") == null) { + response.sendRedirect(back); + return null; + } + + // 调用 sso-server 认证中心单点注销 API + Object loginId = session.getAttribute("userId"); + Map params = new LinkedHashMap<>(); + params.put("msgType", "signout"); + params.put("client", SsoRequestUtil.clientId); + params.put("loginId", String.valueOf(loginId)); + SsoSignUtil.addSignParams(params); + String pushUrl = SsoRequestUtil.buildUrl(SsoRequestUtil.pushSUrl, params); + AjaxJson result = SsoRequestUtil.request(pushUrl); + + // 校验响应状态码,200 代表成功 + if (result.getCode() == 200) { + // 极端场景下,sso-server 中心的单点注销可能并不会通知到此 client 端,所以这里需要再补一刀 + session.removeAttribute("userId"); + // 返回 back 地址 + response.sendRedirect(back); + return null; + } else { + // 将 sso-server 回应的消息作为异常抛出 + throw new RuntimeException(result.getMsg()); + } + } + + // SSO-Server 端消息推送接收地址(单点注销回调等) + @RequestMapping("/sso/pushC") + public Object ssoPushC(HttpServletRequest request) { + + // 将请求参数收集为 Map + Map params = new LinkedHashMap<>(); + for (Map.Entry entry : request.getParameterMap().entrySet()) { + params.put(entry.getKey(), entry.getValue()[0]); + } + + // 校验签名 + if (!SsoSignUtil.verifySign(params)) { + return AjaxJson.getError("无效签名,拒绝应答"); + } + + // 按 msgType 分发处理 + String msgType = params.get("msgType"); + + // 单点注销回调 + if ("logoutCall".equals(msgType)) { + // 注销这个账号 id 在本 client 端的所有会话 + String loginId = params.get("loginId"); + for (HttpSession session : MyHttpSessionHolder.sessionList) { + Object userId = session.getAttribute("userId"); + if (Objects.equals(String.valueOf(userId), loginId)) { + session.removeAttribute("userId"); + } + } + return AjaxJson.getSuccess("账号id=" + loginId + " 注销成功"); + } + + // 其它消息类型 +// if("xxx".equals(msgType)) { +// // 处理 xxx 消息 +// } + + return AjaxJson.getError("未知消息类型:" + msgType); + } + + // 查询我的账号信息(调用 sso-server 端 userinfo 消息处理器) + @RequestMapping("/sso/myInfo") + public Object myInfo(HttpSession session) { + // 如果尚未登录 + if (session.getAttribute("userId") == null) { + return "尚未登录,无法获取"; + } + + Object loginId = session.getAttribute("userId"); + Map params = new LinkedHashMap<>(); + params.put("msgType", "userinfo"); + params.put("client", SsoRequestUtil.clientId); + params.put("loginId", String.valueOf(loginId)); + SsoSignUtil.addSignParams(params); + String pushUrl = SsoRequestUtil.buildUrl(SsoRequestUtil.pushSUrl, params); + AjaxJson result = SsoRequestUtil.request(pushUrl); + + // 返回给前端 + return result; + } + + // 全局异常拦截 + @ExceptionHandler + public AjaxJson handlerException(Exception e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java new file mode 100644 index 000000000..3a7a22ae1 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java @@ -0,0 +1,92 @@ +package com.pj.sso; + +import com.dtflys.forest.Forest; +import com.pj.sso.util.AjaxJson; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Map; + +/** + * 封装一些 sso 共用方法 + * + * @author click33 + * @since 2022-4-30 + */ +public class SsoRequestUtil { + + /** + * SSO-Server 端主机地址 + */ + public static String serverUrl = "http://sa-sso-server.com:9000"; + + /** + * SSO-Server 端统一认证地址 + */ + public static String authUrl = serverUrl + "/sso/auth"; + + /** + * SSO-Server 端统一消息推送地址(ticket校验、单点注销、获取用户信息等均通过此入口) + */ + public static String pushSUrl = serverUrl + "/sso/pushS"; + + /** + * 当前应用的客户端标识(需与 sso-server 端 clients 配置一致) + */ + public static String clientId = "sso-client3-nosdk"; + + /** + * 接口调用秘钥(需与 sso-server 端对应 client 配置一致) + */ + public static String secretKey = "SSO-C3-NoSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"; + + // -------------------------- 工具方法 + + /** + * 发出请求,并返回 AjaxJson 结果 + * @param url 请求地址(含查询参数) + * @return 返回的结果 + */ + public static AjaxJson request(String url) { + Map map = Forest.post(url).executeAsMap(); + return new AjaxJson(map); + } + + /** + * 将参数 Map 拼接到 baseUrl 后面(值进行 URL 编码),返回完整 URL + * @param baseUrl 基础 URL + * @param params 请求参数 + * @return 拼接后的完整 URL + */ + public static String buildUrl(String baseUrl, Map params) { + StringBuilder sb = new StringBuilder(baseUrl).append("?"); + for (Map.Entry entry : params.entrySet()) { + sb.append(entry.getKey()).append("=").append(encodeUrl(entry.getValue())).append("&"); + } + sb.deleteCharAt(sb.length() - 1); + return sb.toString(); + } + + /** + * 指定元素是否为null或者空字符串 + * @param str 指定元素 + * @return 是否为null或者空字符串 + */ + public static boolean isEmpty(Object str) { + return str == null || "".equals(str); + } + + /** + * URL编码 + * @param url see note + * @return see note + */ + public static String encodeUrl(String url) { + try { + return URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoSignUtil.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoSignUtil.java new file mode 100644 index 000000000..188b08074 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoSignUtil.java @@ -0,0 +1,102 @@ +package com.pj.sso; + +import java.security.MessageDigest; +import java.util.Map; +import java.util.Random; +import java.util.TreeMap; + +/** + * SSO 签名工具类:签名生成、注入与校验 + * + * @author click33 + */ +public class SsoSignUtil { + + + + // -------------------------- 签名方法 + + /** + * 计算签名:将 params(排除 sign 字段)按 key 字典序升序排列, + * 拼接为 k=v&k=v 后追加 &key={secretKey},整体 MD5 + * @param params 请求参数(不含 sign) + * @return 签名值 + */ + public static String computeSign(Map params) { + TreeMap sorted = new TreeMap<>(params); + sorted.remove("sign"); + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : sorted.entrySet()) { + if (sb.length() > 0) sb.append("&"); + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + sb.append("&key=").append(SsoRequestUtil.secretKey); + return md5(sb.toString()); + } + + /** + * 向参数 Map 中注入 timestamp、nonce、sign 三个签名参数 + * @param params 请求参数(已填好业务参数,此方法自动追加签名参数) + */ + public static void addSignParams(Map params) { + params.put("timestamp", String.valueOf(System.currentTimeMillis())); + params.put("nonce", getRandomString(20)); + params.put("sign", computeSign(params)); + } + + /** + * 校验请求中的 sign 参数是否合法 + * @param params 包含 sign 的请求参数 + * @return 签名是否合法 + */ + public static boolean verifySign(Map params) { + String sign = params.get("sign"); + if (sign == null) return false; + return sign.equals(computeSign(params)); + } + + + // -------------------------- 基础工具 + + /** + * MD5 加密 + * @param str 指定字符串 + * @return 加密后的字符串 + */ + public static String md5(String str) { + str = (str == null ? "" : str); + char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + try { + byte[] btInput = str.getBytes(); + MessageDigest mdInst = MessageDigest.getInstance("MD5"); + mdInst.update(btInput); + byte[] md = mdInst.digest(); + int j = md.length; + char[] strA = new char[j * 2]; + int k = 0; + for (byte byte0 : md) { + strA[k++] = hexDigits[byte0 >>> 4 & 0xf]; + strA[k++] = hexDigits[byte0 & 0xf]; + } + return new String(strA); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 生成指定长度的随机字符串 + * @param length 字符串的长度 + * @return 一个随机字符串 + */ + public static String getRandomString(int length) { + String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(str.charAt(random.nextInt(62))); + } + return sb.toString(); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/AjaxJson.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/AjaxJson.java new file mode 100644 index 000000000..4773c827b --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/AjaxJson.java @@ -0,0 +1,231 @@ +package com.pj.sso.util; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + + +/** + * ajax请求返回Json格式数据的封装
+ * 所有预留字段:
+ * code=状态码
+ * msg=描述信息
+ * data=携带对象
+ * pageNo=当前页
+ * pageSize=页大小
+ * startIndex=起始索引
+ * dataCount=数据总数
+ * pageCount=分页总数
+ *

返回范例:

+ *
+	{
+		"code": 200,    // 成功时=200, 失败时=500  msg=失败原因
+		"msg": "ok",
+		"data": {}
+	} 
+	
+ */ +public class AjaxJson extends LinkedHashMap implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + + + // ============================ 写值取值 ================================== + + /** 给code赋值,连缀风格 */ + public AjaxJson setCode(int code) { + this.put("code", code); + return this; + } + /** 返回code */ + public Integer getCode() { + return (Integer)this.get("code"); + } + + /** 给msg赋值,连缀风格 */ + public AjaxJson setMsg(String msg) { + this.put("msg", msg); + return this; + } + /** 获取msg */ + public String getMsg() { + return (String)this.get("msg"); + } + + /** 给data赋值,连缀风格 */ + public AjaxJson setData(Object data) { + this.put("data", data); + return this; + } + /** 获取data */ + public Object getData() { + return this.get("data"); + } + /** 将data还原为指定类型并返回 */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) this.getData(); + } + + /** 给dataCount(数据总数)赋值,连缀风格 */ + public AjaxJson setDataCount(Long dataCount) { + this.put("dataCount", dataCount); + // 如果提供了数据总数,则尝试计算page信息 + if(dataCount != null && dataCount >= 0) { + // 如果:已有page信息 + if(get("pageNo") != null) { + this.initPageInfo(); + } +// // 或者:是JavaWeb环境 +// else if(SoMap.isJavaWeb()) { +// SoMap so = SoMap.getRequestSoMap(); +// this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize()); +// this.initPageInfo(); +// } + } + return this; + } + /** 获取dataCount(数据总数) */ + public Long getDataCount() { + return (Long)this.get("dataCount"); + } + + /** 设置pageNo 和 pageSize,并计算出startIndex于pageCount */ + public AjaxJson setPageNoAndSize(long pageNo, long pageSize) { + this.put("pageNo", pageNo); + this.put("pageSize", pageSize); + return this; + } + + /** 根据 pageSize dataCount,计算startIndex 与 pageCount */ + public AjaxJson initPageInfo() { + long pageNo = (long)this.get("pageNo"); + long pageSize = (long)this.get("pageSize"); + long dataCount = (long)this.get("dataCount"); + this.set("startIndex", (pageNo - 1) * pageSize); + long pc = dataCount / pageSize; + this.set("pageCount", (dataCount % pageSize == 0 ? pc : pc + 1)); + return this; + } + + + /** 写入一个值 自定义key, 连缀风格 */ + public AjaxJson set(String key, Object data) { + this.put(key, data); + return this; + } + + /** 写入一个Map, 连缀风格 */ + public AjaxJson setMap(Map map) { + for (String key : map.keySet()) { + this.put(key, map.get(key)); + } + return this; + } + + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.setCode(code); + this.setMsg(msg); + this.setData(data); + if(dataCount != null) { + this.setDataCount(dataCount); + } + } + + public AjaxJson(Map map) { + for (String key: map.keySet()) { + this.set(key, map.get(key)); + } + } + + /** 返回成功 */ + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + + /** 返回失败 */ + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + /** 返回警告 */ + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + /** 返回未登录 */ + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + /** 返回没有权限的 */ + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + /** 返回一个自定义状态码的 */ + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + /** 返回分页和数据的 */ + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + /** 返回, 根据受影响行数的(大于0=ok,小于0=error) */ + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + /** 返回,根据布尔值来确定最终结果的 (true=ok,false=error) */ + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + + + + + + +// // 历史版本遗留代码 +// public int code; // 状态码 +// public String msg; // 描述信息 +// public Object data; // 携带对象 +// public Long dataCount; // 数据总数,用于分页 + + + + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/MyHttpSessionHolder.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/MyHttpSessionHolder.java new file mode 100644 index 000000000..cc62f31e9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/MyHttpSessionHolder.java @@ -0,0 +1,34 @@ +package com.pj.sso.util; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.springframework.stereotype.Component; + +/** + * 记录所有已创建的 HttpSession 对象 + * + * 此种方式有性能问题,仅做demo示例,真实项目中请更换为其它方案记录用户会话数据 + * + * @author click33 + * @since 2022-4-30 + */ +@Component +public class MyHttpSessionHolder implements HttpSessionListener { + + public static List sessionList = new ArrayList<>(); + + public void sessionCreated(HttpSessionEvent httpSessionEvent) { + sessionList.add(httpSessionEvent.getSession()); + } + + public void sessionDestroyed(HttpSessionEvent httpSessionEvent) { + HttpSession session = httpSessionEvent.getSession(); + sessionList.remove(session); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/resources/application.yml new file mode 100644 index 000000000..982a1264f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/resources/application.yml @@ -0,0 +1,36 @@ +# 端口 +server: + port: 9004 + +forest: + # 打开/关闭Forest请求日志(默认为 true) + log-request: true + +spring: + # Redis连接 + redis: + # Redis数据库索引 + database: 2 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/pom.xml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/pom.xml new file mode 100644 index 000000000..f80a61955 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/pom.xml @@ -0,0 +1,54 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso3-client-resdk + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/SaSsoClientReSdkApplication.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/SaSsoClientReSdkApplication.java new file mode 100644 index 000000000..e59123405 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/SaSsoClientReSdkApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaSsoClientReSdkApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSsoClientReSdkApplication.class, args); + + System.out.println(); + System.out.println("---------------------- Sa-Token SSO 模式三 (ReSdk版) demo 启动成功 ----------------------"); + System.out.println("测试访问应用端一: http://sa-sso-client1.com:9005"); + System.out.println("测试访问应用端二: http://sa-sso-client2.com:9005"); + System.out.println("测试访问应用端三: http://sa-sso-client3.com:9005"); + 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/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/MyHttpSessionHolder.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/MyHttpSessionHolder.java new file mode 100644 index 000000000..3e19b615b --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/MyHttpSessionHolder.java @@ -0,0 +1,34 @@ +package com.pj.resdk; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.springframework.stereotype.Component; + +/** + * 记录所有已创建的 HttpSession 对象 + * + * 此种方式有性能问题,仅做demo示例,真实项目中请更换为其它方案记录用户会话数据 + * + * @author click33 + * @since 2022-4-30 + */ +@Component +public class MyHttpSessionHolder implements HttpSessionListener { + + public static List sessionList = new ArrayList<>(); + + public void sessionCreated(HttpSessionEvent httpSessionEvent) { + sessionList.add(httpSessionEvent.getSession()); + } + + public void sessionDestroyed(HttpSessionEvent httpSessionEvent) { + HttpSession session = httpSessionEvent.getSession(); + sessionList.remove(session); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/StpLogicForHttpSession.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/StpLogicForHttpSession.java new file mode 100644 index 000000000..158df6fe9 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/StpLogicForHttpSession.java @@ -0,0 +1,69 @@ +package com.pj.resdk; + +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; + +import javax.servlet.http.HttpSession; +import java.util.Objects; + +/** + * 会话对象 - httpSession 版 + * + * @author click33 + * @since 2025/5/6 + */ +public class StpLogicForHttpSession extends StpLogic { + + /** + * 初始化 StpLogic, 并指定账号类型 + * + * @param type / + * + */ + public StpLogicForHttpSession(String type) { + super(type); + } + + // 判断当前会话是否已登录 + @Override + public boolean isLogin() { + + return SpringMVCUtil.getRequest().getSession().getAttribute("userId") != null; + } + + // 获取当前会话的登录ID + @Override + public Object getLoginId() { + Object userId = SpringMVCUtil.getRequest().getSession().getAttribute("userId"); + if(userId == null) { + throw new RuntimeException("当前会话未登录"); + } + return userId; + } + + // 获取当前登录设备 id + @Override + public String getLoginDeviceId() { + return null; + } + + // 当前会话注销 + @Override + public void logout(SaLogoutParameter logoutParameter) { + SpringMVCUtil.getRequest().getSession().removeAttribute("userId"); + } + + // 当前账号id注销 + @Override + public void _logout(Object loginId, SaLogoutParameter logoutParameter) { + System.out.println("--- 注销账号id:" + loginId); + for (HttpSession session: MyHttpSessionHolder.sessionList) { + Object userId = session.getAttribute("userId"); + if(Objects.equals(String.valueOf(userId), String.valueOf(loginId))) { + session.removeAttribute("userId"); + } + } + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/GlobalExceptionHandler.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/GlobalExceptionHandler.java new file mode 100644 index 000000000..f4e55a31e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.pj.sso; + +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-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..b81df31a3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,92 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import com.pj.resdk.StpLogicForHttpSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpSession; + +/** + * SSO Client端 Controller + * @author click33 + */ +@RestController +public class SsoClientController { + + // SSO-Client端:首页 + @RequestMapping("/") + public String index(HttpSession session) { + boolean isLogin = session.getAttribute("userId") != null; + Object loginId = session.getAttribute("userId"); + String str = "

Sa-Token SSO-Client 应用端 (模式三-ReSdk)

" + + "

当前会话是否登录:" + isLogin + " (" + loginId + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "全端注销 - " + + "账号资料" + + "

"; + return str; + } + + /* + * SSO-Client端:处理所有 SSO 相关请求 + * http://{host}:{port}/sso/login -- Client 端登录地址 + * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) + * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 + */ + @RequestMapping("/sso/*") + public Object ssoLogin() { + return SaSsoClientProcessor.instance.dister(); + } + + // 当前应用独自注销 (不退出其它应用) + @RequestMapping("/sso/logoutByAlone") + public Object logoutByAlone(HttpSession session) { + session.removeAttribute("userId"); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); + } + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + // 自定义底层使用的会话操作对象 + ssoClientTemplate.setStpLogic(new StpLogicForHttpSession(StpUtil.TYPE)); + + // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) + ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { + HttpSession session = SpringMVCUtil.getRequest().getSession(); + session.setAttribute("userId", ctr.loginId); + return SaHolder.getResponse().redirect(back); + }; + } + + // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 + @RequestMapping("/sso/myInfo") + public Object myInfo(HttpSession session) { + // 如果尚未登录 + if(session.getAttribute("userId") == null) { + return "尚未登录,无法获取"; + } + + // 推送消息 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", session.getAttribute("userId")); + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/resources/application.yml new file mode 100644 index 000000000..a65c5ab6f --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/resources/application.yml @@ -0,0 +1,18 @@ +# 端口 +server: + port: 9005 + +# sa-token 配置 +sa-token: + # 是否打印操作日志 + is-log: true + # sso-client 相关配置 + sso-client: + # client 标识 + client: sso-client3-resdk + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 使用 Http 请求校验ticket (模式三) + is-http: true + # API 接口调用秘钥 + secret-key: SSO-C3-ReSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/pom.xml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/pom.xml new file mode 100644 index 000000000..bfde4cba4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/pom.xml @@ -0,0 +1,69 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-sso3-client + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-sso + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-forest + ${sa-token.version} + + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/SaSso3ClientApplication.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/SaSso3ClientApplication.java new file mode 100644 index 000000000..6969038a5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/SaSso3ClientApplication.java @@ -0,0 +1,23 @@ +package com.pj; + +import cn.dev33.satoken.sso.SaSsoManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaSso3ClientApplication { + + public static void main(String[] args) { + SpringApplication.run(SaSso3ClientApplication.class, args); + + System.out.println(); + System.out.println("---------------------- 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/sa-token-demo-sso3-client/src/main/java/com/pj/h5/H5Controller.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/H5Controller.java new file mode 100644 index 000000000..4cea56cb7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/H5Controller.java @@ -0,0 +1,45 @@ +package com.pj.h5; + +import cn.dev33.satoken.sso.model.SaCheckTicketResult; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 前后台分离架构下集成SSO所需的代码 (SSO-Client端) + *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

+ * + * @author click33 + */ +@RestController +public class H5Controller { + + // 当前是否登录 + @RequestMapping("/sso/isLogin") + public Object isLogin() { + return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); + } + + // 返回SSO认证中心登录地址 + @RequestMapping("/sso/getSsoAuthUrl") + public SaResult getSsoAuthUrl(String clientLoginUrl) { + String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); + return SaResult.data(serverAuthUrl); + } + + // 根据ticket进行登录 + @RequestMapping("/sso/doLoginByTicket") + public SaResult doLoginByTicket(String ticket) { + SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket"); + StpUtil.login(ctr.loginId, new SaLoginParameter() + .setTimeout(ctr.remainTokenTimeout) + .setDeviceId(ctr.deviceId) + ); + return SaResult.data(StpUtil.getTokenValue()); + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/SaTokenConfigure.java new file mode 100644 index 000000000..fb7492785 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/SaTokenConfigure.java @@ -0,0 +1,40 @@ +package com.pj.h5; + +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 + * + * @author click33 + */ +@Configuration +public class SaTokenConfigure { + + /** + * CORS 跨域处理策略 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java new file mode 100644 index 000000000..f4e55a31e --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.pj.sso; + +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-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java new file mode 100644 index 000000000..72ad2da3b --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java @@ -0,0 +1,81 @@ +package com.pj.sso; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Sa-Token-SSO Client端 Controller + * @author click33 + */ +@RestController +public class SsoClientController { + + // SSO-Client端:首页 + @RequestMapping("/") + public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "单浏览器注销 - " + + "全端注销 - " + + "账号资料" + + "

"; + return str; + } + + /* + * SSO-Client端:处理所有SSO相关请求 + * http://{host}:{port}/sso/login -- Client 端登录地址 + * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) + * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 + */ + @RequestMapping("/sso/*") + public Object ssoRequest() { + return SaSsoClientProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + } + + // 当前应用独自注销 (不退出其它应用) + @RequestMapping("/sso/logoutByAlone") + public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); + } + + // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 + @RequestMapping("/sso/myInfo") + public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 获取本地 loginId + Object loginId = StpUtil.getLoginId(); + + // 推送消息 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", loginId); + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; + } + +} diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/resources/application.yml new file mode 100644 index 000000000..392f3a421 --- /dev/null +++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/resources/application.yml @@ -0,0 +1,52 @@ +# 端口 +server: + port: 9003 + +# sa-token配置 +sa-token: + # 打印操作日志 + is-log: true + + # sso-client 相关配置 + sso-client: + # 应用标识 + client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) + # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html + # 使用 Http 请求校验 ticket (模式三) + is-http: true + # API 接口调用秘钥 + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +spring: + # 配置 Redis 连接 (此处与 SSO-Server 端连接不同的 Redis) + redis: + # Redis数据库索引 + 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 + +forest: + # 关闭 forest 请求日志打印 + log-enabled: false + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso1/.gitignore b/sa-token-demo/sa-token-demo-sso1/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-sso1/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso1/pom.xml b/sa-token-demo/sa-token-demo-sso1/pom.xml deleted file mode 100644 index 45ae1eccb..000000000 --- a/sa-token-demo/sa-token-demo-sso1/pom.xml +++ /dev/null @@ -1,40 +0,0 @@ - - 4.0.0 - cn.dev33 - sa-token-demo-sso1 - 0.0.1-SNAPSHOT - - - - org.springframework.boot - spring-boot-starter-parent - 2.0.0.RELEASE - - - - - - 1.24.0 - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/SaSsoApplication.java b/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/SaSsoApplication.java deleted file mode 100644 index c80e20a42..000000000 --- a/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/SaSsoApplication.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.pj; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -import cn.dev33.satoken.SaManager; - -@SpringBootApplication -public class SaSsoApplication { - - public static void main(String[] args) { - SpringApplication.run(SaSsoApplication.class, args); - System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); - } - -} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/sso/SsoController.java b/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/sso/SsoController.java deleted file mode 100644 index d5e59d57c..000000000 --- a/sa-token-demo/sa-token-demo-sso1/src/main/java/com/pj/sso/SsoController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.pj.sso; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.pj.util.AjaxJson; - -import cn.dev33.satoken.stp.StpUtil; - -/** - * 测试: 同域单点登录 - * @author kong - */ -@RestController -@RequestMapping("/sso/") -public class SsoController { - - // 测试:进行登录 - @RequestMapping("doLogin") - public AjaxJson doLogin(@RequestParam(defaultValue = "10001") String id) { - System.out.println("---------------- 进行登录 "); - StpUtil.login(id); - return AjaxJson.getSuccess("登录成功: " + id); - } - - // 测试:是否登录 - @RequestMapping("isLogin") - public AjaxJson isLogin() { - System.out.println("---------------- 是否登录 "); - boolean isLogin = StpUtil.isLogin(); - return AjaxJson.getSuccess("是否登录: " + isLogin); - } - -} diff --git a/sa-token-demo/sa-token-demo-sso1/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso1/src/main/resources/application.yml deleted file mode 100644 index 540f442a7..000000000 --- a/sa-token-demo/sa-token-demo-sso1/src/main/resources/application.yml +++ /dev/null @@ -1,8 +0,0 @@ -# 端口 -server: - port: 8081 - -sa-token: - # 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie - cookie-domain: stp.com - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-client/.gitignore b/sa-token-demo/sa-token-demo-sso2-client/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-sso2-client/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-client/pom.xml b/sa-token-demo/sa-token-demo-sso2-client/pom.xml deleted file mode 100644 index e443fdd39..000000000 --- a/sa-token-demo/sa-token-demo-sso2-client/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - 4.0.0 - cn.dev33 - sa-token-demo-sso2-client - 0.0.1-SNAPSHOT - - - - org.springframework.boot - spring-boot-starter-parent - 2.0.0.RELEASE - - - - - - 1.24.0 - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} - - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa-token-version} - - - - - org.apache.commons - commons-pool2 - - - - - cn.dev33 - sa-token-alone-redis - ${sa-token-version} - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/SaSsoClientApplication.java b/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/SaSsoClientApplication.java deleted file mode 100644 index cbafaa0ea..000000000 --- a/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/SaSsoClientApplication.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.pj; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SaSsoClientApplication { - - public static void main(String[] args) { - SpringApplication.run(SaSsoClientApplication.class, args); - System.out.println("\nSa-Token-SSO Client端启动成功"); - } - -} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java deleted file mode 100644 index 612de5538..000000000 --- a/sa-token-demo/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.pj.sso; - -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import cn.dev33.satoken.sso.SaSsoHandle; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-Token-SSO Client端 Controller - * @author kong - */ -@RestController -public class SsoClientController { - - // SSO-Client端:首页 - @RequestMapping("/") - public String index() { - String str = "

Sa-Token SSO-Client 应用端

" + - "

当前会话是否登录:" + StpUtil.isLogin() + "

" + - "

登录 " + - "注销

"; - return str; - } - - // SSO-Client端:处理所有SSO相关请求 - @RequestMapping("/sso/*") - public Object ssoRequest() { - return SaSsoHandle.clientRequest(); - } - - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); - } - -} diff --git a/sa-token-demo/sa-token-demo-sso2-client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso2-client/src/main/resources/application.yml deleted file mode 100644 index 639dfcf54..000000000 --- a/sa-token-demo/sa-token-demo-sso2-client/src/main/resources/application.yml +++ /dev/null @@ -1,40 +0,0 @@ -# 端口 -server: - port: 9001 - -# sa-token配置 -sa-token: - # SSO-相关配置 - sso: - # SSO-Server端 单点登录地址 - auth-url: http://sa-sso-server.com:9000/sso/auth - # 是否打开单点注销接口 - is-slo: true - - # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) - alone-redis: - # Redis数据库索引 - database: 1 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - password: - # 连接超时时间(毫秒) - timeout: 10ms - lettuce: - pool: - # 连接池最大连接数 - max-active: 200 - # 连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - # 连接池中的最大空闲连接 - max-idle: 10 - # 连接池中的最小空闲连接 - min-idle: 0 - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-server/.gitignore b/sa-token-demo/sa-token-demo-sso2-server/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-sso2-server/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-server/pom.xml b/sa-token-demo/sa-token-demo-sso2-server/pom.xml deleted file mode 100644 index e70aad7a1..000000000 --- a/sa-token-demo/sa-token-demo-sso2-server/pom.xml +++ /dev/null @@ -1,59 +0,0 @@ - - 4.0.0 - cn.dev33 - sa-token-demo-sso2-server - 0.0.1-SNAPSHOT - - - - org.springframework.boot - spring-boot-starter-parent - 2.0.0.RELEASE - - - - - - 1.24.0 - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} - - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa-token-version} - - - - - org.apache.commons - commons-pool2 - - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/SaSsoServerApplication.java b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/SaSsoServerApplication.java deleted file mode 100644 index 6b57b5745..000000000 --- a/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/SaSsoServerApplication.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.pj; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SaSsoServerApplication { - - public static void main(String[] args) { - SpringApplication.run(SaSsoServerApplication.class, args); - System.out.println("\nSa-Token-SSO 认证中心启动成功"); - } - -} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/SsoServerController.java b/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/SsoServerController.java deleted file mode 100644 index d211abc2a..000000000 --- a/sa-token-demo/sa-token-demo-sso2-server/src/main/java/com/pj/sso/SsoServerController.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.pj.sso; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.ModelAndView; - -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.sso.SaSsoHandle; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-Token-SSO Server端 Controller - * @author kong - * - */ -@RestController -public class SsoServerController { - - // SSO-Server端:处理所有SSO相关请求 - @RequestMapping("/sso/*") - public Object ssoRequest() { - return SaSsoHandle.serverRequest(); - } - - // 配置SSO相关参数 - @Autowired - private void configSso(SaTokenConfig cfg) { - cfg.sso - // 配置:未登录时返回的View - .setNotLoginView(() -> { - return new ModelAndView("sa-login.html"); - }) - // 配置:登录处理函数 - .setDoLoginHandle((name, pwd) -> { - // 此处仅做模拟登录,真实环境应该查询数据进行登录 - if("sa".equals(name) && "123456".equals(pwd)) { - StpUtil.login(10001); - return SaResult.ok("登录成功!"); - } - return SaResult.error("登录失败!"); - }) - ; - } - - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); - } - -} diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/application.yml deleted file mode 100644 index c8b23222c..000000000 --- a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/application.yml +++ /dev/null @@ -1,42 +0,0 @@ -# 端口 -server: - port: 9000 - -# Sa-Token配置 -sa-token: - # SSO-相关配置 - sso: - # Ticket有效期 (单位: 秒),默认五分钟 - ticket-timeout: 300 - # 所有允许的授权回调地址 - allow-url: http://sa-sso-client1.com:9001/sso/login, http://sa-sso-client2.com:9001/sso/login, http://sa-sso-client3.com:9001/sso/login - -spring: - # Redis配置 - redis: - # Redis数据库索引(默认为0) - database: 1 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - password: - # 连接超时时间(毫秒) - timeout: 10ms - lettuce: - pool: - # 连接池最大连接数 - max-active: 200 - # 连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - # 连接池中的最大空闲连接 - max-idle: 10 - # 连接池中的最小空闲连接 - min-idle: 0 - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.js b/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.js deleted file mode 100644 index f581ee786..000000000 --- a/sa-token-demo/sa-token-demo-sso2-server/src/main/resources/static/sa-res/login.js +++ /dev/null @@ -1,65 +0,0 @@ -// sa -var sa = {}; - -// 打开loading -sa.loading = function(msg) { - layer.closeAll(); // 开始前先把所有弹窗关了 - return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); -}; - -// 隐藏loading -sa.hideLoading = function() { - layer.closeAll(); -}; - - -// ----------------------------------- 登录事件 ----------------------------------- - -$('.login-btn').click(function(){ - sa.loading("正在登录..."); - // 开始登录 - setTimeout(function() { - $.ajax({ - url: "sso/doLogin", - type: "post", - data: { - name: $('[name=name]').val(), - pwd: $('[name=pwd]').val() - }, - dataType: 'json', - success: function(res){ - console.log('返回数据:', res); - sa.hideLoading(); - if(res.code == 200) { - layer.msg('登录成功', {anim: 0, icon: 6 }); - setTimeout(function() { - location.reload(); - }, 800) - } else { - layer.msg(res.msg, {anim: 6, icon: 2 }); - } - }, - error: function(xhr, type, errorThrown){ - sa.hideLoading(); - if(xhr.status == 0){ - return layer.alert('无法连接到服务器,请检查网络'); - } - return layer.alert("异常:" + JSON.stringify(xhr)); - } - }); - }, 400); -}); - -// 绑定回车事件 -$('[name=name],[name=pwd]').bind('keypress', function(event){ - if(event.keyCode == "13") { - $('.login-btn').click(); - } -}); - -// 输入框获取焦点 -$("[name=name]").focus(); - -// 打印信息 -var str = "This page is provided by Sa-Token, Please refer to: " + "http://sa-token.dev33.cn/"; -console.log(str); diff --git a/sa-token-demo/sa-token-demo-sso3-client/.gitignore b/sa-token-demo/sa-token-demo-sso3-client/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-sso3-client/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-client/pom.xml b/sa-token-demo/sa-token-demo-sso3-client/pom.xml deleted file mode 100644 index 4c84d6bde..000000000 --- a/sa-token-demo/sa-token-demo-sso3-client/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - 4.0.0 - cn.dev33 - sa-token-demo-sso3-client - 0.0.1-SNAPSHOT - - - - org.springframework.boot - spring-boot-starter-parent - 2.0.0.RELEASE - - - - - - 1.24.0 - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} - - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa-token-version} - - - - - org.apache.commons - commons-pool2 - - - - - com.ejlchina - okhttps - 3.1.1 - - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/SaSsoClientApplication.java b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/SaSsoClientApplication.java deleted file mode 100644 index cbafaa0ea..000000000 --- a/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/SaSsoClientApplication.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.pj; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SaSsoClientApplication { - - public static void main(String[] args) { - SpringApplication.run(SaSsoClientApplication.class, args); - System.out.println("\nSa-Token-SSO Client端启动成功"); - } - -} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java deleted file mode 100644 index 82e4c39ab..000000000 --- a/sa-token-demo/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.pj.sso; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.ejlchina.okhttps.OkHttps; - -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.sso.SaSsoHandle; -import cn.dev33.satoken.sso.SaSsoUtil; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-Token-SSO Client端 Controller - * @author kong - */ -@RestController -public class SsoClientController { - - // SSO-Client端:首页 - @RequestMapping("/") - public String index() { - String str = "

Sa-Token SSO-Client 应用端

" + - "

当前会话是否登录:" + StpUtil.isLogin() + "

" + - "

登录" + - " 注销

"; - return str; - } - - // SSO-Client端:处理所有SSO相关请求 - @RequestMapping("/sso/*") - public Object ssoRequest() { - return SaSsoHandle.clientRequest(); - } - - // 配置SSO相关参数 - @Autowired - private void configSso(SaTokenConfig cfg) { - cfg.sso - // 配置Http请求处理器 - .setSendHttp(url -> { - return OkHttps.sync(url).get().getBody().toString(); - }) - ; - } - - // 查询我的账号信息 - @RequestMapping("/sso/myinfo") - public Object myinfo() { - Object userinfo = SaSsoUtil.getUserinfo(StpUtil.getLoginId()); - System.out.println("--------info:" + userinfo); - return userinfo; - } - - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); - } - -} diff --git a/sa-token-demo/sa-token-demo-sso3-client/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso3-client/src/main/resources/application.yml deleted file mode 100644 index 3f6194dd9..000000000 --- a/sa-token-demo/sa-token-demo-sso3-client/src/main/resources/application.yml +++ /dev/null @@ -1,51 +0,0 @@ -# 端口 -server: - port: 9001 - -# sa-token配置 -sa-token: - # SSO-相关配置 - sso: - # SSO-Server端 单点登录地址 - auth-url: http://sa-sso-server.com:9000/sso/auth - # 使用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/logout - # 接口调用秘钥 - secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor - # SSO-Server端 查询userinfo地址 - userinfo-url: http://sa-sso-server.com:9000/sso/userinfo - -spring: - # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) - redis: - # Redis数据库索引 - database: 6 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - password: - # 连接超时时间(毫秒) - timeout: 10ms - lettuce: - pool: - # 连接池最大连接数 - max-active: 200 - # 连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - # 连接池中的最大空闲连接 - max-idle: 10 - # 连接池中的最小空闲连接 - min-idle: 0 - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/.gitignore b/sa-token-demo/sa-token-demo-sso3-server/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-sso3-server/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/pom.xml b/sa-token-demo/sa-token-demo-sso3-server/pom.xml deleted file mode 100644 index cbef4ecfd..000000000 --- a/sa-token-demo/sa-token-demo-sso3-server/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - 4.0.0 - cn.dev33 - sa-token-demo-sso3-server - 0.0.1-SNAPSHOT - - - - org.springframework.boot - spring-boot-starter-parent - 2.0.0.RELEASE - - - - - - 1.24.0 - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} - - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa-token-version} - - - - - org.apache.commons - commons-pool2 - - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - - - com.ejlchina - okhttps - 3.1.1 - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/SaSsoServerApplication.java b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/SaSsoServerApplication.java deleted file mode 100644 index 0e07f76d3..000000000 --- a/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/SaSsoServerApplication.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.pj; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SaSsoServerApplication { - - public static void main(String[] args) { - SpringApplication.run(SaSsoServerApplication.class, args); - System.out.println("\nSa-Token-SSO 认证中心启动成功"); - } - -} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerController.java b/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerController.java deleted file mode 100644 index b6acac110..000000000 --- a/sa-token-demo/sa-token-demo-sso3-server/src/main/java/com/pj/sso/SsoServerController.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.pj.sso; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.ModelAndView; - -import com.ejlchina.okhttps.OkHttps; - -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.sso.SaSsoHandle; -import cn.dev33.satoken.sso.SaSsoUtil; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-Token-SSO Server端 Controller - * @author kong - * - */ -@RestController -public class SsoServerController { - - // SSO-Server端:处理所有SSO相关请求 - @RequestMapping("/sso/*") - public Object ssoRequest() { - return SaSsoHandle.serverRequest(); - } - - // 自定义接口:获取userinfo - @RequestMapping("/sso/userinfo") - public Object userinfo(String loginId, String secretkey) { - System.out.println("---------------- 获取userinfo --------"); - - // 校验调用秘钥 - SaSsoUtil.checkSecretkey(secretkey); - - // 自定义返回结果(模拟) - return SaResult.ok() - .set("id", loginId) - .set("name", "linxiaoyu") - .set("sex", "女") - .set("age", 18); - } - - // 配置SSO相关参数 - @Autowired - private void configSso(SaTokenConfig cfg) { - cfg.sso - // SSO-Server端:未登录时返回的View - .setNotLoginView(() -> { - return new ModelAndView("sa-login.html"); - }) - // SSO-Server端:登录函数 - .setDoLoginHandle((name, pwd) -> { - // 此处仅做模拟登录,真实环境应该查询数据进行登录 - if("sa".equals(name) && "123456".equals(pwd)) { - StpUtil.login(10001); - return SaResult.ok("登录成功!"); - } - return SaResult.error("登录失败!"); - }) - // 配置Http请求处理器 - .setSendHttp(url -> { - // 此处为了提高响应速度这里可将sync换为async - return OkHttps.sync(url).get(); - }) - ; - } - - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); - } - -} diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/application.yml deleted file mode 100644 index 87e4209c4..000000000 --- a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/application.yml +++ /dev/null @@ -1,48 +0,0 @@ -# 端口 -server: - port: 9000 - -# Sa-Token配置 -sa-token: - # SSO-相关配置 - sso: - # Ticket有效期 (单位: 秒),默认五分钟 - ticket-timeout: 300 - # 所有允许的授权回调地址 - allow-url: http://sa-sso-client1.com:9001/sso/login, http://sa-sso-client2.com:9001/sso/login, http://sa-sso-client3.com:9001/sso/login - # 接口调用秘钥(用于SSO模式三的单点注销功能) - secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor - # 使用Http请求校验ticket - is-http: true - # 打开单点注销功能 - is-slo: true - -spring: - # Redis配置 - redis: - # Redis数据库索引(默认为0) - database: 5 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - password: - # 连接超时时间(毫秒) - timeout: 10ms - lettuce: - pool: - # 连接池最大连接数 - max-active: 200 - # 连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - # 连接池中的最大空闲连接 - max-idle: 10 - # 连接池中的最小空闲连接 - min-idle: 0 - - - - - - \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/login.js b/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/login.js deleted file mode 100644 index f581ee786..000000000 --- a/sa-token-demo/sa-token-demo-sso3-server/src/main/resources/static/sa-res/login.js +++ /dev/null @@ -1,65 +0,0 @@ -// sa -var sa = {}; - -// 打开loading -sa.loading = function(msg) { - layer.closeAll(); // 开始前先把所有弹窗关了 - return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); -}; - -// 隐藏loading -sa.hideLoading = function() { - layer.closeAll(); -}; - - -// ----------------------------------- 登录事件 ----------------------------------- - -$('.login-btn').click(function(){ - sa.loading("正在登录..."); - // 开始登录 - setTimeout(function() { - $.ajax({ - url: "sso/doLogin", - type: "post", - data: { - name: $('[name=name]').val(), - pwd: $('[name=pwd]').val() - }, - dataType: 'json', - success: function(res){ - console.log('返回数据:', res); - sa.hideLoading(); - if(res.code == 200) { - layer.msg('登录成功', {anim: 0, icon: 6 }); - setTimeout(function() { - location.reload(); - }, 800) - } else { - layer.msg(res.msg, {anim: 6, icon: 2 }); - } - }, - error: function(xhr, type, errorThrown){ - sa.hideLoading(); - if(xhr.status == 0){ - return layer.alert('无法连接到服务器,请检查网络'); - } - return layer.alert("异常:" + JSON.stringify(xhr)); - } - }); - }, 400); -}); - -// 绑定回车事件 -$('[name=name],[name=pwd]').bind('keypress', function(event){ - if(event.keyCode == "13") { - $('.login-btn').click(); - } -}); - -// 输入框获取焦点 -$("[name=name]").focus(); - -// 打印信息 -var str = "This page is provided by Sa-Token, Please refer to: " + "http://sa-token.dev33.cn/"; -console.log(str); diff --git a/sa-token-demo/sa-token-demo-test/pom.xml b/sa-token-demo/sa-token-demo-test/pom.xml new file mode 100644 index 000000000..b30cfda17 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/pom.xml @@ -0,0 +1,144 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-test + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + + 1.45.0 + com.pj.SaTokenApplication + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + cn.hutool + hutool-all + 5.8.36 + + + + + + + + cn.dev33 + sa-token-redis-template + ${sa-token.version} + + + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-sign + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + + + + src/main/java + + **/*.xml + + + + src/main/resources + + **/*.* + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + lib/ + ${java.run.main.class} + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + package + + copy-dependencies + + + + ${project.build.directory}/lib + + + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/SaTokenApplication.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/SaTokenApplication.java new file mode 100644 index 000000000..90c205224 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/SaTokenApplication.java @@ -0,0 +1,21 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +/** + * Sa-Token 测试 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/GlobalException.java new file mode 100644 index 000000000..04baeedeb --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/GlobalException.java @@ -0,0 +1,56 @@ +package com.pj.current; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.pj.util.AjaxJson; + +import cn.dev33.satoken.exception.DisableServiceException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { + + // 打印堆栈,以供调试 + System.out.println("全局异常---------------"); + e.printStackTrace(); + + // 不同异常返回不同状态码 + AjaxJson aj = null; + if (e instanceof NotLoginException) { // 如果是未登录异常 + NotLoginException ee = (NotLoginException) e; + aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); + } + else if(e instanceof NotRoleException) { // 如果是角色异常 + NotRoleException ee = (NotRoleException) e; + aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); + } + else if(e instanceof NotPermissionException) { // 如果是权限异常 + NotPermissionException ee = (NotPermissionException) e; + aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); + } + else if(e instanceof DisableServiceException) { // 如果是被封禁异常 + DisableServiceException ee = (DisableServiceException) e; + aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); + } + else { // 普通异常, 输出:500 + 异常信息 + aj = AjaxJson.getError(e.getMessage()); + } + + // 返回给前端 + return aj; + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/NotFoundHandle.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/NotFoundHandle.java new file mode 100644 index 000000000..0c5fa56bd --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/NotFoundHandle.java @@ -0,0 +1,27 @@ +package com.pj.current; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.util.SaResult; + +/** + * 处理 404 + * @author click33 + */ +@RestController +public class NotFoundHandle implements ErrorController { + + @RequestMapping("/error") + public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setStatus(200); + return SaResult.get(404, "not found", null); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysRole.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysRole.java new file mode 100644 index 000000000..9048c0e77 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysRole.java @@ -0,0 +1,64 @@ +package com.pj.model; + +/** + * Role 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysRole { +// +// public SysRole() { +// } +// +// public SysRole(long id, String name) { +// super(); +// this.id = id; +// this.name = name; +// } +// +// +// /** +// * 角色id +// */ +// private long id; +// +// /** +// * 角色名称 +// */ +// private String name; +// +// /** +// * @return id +// */ +// public long getId() { +// return id; +// } +// +// /** +// * @param id 要设置的 id +// */ +// public void setId(long id) { +// this.id = id; +// } +// +// /** +// * @return name +// */ +// public String getName() { +// return name; +// } +// +// /** +// * @param name 要设置的 name +// */ +// public void setName(String name) { +// this.name = name; +// } +// +// @Override +// public String toString() { +// return "SysRole [id=" + id + ", name=" + name + "]"; +// } +// +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysUser.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysUser.java new file mode 100644 index 000000000..b130f0094 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysUser.java @@ -0,0 +1,103 @@ +package com.pj.model; + +/** + * User 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysUser { + + public SysUser() { + } + + public SysUser(long id, String name, int age) { + super(); + this.id = id; + this.name = name; + this.age = age; + } + + + /** + * 用户id + */ + private long id; + + /** + * 用户名称 + */ + private String name; + + /** + * 用户年龄 + */ + private int age; + + /** + * 用户角色 + */ + private SysRole role; + + /** + * @return id + */ + public long getId() { + return id; + } + + /** + * @param id 要设置的 id + */ + public void setId(long id) { + this.id = id; + } + + /** + * @return name + */ + public String getName() { + return name; + } + + /** + * @param name 要设置的 name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return age + */ + public int getAge() { + return age; + } + + /** + * @param age 要设置的 age + */ + public void setAge(int age) { + this.age = age; + } + + public SysRole getRole() { + return role; + } + + public SysUser setRole(SysRole role) { + this.role = role; + return this; + } + + @Override + public String toString() { + return "SysUser{" + + "id=" + id + + ", name='" + name + '\'' + + ", age=" + age + + ", role=" + role + + '}'; + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaLogForSlf4j.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaLogForSlf4j.java new file mode 100644 index 000000000..106506c79 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaLogForSlf4j.java @@ -0,0 +1,48 @@ +package com.pj.satoken; + +import cn.dev33.satoken.log.SaLog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 将 Sa-Token log 信息转接到 Slf4j + * + * @author click33 + * @since 2022-11-2 + */ +//@Component +public class SaLogForSlf4j implements SaLog { + + Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); + + @Override + public void trace(String str, Object... args) { + log.trace(str, args); + } + + @Override + public void debug(String str, Object... args) { + log.debug(str, args); + } + + @Override + public void info(String str, Object... args) { + log.info(str, args); + } + + @Override + public void warn(String str, Object... args) { + log.warn(str, args); + } + + @Override + public void error(String str, Object... args) { + log.error(str, args); + } + + @Override + public void fatal(String str, Object... args) { + log.error(str, args); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..4173e4035 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,125 @@ +package com.pj.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.plugin.SaTokenPluginHolder; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +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("/**"); + } + + /** + * 注册 [Sa-Token 全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 [拦截路由] 与 [放行路由] + .addInclude("/**")// .addExclude("/favicon.ico") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + // 输出 API 请求日志,方便调试代码 + // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + e.printStackTrace(); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行 + .setBeforeAuth(obj -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + ; + }) + ; + } + + /** + * CORS 跨域处理 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + + /** + * 注册插件 + */ + @Bean + public SaTokenPluginHolder getSaTokenPluginHolder() { + System.out.println("自定义插件安装钩子函数..."); + + return SaTokenPluginHolder.instance +// .onBeforeInstall(SaTokenPluginForJackson.class, plugin -> { +// System.out.println("SaTokenPluginForJackson 插件安装前置钩子..."); +// }) +// +// .onAfterInstall(SaTokenPluginForJackson.class, plugin -> { +// System.out.println("SaTokenPluginForJackson 插件安装后置钩子..."); +// }) +// +// .onAfterInstall(SaTokenPluginForJackson.class, plugin -> { +// System.out.println("SaTokenPluginForJackson 插件安装后置钩子2..."); +// }) + +// .onInstall(SaTokenPluginForJackson.class, plugin -> { +// System.out.println("注册 install 钩子函数后,插件的默认安装行为将不再执行 ..."); +// }) + + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..6cfd8edb7 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,43 @@ +package com.pj.satoken; + +import cn.dev33.satoken.stp.StpInterface; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpUserUtil.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpUserUtil.java new file mode 100644 index 000000000..cc6cc9006 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpUserUtil.java @@ -0,0 +1,1453 @@ +package com.pj.satoken; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaTwoParamFunction; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.SaTerminalInfo; +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; + +import java.util.List; + +/** + * 【User账号体系】Sa-Token 权限认证工具类 + * + * @author click33 + * @since 1.0.0 + */ +public class StpUserUtil { + + private StpUserUtil() {} + + /** + * 多账号体系下的类型标识 + */ + public static final String TYPE = "user"; + + /** + * 底层使用的 StpLogic 对象 + */ + public static StpLogic stpLogic = new StpLogic(TYPE); + + /** + * 获取当前 StpLogic 的账号类型 + * + * @return / + */ + public static String getLoginType(){ + return stpLogic.getLoginType(); + } + + /** + * 安全的重置 StpLogic 对象 + * + *
1、更改此账户的 StpLogic 对象 + *
2、put 到全局 StpLogic 集合中 + *
3、发送日志 + * + * @param newStpLogic / + */ + public static void setStpLogic(StpLogic newStpLogic) { + // 1、重置此账户的 StpLogic 对象 + stpLogic = newStpLogic; + + // 2、添加到全局 StpLogic 集合中 + // 以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic + SaManager.putStpLogic(newStpLogic); + + // 3、$$ 发布事件:更新了 stpLogic 对象 + SaTokenEventCenter.doSetStpLogic(stpLogic); + } + + /** + * 获取 StpLogic 对象 + * + * @return / + */ + public static StpLogic getStpLogic() { + return stpLogic; + } + + + // ------------------- 获取 token 相关 ------------------- + + /** + * 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀 + * + * @return / + */ + public static String getTokenName() { + return stpLogic.getTokenName(); + } + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + */ + public static void setTokenValue(String tokenValue){ + stpLogic.setTokenValue(tokenValue); + } + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + * @param cookieTimeout Cookie存活时间(秒) + */ + public static void setTokenValue(String tokenValue, int cookieTimeout){ + stpLogic.setTokenValue(tokenValue, cookieTimeout); + } + + /** + * 在当前会话写入指定 token 值 + * + * @param tokenValue token 值 + * @param loginParameter 登录参数 + */ + public static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){ + stpLogic.setTokenValue(tokenValue, loginParameter); + } + + /** + * 获取当前请求的 token 值 + * + * @return 当前tokenValue + */ + public static String getTokenValue() { + return stpLogic.getTokenValue(); + } + + /** + * 获取当前请求的 token 值 (不裁剪前缀) + * + * @return / + */ + public static String getTokenValueNotCut(){ + return stpLogic.getTokenValueNotCut(); + } + + /** + * 获取当前会话的 token 参数信息 + * + * @return token 参数信息 + */ + public static SaTokenInfo getTokenInfo() { + return stpLogic.getTokenInfo(); + } + + + // ------------------- 登录相关操作 ------------------- + + // --- 登录 + + /** + * 会话登录 + * + * @param id 账号id,建议的类型:(long | int | String) + */ + public static void login(Object id) { + stpLogic.login(id); + } + + /** + * 会话登录,并指定登录设备类型 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param deviceType 设备类型 + */ + public static void login(Object id, String deviceType) { + stpLogic.login(id, deviceType); + } + + /** + * 会话登录,并指定是否 [记住我] + * + * @param id 账号id,建议的类型:(long | int | String) + * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 + */ + public static void login(Object id, boolean isLastingCookie) { + stpLogic.login(id, isLastingCookie); + } + + /** + * 会话登录,并指定此次登录 token 的有效期, 单位:秒 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param timeout 此次登录 token 的有效期, 单位:秒 + */ + public static void login(Object id, long timeout) { + stpLogic.login(id, timeout); + } + + /** + * 会话登录,并指定所有登录参数 Model + * + * @param id 账号id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + */ + public static void login(Object id, SaLoginParameter loginParameter) { + stpLogic.login(id, loginParameter); + } + + /** + * 创建指定账号 id 的登录会话数据 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public static String createLoginSession(Object id) { + return stpLogic.createLoginSession(id); + } + + /** + * 创建指定账号 id 的登录会话数据 + * + * @param id 账号id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + * @return 返回会话令牌 + */ + public static String createLoginSession(Object id, SaLoginParameter loginParameter) { + return stpLogic.createLoginSession(id, loginParameter); + } + + /** + * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 + * + * @param id 账号id,建议的类型:(long | int | String) + * @return 返回会话令牌 + */ + public static String getOrCreateLoginSession(Object id) { + return stpLogic.getOrCreateLoginSession(id); + } + + // --- 注销 (根据 token) + + /** + * 在当前客户端会话注销 + */ + public static void logout() { + stpLogic.logout(); + } + + /** + * 在当前客户端会话注销,根据注销参数 + */ + public static void logout(SaLogoutParameter logoutParameter) { + stpLogic.logout(logoutParameter); + } + + /** + * 注销下线,根据指定 token + * + * @param tokenValue 指定 token + */ + public static void logoutByTokenValue(String tokenValue) { + stpLogic.logoutByTokenValue(tokenValue); + } + + /** + * 注销下线,根据指定 token、注销参数 + * + * @param tokenValue 指定 token + * @param logoutParameter / + */ + public static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.logoutByTokenValue(tokenValue, logoutParameter); + } + + /** + * 踢人下线,根据指定 token + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param tokenValue 指定 token + */ + public static void kickoutByTokenValue(String tokenValue) { + stpLogic.kickoutByTokenValue(tokenValue); + } + + /** + * 踢人下线,根据指定 token、注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param tokenValue 指定 token + * @param logoutParameter 注销参数 + */ + public static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.kickoutByTokenValue(tokenValue, logoutParameter); + } + + /** + * 顶人下线,根据指定 token + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param tokenValue 指定 token + */ + public static void replacedByTokenValue(String tokenValue) { + stpLogic.replacedByTokenValue(tokenValue); + } + + /** + * 顶人下线,根据指定 token、注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param tokenValue 指定 token + * @param logoutParameter / + */ + public static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + stpLogic.replacedByTokenValue(tokenValue, logoutParameter); + } + + // --- 注销 (根据 loginId) + + /** + * 会话注销,根据账号id + * + * @param loginId 账号id + */ + public static void logout(Object loginId) { + stpLogic.logout(loginId); + } + + /** + * 会话注销,根据账号id 和 设备类型 + * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型) + */ + public static void logout(Object loginId, String deviceType) { + stpLogic.logout(loginId, deviceType); + } + + /** + * 会话注销,根据账号id 和 注销参数 + * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void logout(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.logout(loginId, logoutParameter); + } + + /** + * 踢人下线,根据账号id + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + */ + public static void kickout(Object loginId) { + stpLogic.kickout(loginId); + } + + /** + * 踢人下线,根据账号id 和 设备类型 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型) + */ + public static void kickout(Object loginId, String deviceType) { + stpLogic.kickout(loginId, deviceType); + } + + /** + * 踢人下线,根据账号id 和 注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

+ * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void kickout(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.kickout(loginId, logoutParameter); + } + + /** + * 顶人下线,根据账号id + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + */ + public static void replaced(Object loginId) { + stpLogic.replaced(loginId); + } + + /** + * 顶人下线,根据账号id 和 设备类型 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + * @param deviceType 设备类型 (填 null 代表顶替该账号的所有设备类型) + */ + public static void replaced(Object loginId, String deviceType) { + stpLogic.replaced(loginId, deviceType); + } + + /** + * 顶人下线,根据账号id 和 注销参数 + *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

+ * + * @param loginId 账号id + * @param logoutParameter 注销参数 + */ + public static void replaced(Object loginId, SaLogoutParameter logoutParameter) { + stpLogic.replaced(loginId, logoutParameter); + } + + // --- 注销 (会话管理辅助方法) + + /** + * 在 Account-Session 上移除 Terminal 信息 (注销下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByLogout(session, terminal); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByKickout(session, terminal); + } + + /** + * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式) + * @param session / + * @param terminal / + */ + public static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) { + stpLogic.removeTerminalByReplaced(session, terminal); + } + + + // 会话查询 + + /** + * 判断当前会话是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ + public static boolean isLogin() { + return stpLogic.isLogin(); + } + + /** + * 判断指定账号是否已经登录 + * + * @return 已登录返回 true,未登录返回 false + */ + public static boolean isLogin(Object loginId) { + return stpLogic.isLogin(loginId); + } + + /** + * 检验当前会话是否已经登录,如未登录,则抛出异常 + */ + public static void checkLogin() { + stpLogic.checkLogin(); + } + + /** + * 获取当前会话账号id,如果未登录,则抛出异常 + * + * @return 账号id + */ + public static Object getLoginId() { + return stpLogic.getLoginId(); + } + + /** + * 获取当前会话账号id, 如果未登录,则返回默认值 + * + * @param 返回类型 + * @param defaultValue 默认值 + * @return 登录id + */ + public static T getLoginId(T defaultValue) { + return stpLogic.getLoginId(defaultValue); + } + + /** + * 获取当前会话账号id, 如果未登录,则返回null + * + * @return 账号id + */ + public static Object getLoginIdDefaultNull() { + return stpLogic.getLoginIdDefaultNull(); + } + + /** + * 获取当前会话账号id, 并转换为 String 类型 + * + * @return 账号id + */ + public static String getLoginIdAsString() { + return stpLogic.getLoginIdAsString(); + } + + /** + * 获取当前会话账号id, 并转换为 int 类型 + * + * @return 账号id + */ + public static int getLoginIdAsInt() { + return stpLogic.getLoginIdAsInt(); + } + + /** + * 获取当前会话账号id, 并转换为 long 类型 + * + * @return 账号id + */ + public static long getLoginIdAsLong() { + return stpLogic.getLoginIdAsLong(); + } + + /** + * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶、被冻结等状态,则返回 null + * + * @param tokenValue token + * @return 账号id + */ + public static Object getLoginIdByToken(String tokenValue) { + return stpLogic.getLoginIdByToken(tokenValue); + } + + /** + * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结),则返回 null + * + * @param tokenValue token + * @return 账号id + */ + public Object getLoginIdByTokenNotThinkFreeze(String tokenValue) { + return stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue); + } + + /** + * 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) + * + * @param key 键值 + * @return 对应的扩展数据 + */ + public static Object getExtra(String key) { + return stpLogic.getExtra(key); + } + + /** + * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) + * + * @param tokenValue 指定的 Token 值 + * @param key 键值 + * @return 对应的扩展数据 + */ + public static Object getExtra(String tokenValue, String key) { + return stpLogic.getExtra(tokenValue, key); + } + + + // ------------------- Account-Session 相关 ------------------- + + /** + * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * + * @param loginId 账号id + * @param isCreate 是否新建 + * @return SaSession 对象 + */ + public static SaSession getSessionByLoginId(Object loginId, boolean isCreate) { + return stpLogic.getSessionByLoginId(loginId, isCreate); + } + + /** + * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,则返回 null + * + * @param sessionId SessionId + * @return Session对象 + */ + public static SaSession getSessionBySessionId(String sessionId) { + return stpLogic.getSessionBySessionId(sessionId); + } + + /** + * 获取指定账号 id 的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @param loginId 账号id + * @return SaSession 对象 + */ + public static SaSession getSessionByLoginId(Object loginId) { + return stpLogic.getSessionByLoginId(loginId); + } + + /** + * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 + * + * @param isCreate 是否新建 + * @return Session对象 + */ + public static SaSession getSession(boolean isCreate) { + return stpLogic.getSession(isCreate); + } + + /** + * 获取当前已登录账号的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @return Session对象 + */ + public static SaSession getSession() { + return stpLogic.getSession(); + } + + + // ------------------- Token-Session 相关 ------------------- + + /** + * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @param tokenValue Token值 + * @return Session对象 + */ + public static SaSession getTokenSessionByToken(String tokenValue) { + return stpLogic.getTokenSessionByToken(tokenValue); + } + + /** + * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 + * + * @return Session对象 + */ + public static SaSession getTokenSession() { + return stpLogic.getTokenSession(); + } + + /** + * 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) + * + * @return Token-Session 对象 + */ + public static SaSession getAnonTokenSession() { + return stpLogic.getAnonTokenSession(); + } + + + // ------------------- Active-Timeout token 最低活跃度 验证相关 ------------------- + + /** + * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) + *

+ * 请注意: 即使 token 已被冻结 也可续签成功, + * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 + *

+ */ + public static void updateLastActiveToNow() { + stpLogic.updateLastActiveToNow(); + } + + /** + * 检查当前 token 是否已被冻结,如果是则抛出异常 + */ + public static void checkActiveTimeout() { + stpLogic.checkActiveTimeout(); + } + + + // ------------------- 过期时间相关 ------------------- + + /** + * 获取当前会话 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public static long getTokenTimeout() { + return stpLogic.getTokenTimeout(); + } + + /** + * 获取指定 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @param token 指定token + * @return token剩余有效时间 + */ + public static long getTokenTimeout(String token) { + return stpLogic.getTokenTimeout(token); + } + + /** + * 获取当前登录账号的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public static long getSessionTimeout() { + return stpLogic.getSessionTimeout(); + } + + /** + * 获取当前 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) + * + * @return token剩余有效时间 + */ + public static long getTokenSessionTimeout() { + return stpLogic.getTokenSessionTimeout(); + } + + /** + * 获取当前 token 剩余活跃有效期:当前 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) + * + * @return / + */ + public static long getTokenActiveTimeout() { + return stpLogic.getTokenActiveTimeout(); + } + + /** + * 重置当前 token 的 timeout 有效期 + * + * @param timeout 要修改成为的有效时间 (单位: 秒) + */ + public static void renewTimeout(long timeout) { + stpLogic.renewTimeout(timeout); + } + + /** + * 重置指定 token 的 timeout 有效期 + * + * @param tokenValue 指定 token + * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) + */ + public static void renewTimeout(String tokenValue, long timeout) { + stpLogic.renewTimeout(tokenValue, timeout); + } + + + // ------------------- 角色认证操作 ------------------- + + /** + * 获取:当前账号的角色集合 + * + * @return / + */ + public static List getRoleList() { + return stpLogic.getRoleList(); + } + + /** + * 获取:指定账号的角色集合 + * + * @param loginId 指定账号id + * @return / + */ + public static List getRoleList(Object loginId) { + return stpLogic.getRoleList(loginId); + } + + /** + * 判断:当前账号是否拥有指定角色, 返回 true 或 false + * + * @param role 角色 + * @return / + */ + public static boolean hasRole(String role) { + return stpLogic.hasRole(role); + } + + /** + * 判断:指定账号是否含有指定角色标识, 返回 true 或 false + * + * @param loginId 账号id + * @param role 角色标识 + * @return 是否含有指定角色标识 + */ + public static boolean hasRole(Object loginId, String role) { + return stpLogic.hasRole(loginId, role); + } + + /** + * 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] + * + * @param roleArray 角色标识数组 + * @return true或false + */ + public static boolean hasRoleAnd(String... roleArray){ + return stpLogic.hasRoleAnd(roleArray); + } + + /** + * 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] + * + * @param roleArray 角色标识数组 + * @return true或false + */ + public static boolean hasRoleOr(String... roleArray){ + return stpLogic.hasRoleOr(roleArray); + } + + /** + * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException + * + * @param role 角色标识 + */ + public static void checkRole(String role) { + stpLogic.checkRole(role); + } + + /** + * 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] + * + * @param roleArray 角色标识数组 + */ + public static void checkRoleAnd(String... roleArray){ + stpLogic.checkRoleAnd(roleArray); + } + + /** + * 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] + * + * @param roleArray 角色标识数组 + */ + public static void checkRoleOr(String... roleArray){ + stpLogic.checkRoleOr(roleArray); + } + + + // ------------------- 权限认证操作 ------------------- + + /** + * 获取:当前账号的权限码集合 + * + * @return / + */ + public static List getPermissionList() { + return stpLogic.getPermissionList(); + } + + /** + * 获取:指定账号的权限码集合 + * + * @param loginId 指定账号id + * @return / + */ + public static List getPermissionList(Object loginId) { + return stpLogic.getPermissionList(loginId); + } + + /** + * 判断:当前账号是否含有指定权限, 返回 true 或 false + * + * @param permission 权限码 + * @return 是否含有指定权限 + */ + public static boolean hasPermission(String permission) { + return stpLogic.hasPermission(permission); + } + + /** + * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false + * + * @param loginId 账号 id + * @param permission 权限码 + * @return 是否含有指定权限 + */ + public static boolean hasPermission(Object loginId, String permission) { + return stpLogic.hasPermission(loginId, permission); + } + + /** + * 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ] + * + * @param permissionArray 权限码数组 + * @return true 或 false + */ + public static boolean hasPermissionAnd(String... permissionArray){ + return stpLogic.hasPermissionAnd(permissionArray); + } + + /** + * 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] + * + * @param permissionArray 权限码数组 + * @return true 或 false + */ + public static boolean hasPermissionOr(String... permissionArray){ + return stpLogic.hasPermissionOr(permissionArray); + } + + /** + * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException + * + * @param permission 权限码 + */ + public static void checkPermission(String permission) { + stpLogic.checkPermission(permission); + } + + /** + * 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ] + * + * @param permissionArray 权限码数组 + */ + public static void checkPermissionAnd(String... permissionArray) { + stpLogic.checkPermissionAnd(permissionArray); + } + + /** + * 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] + * + * @param permissionArray 权限码数组 + */ + public static void checkPermissionOr(String... permissionArray) { + stpLogic.checkPermissionOr(permissionArray); + } + + + // ------------------- id 反查 token 相关操作 ------------------- + + /** + * 获取指定账号 id 的 token + *

+ * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + *

+ * + * @param loginId 账号id + * @return token值 + */ + public static String getTokenValueByLoginId(Object loginId) { + return stpLogic.getTokenValueByLoginId(loginId); + } + + /** + * 获取指定账号 id 指定设备类型端的 token + *

+ * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + *

+ * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return token值 + */ + public static String getTokenValueByLoginId(Object loginId, String deviceType) { + return stpLogic.getTokenValueByLoginId(loginId, deviceType); + } + + /** + * 获取指定账号 id 的 token 集合 + * + * @param loginId 账号id + * @return 此 loginId 的所有相关 token + */ + public static List getTokenValueListByLoginId(Object loginId) { + return stpLogic.getTokenValueListByLoginId(loginId); + } + + /** + * 获取指定账号 id 指定设备类型端的 token 集合 + * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return 此 loginId 的所有登录 token + */ + public static List getTokenValueListByLoginId(Object loginId, String deviceType) { + return stpLogic.getTokenValueListByLoginId(loginId, deviceType); + } + + /** + * 获取指定账号 id 已登录设备信息集合 + * + * @param loginId 账号id + * @return 此 loginId 的所有登录 token + */ + public static List getTerminalListByLoginId(Object loginId) { + return stpLogic.getTerminalListByLoginId(loginId); + } + + /** + * 获取指定账号 id 指定设备类型端的已登录设备信息集合 + * + * @param loginId 账号id + * @param deviceType 设备类型,填 null 代表不限设备类型 + * @return / + */ + public static List getTerminalListByLoginId(Object loginId, String deviceType) { + return stpLogic.getTerminalListByLoginId(loginId, deviceType); + } + + /** + * 获取指定账号 id 已登录设备信息集合,执行特定函数 + * + * @param loginId 账号id + * @param function 需要执行的函数 + */ + public static void forEachTerminalList(Object loginId, SaTwoParamFunction function) { + stpLogic.forEachTerminalList(loginId, function); + } + + /** + * 返回当前会话的登录设备类型 + * + * @return 当前令牌的登录设备类型 + */ + public static String getLoginDeviceType() { + return stpLogic.getLoginDeviceType(); + } + + /** + * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 + */ + public static String getLoginDeviceTypeByToken(String tokenValue) { + return stpLogic.getLoginDeviceTypeByToken(tokenValue); + } + + /** + * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 + * + * @return / + */ + public static long getTokenLastActiveTime() { + return stpLogic.getTokenLastActiveTime(); + } + + /** + * 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 + * @param deviceId / + * @return / + */ + public static boolean isTrustDeviceId(Object userId, String deviceId) { + return stpLogic.isTrustDeviceId(userId, deviceId); + } + + + + // ------------------- 会话管理 ------------------- + + /** + * 根据条件查询缓存中所有的 token + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return token集合 + */ + public static List searchTokenValue(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchTokenValue(keyword, start, size, sortType); + } + + /** + * 根据条件查询缓存中所有的 SessionId + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return sessionId集合 + */ + public static List searchSessionId(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchSessionId(keyword, start, size, sortType); + } + + /** + * 根据条件查询缓存中所有的 Token-Session-Id + * + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * + * @return sessionId集合 + */ + public static List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { + return stpLogic.searchTokenSessionId(keyword, start, size, sortType); + } + + + // ------------------- 账号封禁 ------------------- + + /** + * 封禁:指定账号 + *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) + * + * @param loginId 指定账号id + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disable(Object loginId, long time) { + stpLogic.disable(loginId, time); + } + + /** + * 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) + * + * @param loginId 账号id + * @return / + */ + public static boolean isDisable(Object loginId) { + return stpLogic.isDisable(loginId); + } + + /** + * 校验:指定账号是否已被封禁,如果被封禁则抛出异常 + * + * @param loginId 账号id + */ + public static void checkDisable(Object loginId) { + stpLogic.checkDisable(loginId); + } + + /** + * 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * + * @param loginId 账号id + * @return / + */ + public static long getDisableTime(Object loginId) { + return stpLogic.getDisableTime(loginId); + } + + /** + * 解封:指定账号 + * + * @param loginId 账号id + */ + public static void untieDisable(Object loginId) { + stpLogic.untieDisable(loginId); + } + + + // ------------------- 分类封禁 ------------------- + + /** + * 封禁:指定账号的指定服务 + *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) + * + * @param loginId 指定账号id + * @param service 指定服务 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disable(Object loginId, String service, long time) { + stpLogic.disable(loginId, service, time); + } + + /** + * 判断:指定账号的指定服务 是否已被封禁(true=已被封禁, false=未被封禁) + * + * @param loginId 账号id + * @param service 指定服务 + * @return / + */ + public static boolean isDisable(Object loginId, String service) { + return stpLogic.isDisable(loginId, service); + } + + /** + * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 + * + * @param loginId 账号id + * @param services 指定服务,可以指定多个 + */ + public static void checkDisable(Object loginId, String... services) { + stpLogic.checkDisable(loginId, services); + } + + /** + * 获取:指定账号 指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) + * + * @param loginId 账号id + * @param service 指定服务 + * @return see note + */ + public static long getDisableTime(Object loginId, String service) { + return stpLogic.getDisableTime(loginId, service); + } + + /** + * 解封:指定账号、指定服务 + * + * @param loginId 账号id + * @param services 指定服务,可以指定多个 + */ + public static void untieDisable(Object loginId, String... services) { + stpLogic.untieDisable(loginId, services); + } + + + // ------------------- 阶梯封禁 ------------------- + + /** + * 封禁:指定账号,并指定封禁等级 + * + * @param loginId 指定账号id + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disableLevel(Object loginId, int level, long time) { + stpLogic.disableLevel(loginId, level, time); + } + + /** + * 封禁:指定账号的指定服务,并指定封禁等级 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + */ + public static void disableLevel(Object loginId, String service, int level, long time) { + stpLogic.disableLevel(loginId, service, level, time); + } + + /** + * 判断:指定账号是否已被封禁到指定等级 + * + * @param loginId 指定账号id + * @param level 指定封禁等级 + * @return / + */ + public static boolean isDisableLevel(Object loginId, int level) { + return stpLogic.isDisableLevel(loginId, level); + } + + /** + * 判断:指定账号的指定服务,是否已被封禁到指定等级 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 指定封禁等级 + * @return / + */ + public static boolean isDisableLevel(Object loginId, String service, int level) { + return stpLogic.isDisableLevel(loginId, service, level); + } + + /** + * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) + * + * @param loginId 指定账号id + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + */ + public static void checkDisableLevel(Object loginId, int level) { + stpLogic.checkDisableLevel(loginId, level); + } + + /** + * 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常) + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + */ + public static void checkDisableLevel(Object loginId, String service, int level) { + stpLogic.checkDisableLevel(loginId, service, level); + } + + /** + * 获取:指定账号被封禁的等级,如果未被封禁则返回-2 + * + * @param loginId 指定账号id + * @return / + */ + public static int getDisableLevel(Object loginId) { + return stpLogic.getDisableLevel(loginId); + } + + /** + * 获取:指定账号的 指定服务 被封禁的等级,如果未被封禁则返回-2 + * + * @param loginId 指定账号id + * @param service 指定封禁服务 + * @return / + */ + public static int getDisableLevel(Object loginId, String service) { + return stpLogic.getDisableLevel(loginId, service); + } + + + // ------------------- 临时身份切换 ------------------- + + /** + * 临时切换身份为指定账号id + * + * @param loginId 指定loginId + */ + public static void switchTo(Object loginId) { + stpLogic.switchTo(loginId); + } + + /** + * 结束临时切换身份 + */ + public static void endSwitch() { + stpLogic.endSwitch(); + } + + /** + * 判断当前请求是否正处于 [ 身份临时切换 ] 中 + * + * @return / + */ + public static boolean isSwitch() { + return stpLogic.isSwitch(); + } + + /** + * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 + * + * @param loginId 指定账号id + * @param function 要执行的方法 + */ + public static void switchTo(Object loginId, SaFunction function) { + stpLogic.switchTo(loginId, function); + } + + + // ------------------- 二级认证 ------------------- + + /** + * 在当前会话 开启二级认证 + * + * @param safeTime 维持时间 (单位: 秒) + */ + public static void openSafe(long safeTime) { + stpLogic.openSafe(safeTime); + } + + /** + * 在当前会话 开启二级认证 + * + * @param service 业务标识 + * @param safeTime 维持时间 (单位: 秒) + */ + public static void openSafe(String service, long safeTime) { + stpLogic.openSafe(service, safeTime); + } + + /** + * 判断:当前会话是否处于二级认证时间内 + * + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public static boolean isSafe() { + return stpLogic.isSafe(); + } + + /** + * 判断:当前会话 是否处于指定业务的二级认证时间内 + * + * @param service 业务标识 + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public static boolean isSafe(String service) { + return stpLogic.isSafe(service); + } + + /** + * 判断:指定 token 是否处于二级认证时间内 + * + * @param tokenValue Token 值 + * @param service 业务标识 + * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 + */ + public static boolean isSafe(String tokenValue, String service) { + return stpLogic.isSafe(tokenValue, service); + } + + /** + * 校验:当前会话是否已通过二级认证,如未通过则抛出异常 + */ + public static void checkSafe() { + stpLogic.checkSafe(); + } + + /** + * 校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常 + * + * @param service 业务标识 + */ + public static void checkSafe(String service) { + stpLogic.checkSafe(service); + } + + /** + * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) + * + * @return 剩余有效时间 + */ + public static long getSafeTime() { + return stpLogic.getSafeTime(); + } + + /** + * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) + * + * @param service 业务标识 + * @return 剩余有效时间 + */ + public static long getSafeTime(String service) { + return stpLogic.getSafeTime(service); + } + + /** + * 在当前会话 结束二级认证 + */ + public static void closeSafe() { + stpLogic.closeSafe(); + } + + /** + * 在当前会话 结束指定业务标识的二级认证 + * + * @param service 业务标识 + */ + public static void closeSafe(String service) { + stpLogic.closeSafe(service); + } + + + // ------------------- Bean 对象、字段代理 ------------------- + + /** + * 根据当前配置对象创建一个 SaLoginParameter 对象 + * + * @return / + */ + public static SaLoginParameter createSaLoginParameter() { + return stpLogic.createSaLoginParameter(); + } + + + // ------------------- 过期方法 ------------------- + + /** + *

请更换为 getLoginDeviceType

+ * 返回当前会话的登录设备类型 + * + * @return 当前令牌的登录设备类型 + */ + @Deprecated + public static String getLoginDevice() { + return stpLogic.getLoginDevice(); + } + + /** + *

请更换为 getLoginDeviceTypeByToken

+ * 返回指定 token 会话的登录设备类型 + * + * @param tokenValue 指定token + * @return 当前令牌的登录设备类型 + */ + @Deprecated + public static String getLoginDeviceByToken(String tokenValue) { + return stpLogic.getLoginDeviceByToken(tokenValue); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/AtController.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/AtController.java new file mode 100644 index 000000000..d36388e9e --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/AtController.java @@ -0,0 +1,80 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.annotation.SaCheckHttpBasic; +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; + +/** + * 注解鉴权测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/at/") +public class AtController { + + // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin + @SaCheckLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + return SaResult.ok(); + } + + // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission + @SaCheckPermission("user-add") + @RequestMapping("checkPermission") + public SaResult checkPermission() { + return SaResult.ok(); + } + + // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd + @SaCheckPermission({"user-add", "user-delete", "user-update"}) + @RequestMapping("checkPermissionAnd") + public SaResult checkPermissionAnd() { + return SaResult.ok(); + } + + // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr + @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) + @RequestMapping("checkPermissionOr") + public SaResult checkPermissionOr() { + return SaResult.ok(); + } + + // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole + @SaCheckRole("admin") + @RequestMapping("checkRole") + public SaResult checkRole() { + return SaResult.ok(); + } + + // 完成二级认证 ---- http://localhost:8081/at/openSafe + @RequestMapping("openSafe") + public SaResult openSafe() { + StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 + return SaResult.ok(); + } + + // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe + @SaCheckSafe + @RequestMapping("checkSafe") + public SaResult checkSafe() { + return SaResult.ok(); + } + + // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic + @SaCheckHttpBasic(account = "sa:123456") + @RequestMapping("checkBasic") + public SaResult checkBasic() { + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..02440fdd2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,71 @@ +package com.pj.test; + +import cn.dev33.satoken.session.SaTerminalInfo; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + StpUtil.getTokenSession(); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 校验登录 ---- http://localhost:8081/acc/checkLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + StpUtil.checkLogin(); + return SaResult.ok(); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 查询账号登录设备信息 ---- http://localhost:8081/acc/terminalInfo + @RequestMapping("terminalInfo") + public SaResult terminalInfo() { + System.out.println("账号 10001 登录设备信息:"); + List terminalList = StpUtil.getTerminalListByLoginId(10001); + for (SaTerminalInfo ter : terminalList) { + System.out.println("登录index=" + ter.getIndex() + ", 设备type=" + ter.getDeviceType() + ", token=" + ter.getTokenValue() + ", 登录time=" + ter.getCreateTime()); + } + return SaResult.data(terminalList); + } + + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.login(10001, SaLoginParameter.create().setIsConcurrent(false)); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/StressTestController.java new file mode 100644 index 000000000..b42731093 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/StressTestController.java @@ -0,0 +1,61 @@ +package com.pj.test; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.pj.util.Ttime; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 压力测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/s-test/") +public class StressTestController { + + // 测试 浏览器访问: http://localhost:8081/s-test/login + // 测试前,请先将 is-read-cookie 配置为 false + @RequestMapping("login") + public SaResult login() { +// StpUtil.getTokenSession().logout(); +// StpUtil.logoutByLoginId(10001); + + int count = 10; // 循环多少轮 + int loginCount = 10000; // 每轮循环多少次 + + // 循环10次 取平均时间 + List list = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + System.out.println("\n---------------------第" + i + "轮---------------------"); + Ttime t = new Ttime().start(); + // 每次登录的次数 + for (int j = 1; j <= loginCount; j++) { + StpUtil.login("1000" + j, "PC-" + j); + if(j % 1000 == 0) { + System.out.println("已登录:" + j); + } + } + t.end(); + list.add((t.returnMs() + 0.0) / 1000); + System.out.println("第" + i + "轮" + "用时:" + t.toString()); + } +// System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); + + System.out.println("\n---------------------测试结果---------------------"); + System.out.println(list.size() + "次测试: " + list); + double ss = 0; + for (int i = 0; i < list.size(); i++) { + ss += list.get(i); + } + System.out.println("平均用时: " + ss / list.size()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/Test2Controller.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/Test2Controller.java new file mode 100644 index 000000000..57697d3a4 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/Test2Controller.java @@ -0,0 +1,31 @@ +package com.pj.test; + +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +public class Test2Controller { + + // 测试登录 ---- http://localhost:8081/test + @RequestMapping("/test") + public SaResult test2() { + + System.out.println(SpringMVCUtil.getRequest()); + System.out.println(SaTokenContextServletUtil.getRequest()); + +// StpUtil.login(30003); +// System.out.println(StpUtil.getSession().timeout()); +// System.out.println(StpUtil.getStpLogic().getTokenSession(false)); + + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..5a8ec15af --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,101 @@ +package com.pj.test; + +import cn.dev33.satoken.annotation.SaCheckHttpDigest; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.SaTerminalInfo; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.List; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + // 测试登录 ---- http://localhost:8081/test/login + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue = "10001") long id, String dt) { + StpUtil.login(id, new SaLoginParameter() + .setIsConcurrent(true) + .setIsShare(false) +// .setDeviceType(dt) + .setMaxLoginCount(4) + .setMaxTryTimes(12) + .setTerminalExtra("deviceSimpleTitle", "XiaoMi 15 Ultra") + .setTerminalExtra("loginAddress", "浙江省杭州市西湖区") + .setTerminalExtra("loginIp", "127.0.0.1") + .setTerminalExtra("loginTime", SaFoxUtil.formatDate(new Date())) + ); + StpUtil.getTokenSession(); + return SaResult.ok("登录成功"); + } + + // 测试 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("------------进来了 " + SaFoxUtil.formatDate(new Date())); + + + // 获取所有已登录的会话id + List sessionIdList = StpUtil.searchSessionId(null, 0, -1, false); + + for (String sessionId : sessionIdList) { + + // 根据会话id,查询对应的 SaSession 对象,此处一个 SaSession 对象即代表一个登录的账号 + SaSession session = StpUtil.getSessionBySessionId(sessionId); + + // 查询这个账号都在哪些设备登录了,依据上面的示例,账号A 的 SaTerminalInfo 数量是 3,账号B 的 SaTerminalInfo 数量是 2 + List terminalList = session.terminalListCopy(); + System.out.println("会话id:" + sessionId + ",共在 " + terminalList.size() + " 设备登录"); + } + + + // 返回 + return SaResult.data(null); + } + + // 测试 浏览器访问: http://localhost:8081/test/test2 + @RequestMapping("test2") + public SaResult test2() { + return SaResult.ok(); + } + + // 测试 浏览器访问: http://localhost:8081/test/getRequestPath + @RequestMapping("getRequestPath") + public SaResult getRequestPath() { + System.out.println("------------ 测试访问路径获取 "); + System.out.println("SpringMVCUtil.getRequest().getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); + System.out.println("SaHolder.getRequest().getRequestPath() " + SaHolder.getRequest().getRequestPath()); + return SaResult.ok(); + } + + // 测试 Http Digest 认证 浏览器访问: http://localhost:8081/test/testDigest + @SaCheckHttpDigest("sa:123456") + @RequestMapping("testDigest") + public SaResult testDigest() { + // SaHttpDigestUtil.check("sa", "123456"); + // 返回 + return SaResult.data(null); + } + + // 测试注销 浏览器访问: http://localhost:8081/test/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.data(null); + } + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/AjaxJson.java new file mode 100644 index 000000000..768d05784 --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/AjaxJson.java @@ -0,0 +1,162 @@ +package com.pj.util; + +import java.io.Serializable; +import java.util.List; + + +/** + * ajax请求返回Json格式数据的封装 + */ +public class AjaxJson implements Serializable{ + + private static final long serialVersionUID = 1L; // 序列化版本号 + + public static final int CODE_SUCCESS = 200; // 成功状态码 + public static final int CODE_ERROR = 500; // 错误状态码 + public static final int CODE_WARNING = 501; // 警告状态码 + public static final int CODE_NOT_JUR = 403; // 无权限状态码 + public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 + public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 + + public int code; // 状态码 + public String msg; // 描述信息 + public Object data; // 携带对象 + public Long dataCount; // 数据总数,用于分页 + + /** + * 返回code + * @return + */ + public int getCode() { + return this.code; + } + + /** + * 给msg赋值,连缀风格 + */ + public AjaxJson setMsg(String msg) { + this.msg = msg; + return this; + } + public String getMsg() { + return this.msg; + } + + /** + * 给data赋值,连缀风格 + */ + public AjaxJson setData(Object data) { + this.data = data; + return this; + } + + /** + * 将data还原为指定类型并返回 + */ + @SuppressWarnings("unchecked") + public T getData(Class cs) { + return (T) data; + } + + // ============================ 构建 ================================== + + public AjaxJson(int code, String msg, Object data, Long dataCount) { + this.code = code; + this.msg = msg; + this.data = data; + this.dataCount = dataCount; + } + + // 返回成功 + public static AjaxJson getSuccess() { + return new AjaxJson(CODE_SUCCESS, "ok", null, null); + } + public static AjaxJson getSuccess(String msg) { + return new AjaxJson(CODE_SUCCESS, msg, null, null); + } + public static AjaxJson getSuccess(String msg, Object data) { + return new AjaxJson(CODE_SUCCESS, msg, data, null); + } + public static AjaxJson getSuccessData(Object data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + public static AjaxJson getSuccessArray(Object... data) { + return new AjaxJson(CODE_SUCCESS, "ok", data, null); + } + + // 返回失败 + public static AjaxJson getError() { + return new AjaxJson(CODE_ERROR, "error", null, null); + } + public static AjaxJson getError(String msg) { + return new AjaxJson(CODE_ERROR, msg, null, null); + } + + // 返回警告 + public static AjaxJson getWarning() { + return new AjaxJson(CODE_ERROR, "warning", null, null); + } + public static AjaxJson getWarning(String msg) { + return new AjaxJson(CODE_WARNING, msg, null, null); + } + + // 返回未登录 + public static AjaxJson getNotLogin() { + return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); + } + + // 返回没有权限的 + public static AjaxJson getNotJur(String msg) { + return new AjaxJson(CODE_NOT_JUR, msg, null, null); + } + + // 返回一个自定义状态码的 + public static AjaxJson get(int code, String msg){ + return new AjaxJson(code, msg, null, null); + } + + // 返回分页和数据的 + public static AjaxJson getPageData(Long dataCount, Object data){ + return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); + } + + // 返回,根据受影响行数的(大于0=ok,小于0=error) + public static AjaxJson getByLine(int line){ + if(line > 0){ + return getSuccess("ok", line); + } + return getError("error").setData(line); + } + + // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) + public static AjaxJson getByBoolean(boolean b){ + return b ? getSuccess("ok") : getError("error"); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @SuppressWarnings("rawtypes") + @Override + public String toString() { + String data_string = null; + if(data == null){ + + } else if(data instanceof List){ + data_string = "List(length=" + ((List)data).size() + ")"; + } else { + data_string = data.toString(); + } + return "{" + + "\"code\": " + this.getCode() + + ", \"msg\": \"" + this.getMsg() + "\"" + + ", \"data\": " + data_string + + ", \"dataCount\": " + dataCount + + "}"; + } + + + + + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/Ttime.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/Ttime.java new file mode 100644 index 000000000..dec4f1cbe --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/Ttime.java @@ -0,0 +1,63 @@ +package com.pj.util; + + +/** + * 用于测试用时 + * @author click33 + * + */ +public class Ttime { + + private long start=0; //开始时间 + private long end=0; //结束时间 + + public static Ttime t = new Ttime(); //static快捷使用 + + /** + * 开始计时 + * @return + */ + public Ttime start() { + start=System.currentTimeMillis(); + return this; + } + + + /** + * 结束计时 + */ + public Ttime end() { + end=System.currentTimeMillis(); + return this; + } + + + /** + * 返回所用毫秒数 + */ + public long returnMs() { + return end-start; + } + + /** + * 格式化输出结果 + */ + public void outTime() { + System.out.println(this.toString()); + } + + /** + * 结束并格式化输出结果 + */ + public void endOutTime() { + this.end().outTime(); + } + + @Override + public String toString() { + return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s + } + + + +} diff --git a/sa-token-demo/sa-token-demo-test/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-test/src/main/resources/application.yml new file mode 100644 index 000000000..95e98258f --- /dev/null +++ b/sa-token-demo/sa-token-demo-test/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-thymeleaf/pom.xml b/sa-token-demo/sa-token-demo-thymeleaf/pom.xml new file mode 100644 index 000000000..ea213cb27 --- /dev/null +++ b/sa-token-demo/sa-token-demo-thymeleaf/pom.xml @@ -0,0 +1,71 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-thymeleaf + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-thymeleaf + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-devtools + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + diff --git a/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/SaTokenThymeleafDemoApplication.java b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/SaTokenThymeleafDemoApplication.java new file mode 100644 index 000000000..dbbd2eb04 --- /dev/null +++ b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/SaTokenThymeleafDemoApplication.java @@ -0,0 +1,17 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +@SpringBootApplication +public class SaTokenThymeleafDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SaTokenThymeleafDemoApplication.class, args); + System.out.println("\n启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); + System.out.println("\n测试访问:http://localhost:8081/"); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..52881c60b --- /dev/null +++ b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,33 @@ +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.web.servlet.config.annotation.WebMvcConfigurer; +import org.thymeleaf.spring5.view.ThymeleafViewResolver; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.thymeleaf.dialect.SaTokenDialect; + + +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + * + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + + // Sa-Token 标签方言 (Thymeleaf版) + @Bean + public SaTokenDialect getSaTokenDialect() { + return new SaTokenDialect(); + } + + // 为 Thymeleaf 注入全局变量,以便在页面中调用 Sa-Token 的方法 + @Autowired + private void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) { + viewResolver.addStaticVariable("stp", StpUtil.stpLogic); + } + +} diff --git a/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..b6cc79f71 --- /dev/null +++ b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/GlobalException.java new file mode 100644 index 000000000..4992d3d1a --- /dev/null +++ b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/GlobalException.java @@ -0,0 +1,24 @@ +package com.pj.test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import cn.dev33.satoken.util.SaResult; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + // 全局异常拦截(拦截项目中的所有异常) + @ExceptionHandler + public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..c270a6e4a --- /dev/null +++ b/sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,40 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 测试 Controller + * + * @author click33 + */ +@RestController +public class TestController { + + // 首页 + @RequestMapping("/") + public Object index() { + return new ModelAndView("index.html"); + } + + // 登录 + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue="10001") String id) { + StpUtil.login(id); + StpUtil.getSession().set("name", "zhangsan"); + return SaResult.ok(); + } + + // 注销 + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/application.yml new file mode 100644 index 000000000..aac39af7f --- /dev/null +++ b/sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/application.yml @@ -0,0 +1,5 @@ +# 端口 +server: + port: 8081 + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/templates/index.html b/sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/templates/index.html new file mode 100644 index 000000000..c409348c5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/templates/index.html @@ -0,0 +1,37 @@ + + + + Sa-Token 集成 Thymeleaf 标签方言 + + + + +
+

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

+

当前是否登录:

+

+ 登录 + 注销 +

+ +

登录之后才能显示:value

+

不登录才能显示:value

+ +

具有角色 admin 才能显示:value

+

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

+

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

+

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

+ +

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

+

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

+

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

+

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

+ +

+ 从SaSession中取值: + +

+ +
+ + diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/pom.xml b/sa-token-demo/sa-token-demo-webflux-springboot3/pom.xml new file mode 100644 index 000000000..0a795b9ac --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/pom.xml @@ -0,0 +1,61 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-webflux-springboot3 + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 3.0.1 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + cn.dev33 + sa-token-reactor-spring-boot3-starter + ${sa-token.version} + + + + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/SaTokenWebfluxSpringboot3Application.java b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/SaTokenWebfluxSpringboot3Application.java new file mode 100644 index 000000000..e6390aa0e --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/SaTokenWebfluxSpringboot3Application.java @@ -0,0 +1,22 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token整合webflux 示例 (springboot3) + * + * @author click33 + * @since 2023年1月3日 + * + */ +@SpringBootApplication +public class SaTokenWebfluxSpringboot3Application { + + public static void main(String[] args) { + SpringApplication.run(SaTokenWebfluxSpringboot3Application.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/MyFilter.java b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/MyFilter.java new file mode 100644 index 000000000..5c0a4adf1 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/MyFilter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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 com.pj.satoken; + +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * 自定义过滤器 + */ +@Component +public class MyFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + System.out.println("进入自定义过滤器"); + + try { + // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 + SaReactorSyncHolder.setContext(exchange); + System.out.println(StpUtil.isLogin()); + } + finally { + SaReactorSyncHolder.clearContext(); + } + + return chain.filter(exchange); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..8524f11f3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,39 @@ +package com.pj.satoken; + +import cn.dev33.satoken.reactor.filter.SaReactorFilter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + * + */ +@Configuration +public class SaTokenConfigure { + + /** + * 注册 [sa-token全局过滤器] + */ + @Bean + public SaReactorFilter getSaReactorFilter() { + return new SaReactorFilter() + // 指定 [拦截路由] + .addInclude("/**") + // 指定 [放行路由] + .addExclude("/favicon.ico") + // 指定[认证函数]: 每次请求执行 + .setAuth(r -> { + System.out.println("---------- sa全局认证"); + // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); + }) + // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + return SaResult.error(e.getMessage()); + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..b6cc79f71 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/DefineRoutes.java b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/DefineRoutes.java new file mode 100644 index 000000000..35ab106dd --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/DefineRoutes.java @@ -0,0 +1,33 @@ +package com.pj.test; + +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +@Configuration +public class DefineRoutes { + + /** + * 函数式编程,初始化路由表 + * @return 路由表 + */ + @SuppressWarnings("deprecation") + @Bean + public RouterFunction getRoutes() { + return RouterFunctions.route(RequestPredicates.GET("/fun"), req -> { + return SaReactorSyncHolder.setContext(req.exchange(), () -> { + System.out.println("是否登录:" + StpUtil.isLogin()); + SaResult res = SaResult.data(StpUtil.getTokenInfo()); + return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).syncBody(res); + }); + }); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/GlobalException.java new file mode 100644 index 000000000..34743ae87 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/GlobalException.java @@ -0,0 +1,19 @@ +package com.pj.test; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..21d91ab6b --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,126 @@ +package com.pj.test; + +import cn.dev33.satoken.reactor.context.SaReactorHolder; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +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.CookieValue; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + UserService userService; + + // 登录测试:Controller 里调用 Sa-Token API --- http://localhost:8081/test/login + @RequestMapping("login") + public Mono login(@RequestParam(defaultValue="10001") String id) { + return SaReactorHolder.sync(() -> { + StpUtil.login(id); + return SaResult.ok("登录成功"); + }); + } + + // API测试:手动设置上下文、try-finally 形式 --- http://localhost:8081/test/isLogin + @RequestMapping("isLogin") + public SaResult isLogin(ServerWebExchange exchange) { + try { + SaReactorSyncHolder.setContext(exchange); + System.out.println("是否登录:" + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + } finally { + SaReactorSyncHolder.clearContext(); + } + } + + // API测试:手动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin2 + @RequestMapping("isLogin2") + public SaResult isLogin2(ServerWebExchange exchange) { + SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{ + System.out.println("是否登录:" + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + }); + return SaResult.data(res); + } + + // API测试:自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3 + @RequestMapping("isLogin3") + public Mono isLogin3() { + return SaReactorHolder.sync(() -> { + System.out.println("是否登录:" + StpUtil.isLogin()); + userService.isLogin(); + return SaResult.data(StpUtil.getTokenInfo()); + }); + } + + // API测试:自动设置上下文、调用 userService Mono 方法 --- http://localhost:8081/test/isLogin4 + @RequestMapping("isLogin4") + public Mono isLogin4() { + return userService.findUserIdByNamePwd("ZhangSan", "123456").flatMap(userId -> { + return SaReactorHolder.sync(() -> { + StpUtil.login(userId); + return SaResult.data(StpUtil.getTokenInfo()); + }); + }); + } + + // API测试:切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5 + @RequestMapping("isLogin5") + public Mono isLogin5() { + System.out.println("线程id-----" + Thread.currentThread().getId()); + // 要点:在流里调用 Sa-Token API 之前,必须用 SaReactorHolder.sync( () -> {} ) 进行包裹 + return Mono.delay(Duration.ofSeconds(1)) + .doOnNext(r-> System.out.println("线程id-----" + Thread.currentThread().getId())) + .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) + .map(r-> userService.findUserIdByNamePwd("ZhangSan", "123456")) + .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) + .flatMap(isLogin -> { + System.out.println("是否登录 " + isLogin); + return SaReactorHolder.sync(() -> { + System.out.println("是否登录 " + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + }); + }); + } + + // API测试:使用上下文无关的API --- http://localhost:8081/test/isLogin6 + @RequestMapping("isLogin6") + public SaResult isLogin6(@CookieValue("satoken") String satoken) { + System.out.println("token 为:" + satoken); + System.out.println("登录人:" + StpUtil.getLoginIdByToken(satoken)); + return SaResult.ok("登录人:" + StpUtil.getLoginIdByToken(satoken)); + } + + // API测试:SaSession 写值 --- http://localhost:8081/test/sessionSet + @RequestMapping("sessionSet") + public Mono sessionSet() { + return SaReactorHolder.sync(() -> { + System.out.println("session name 值为:" + StpUtil.getSession().get("name")); + StpUtil.getSession().set("name", "zhangsan"); + System.out.println("session name 值为:" + StpUtil.getSession().get("name")); + return SaResult.data(StpUtil.getSession().get("name")); + }); + } + + // 测试 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("线程id------- " + Thread.currentThread().getId()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/UserService.java b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/UserService.java new file mode 100644 index 000000000..90babbd52 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/UserService.java @@ -0,0 +1,25 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +/** + * 模拟 Service 方法 + * @author click33 + * @since 2025/4/6 + */ +@Service +public class UserService { + + public boolean isLogin() { + System.out.println("UserService 里调用 API 测试,是否登录:" + StpUtil.isLogin()); + return StpUtil.isLogin(); + } + + public Mono findUserIdByNamePwd(String name, String pwd) { + // ... + return Mono.just(10001L); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/resources/application.yml new file mode 100644 index 000000000..ea897bb94 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/resources/application.yml @@ -0,0 +1,46 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + data: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10000ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/pom.xml b/sa-token-demo/sa-token-demo-webflux-springboot4/pom.xml new file mode 100644 index 000000000..f803b6dc1 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/pom.xml @@ -0,0 +1,61 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-webflux-springboot4 + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + cn.dev33 + sa-token-reactor-spring-boot4-starter + ${sa-token.version} + + + + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/SaTokenWebfluxSpringboot4Application.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/SaTokenWebfluxSpringboot4Application.java new file mode 100644 index 000000000..649d1b0d6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/SaTokenWebfluxSpringboot4Application.java @@ -0,0 +1,22 @@ +package com.pj; + +import cn.dev33.satoken.SaManager; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sa-Token整合webflux 示例 (springboot4) + * + * @author click33 + * @since 2026年2月27日 + * + */ +@SpringBootApplication +public class SaTokenWebfluxSpringboot4Application { + + public static void main(String[] args) { + SpringApplication.run(SaTokenWebfluxSpringboot4Application.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/MyFilter.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/MyFilter.java new file mode 100644 index 000000000..5c0a4adf1 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/MyFilter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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 com.pj.satoken; + +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * 自定义过滤器 + */ +@Component +public class MyFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + System.out.println("进入自定义过滤器"); + + try { + // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 + SaReactorSyncHolder.setContext(exchange); + System.out.println(StpUtil.isLogin()); + } + finally { + SaReactorSyncHolder.clearContext(); + } + + return chain.filter(exchange); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/SaTokenConfigure.java new file mode 100644 index 000000000..8524f11f3 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/SaTokenConfigure.java @@ -0,0 +1,39 @@ +package com.pj.satoken; + +import cn.dev33.satoken.reactor.filter.SaReactorFilter; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + * + */ +@Configuration +public class SaTokenConfigure { + + /** + * 注册 [sa-token全局过滤器] + */ + @Bean + public SaReactorFilter getSaReactorFilter() { + return new SaReactorFilter() + // 指定 [拦截路由] + .addInclude("/**") + // 指定 [放行路由] + .addExclude("/favicon.ico") + // 指定[认证函数]: 每次请求执行 + .setAuth(r -> { + System.out.println("---------- sa全局认证"); + // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); + }) + // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + return SaResult.error(e.getMessage()); + }) + ; + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..b6cc79f71 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/StpInterfaceImpl.java @@ -0,0 +1,44 @@ +package com.pj.satoken; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; + +/** + * 自定义权限验证接口扩展 + */ +@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user-add"); + list.add("user-delete"); + list.add("user-update"); + list.add("user-get"); + list.add("article-get"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/DefineRoutes.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/DefineRoutes.java new file mode 100644 index 000000000..4e406f31f --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/DefineRoutes.java @@ -0,0 +1,32 @@ +package com.pj.test; + +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +@Configuration +public class DefineRoutes { + + /** + * 函数式编程,初始化路由表 + * @return 路由表 + */ + @Bean + public RouterFunction getRoutes() { + return RouterFunctions.route(RequestPredicates.GET("/fun"), req -> { + return SaReactorSyncHolder.setContext(req.exchange(), () -> { + System.out.println("是否登录:" + StpUtil.isLogin()); + SaResult res = SaResult.data(StpUtil.getTokenInfo()); + return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(res); + }); + }); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/GlobalException.java new file mode 100644 index 000000000..34743ae87 --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/GlobalException.java @@ -0,0 +1,19 @@ +package com.pj.test; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@RestControllerAdvice +public class GlobalException { + + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/TestController.java new file mode 100644 index 000000000..21d91ab6b --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/TestController.java @@ -0,0 +1,126 @@ +package com.pj.test; + +import cn.dev33.satoken.reactor.context.SaReactorHolder; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +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.CookieValue; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * 测试专用Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + UserService userService; + + // 登录测试:Controller 里调用 Sa-Token API --- http://localhost:8081/test/login + @RequestMapping("login") + public Mono login(@RequestParam(defaultValue="10001") String id) { + return SaReactorHolder.sync(() -> { + StpUtil.login(id); + return SaResult.ok("登录成功"); + }); + } + + // API测试:手动设置上下文、try-finally 形式 --- http://localhost:8081/test/isLogin + @RequestMapping("isLogin") + public SaResult isLogin(ServerWebExchange exchange) { + try { + SaReactorSyncHolder.setContext(exchange); + System.out.println("是否登录:" + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + } finally { + SaReactorSyncHolder.clearContext(); + } + } + + // API测试:手动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin2 + @RequestMapping("isLogin2") + public SaResult isLogin2(ServerWebExchange exchange) { + SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{ + System.out.println("是否登录:" + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + }); + return SaResult.data(res); + } + + // API测试:自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3 + @RequestMapping("isLogin3") + public Mono isLogin3() { + return SaReactorHolder.sync(() -> { + System.out.println("是否登录:" + StpUtil.isLogin()); + userService.isLogin(); + return SaResult.data(StpUtil.getTokenInfo()); + }); + } + + // API测试:自动设置上下文、调用 userService Mono 方法 --- http://localhost:8081/test/isLogin4 + @RequestMapping("isLogin4") + public Mono isLogin4() { + return userService.findUserIdByNamePwd("ZhangSan", "123456").flatMap(userId -> { + return SaReactorHolder.sync(() -> { + StpUtil.login(userId); + return SaResult.data(StpUtil.getTokenInfo()); + }); + }); + } + + // API测试:切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5 + @RequestMapping("isLogin5") + public Mono isLogin5() { + System.out.println("线程id-----" + Thread.currentThread().getId()); + // 要点:在流里调用 Sa-Token API 之前,必须用 SaReactorHolder.sync( () -> {} ) 进行包裹 + return Mono.delay(Duration.ofSeconds(1)) + .doOnNext(r-> System.out.println("线程id-----" + Thread.currentThread().getId())) + .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) + .map(r-> userService.findUserIdByNamePwd("ZhangSan", "123456")) + .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) + .flatMap(isLogin -> { + System.out.println("是否登录 " + isLogin); + return SaReactorHolder.sync(() -> { + System.out.println("是否登录 " + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + }); + }); + } + + // API测试:使用上下文无关的API --- http://localhost:8081/test/isLogin6 + @RequestMapping("isLogin6") + public SaResult isLogin6(@CookieValue("satoken") String satoken) { + System.out.println("token 为:" + satoken); + System.out.println("登录人:" + StpUtil.getLoginIdByToken(satoken)); + return SaResult.ok("登录人:" + StpUtil.getLoginIdByToken(satoken)); + } + + // API测试:SaSession 写值 --- http://localhost:8081/test/sessionSet + @RequestMapping("sessionSet") + public Mono sessionSet() { + return SaReactorHolder.sync(() -> { + System.out.println("session name 值为:" + StpUtil.getSession().get("name")); + StpUtil.getSession().set("name", "zhangsan"); + System.out.println("session name 值为:" + StpUtil.getSession().get("name")); + return SaResult.data(StpUtil.getSession().get("name")); + }); + } + + // 测试 浏览器访问: http://localhost:8081/test/test + @RequestMapping("test") + public SaResult test() { + System.out.println("线程id------- " + Thread.currentThread().getId()); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/UserService.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/UserService.java new file mode 100644 index 000000000..004ef0eff --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/UserService.java @@ -0,0 +1,25 @@ +package com.pj.test; + +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +/** + * 模拟 Service 方法 + * @author click33 + * @since 2025/4/6 + */ +@Service +public class UserService { + + public boolean isLogin() { + System.out.println("UserService 里调用 API 测试,是否登录:" + StpUtil.isLogin()); + return StpUtil.isLogin(); + } + + public Mono findUserIdByNamePwd(String name, String pwd) { + // ... + return Mono.just(10001L); + } + +} diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/resources/application.yml new file mode 100644 index 000000000..b257c331d --- /dev/null +++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/resources/application.yml @@ -0,0 +1,47 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + data: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10000ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + diff --git a/sa-token-demo/sa-token-demo-webflux/.gitignore b/sa-token-demo/sa-token-demo-webflux/.gitignore deleted file mode 100644 index 99a6e7679..000000000 --- a/sa-token-demo/sa-token-demo-webflux/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.idea/ - -.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-webflux/pom.xml b/sa-token-demo/sa-token-demo-webflux/pom.xml index 0a59be1ff..2fef46285 100644 --- a/sa-token-demo/sa-token-demo-webflux/pom.xml +++ b/sa-token-demo/sa-token-demo-webflux/pom.xml @@ -10,13 +10,13 @@ org.springframework.boot spring-boot-starter-parent - 2.0.0.RELEASE + 2.7.18 - + - 1.24.0 + 1.45.0 @@ -26,32 +26,21 @@ org.springframework.boot spring-boot-starter-webflux - - org.springframework.boot - spring-boot-starter-aop - - - + + cn.dev33 sa-token-reactor-spring-boot-starter - ${sa-token-version} + ${sa-token.version} - - - - - + + - + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/SaTokenWebSocketSpringApplication.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/SaTokenWebSocketSpringApplication.java new file mode 100644 index 000000000..35ea8291a --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/SaTokenWebSocketSpringApplication.java @@ -0,0 +1,32 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +/** + * Sa-Token 整合 WebSocket 鉴权示例 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenWebSocketSpringApplication { + + /* + * 1、访问登录接口,拿到会话Token: + * http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + * + * 2、找一个WebSocket在线测试页面进行连接, + * 例如: + * https://www.bejson.com/httputil/websocket/ + * 然后连接地址: + * ws://localhost:8081/ws-connect?satoken=2e6db38f-1e78-40bc-aa8f-e8f1f77fbef5 + */ + + public static void main(String[] args) { + SpringApplication.run(SaTokenWebSocketSpringApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..78f2ddc86 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/MyWebSocketHandler.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/MyWebSocketHandler.java new file mode 100644 index 000000000..2318d6b6c --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/MyWebSocketHandler.java @@ -0,0 +1,83 @@ +package com.pj.ws; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +/** + * 处理 WebSocket 连接 + * + * @author click33 + * @since 2022-2-11 + */ +public class MyWebSocketHandler extends TextWebSocketHandler { + + /** + * 固定前缀 + */ + private static final String USER_ID = "user_id_"; + + /** + * 存放Session集合,方便推送消息 + */ + private static ConcurrentHashMap webSocketSessionMaps = new ConcurrentHashMap<>(); + + // 监听:连接开启 + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + + // put到集合,方便后续操作 + String userId = session.getAttributes().get("userId").toString(); + webSocketSessionMaps.put(USER_ID + userId, session); + + + // 给个提示 + String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId; + System.out.println(tips); + sendMessage(session, tips); + } + + // 监听:连接关闭 + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + // 从集合移除 + String userId = session.getAttributes().get("userId").toString(); + webSocketSessionMaps.remove(USER_ID + userId); + + // 给个提示 + String tips = "Web-Socket 连接关闭,sid=" + session.getId() + ",userId=" + userId; + System.out.println(tips); + } + + // 收到消息 + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { + System.out.println("sid为:" + session.getId() + ",发来:" + message); + } + + // ----------- + + // 向指定客户端推送消息 + public static void sendMessage(WebSocketSession session, String message) { + try { + System.out.println("向sid为:" + session.getId() + ",发送:" + message); + session.sendMessage(new TextMessage(message)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // 向指定用户推送消息 + public static void sendMessage(long userId, String message) { + WebSocketSession session = webSocketSessionMaps.get(USER_ID + userId); + if(session != null) { + sendMessage(session, message); + } + } + +} + diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketConfig.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketConfig.java new file mode 100644 index 000000000..2ece6935a --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketConfig.java @@ -0,0 +1,30 @@ +package com.pj.ws; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +/** + * WebSocket 相关配置 + * + * @author click33 + * @since 2022-2-11 + */ +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + // 注册 WebSocket 处理器 + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { + webSocketHandlerRegistry + // WebSocket 连接处理器 + .addHandler(new MyWebSocketHandler(), "/ws-connect") + // WebSocket 拦截器 + .addInterceptors(new WebSocketInterceptor()) + // 允许跨域 + .setAllowedOrigins("*"); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketInterceptor.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketInterceptor.java new file mode 100644 index 000000000..41d19b972 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketInterceptor.java @@ -0,0 +1,45 @@ +package com.pj.ws; + +import java.util.Map; + +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import cn.dev33.satoken.stp.StpUtil; + +/** + * WebSocket 握手的前置拦截器 + * + * @author click33 + * @since 2022-2-11 + */ +public class WebSocketInterceptor implements HandshakeInterceptor { + + // 握手之前触发 (return true 才会握手成功 ) + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, + Map attr) { + + System.out.println("---- 握手之前触发 " + StpUtil.getTokenValue()); + + // 未登录情况下拒绝握手 + if(StpUtil.isLogin() == false) { + System.out.println("---- 未授权客户端,连接失败"); + return false; + } + + // 标记 userId,握手成功 + attr.put("userId", StpUtil.getLoginIdAsLong()); + return true; + } + + // 握手之后触发 + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Exception exception) { + System.out.println("---- 握手之后触发 "); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-websocket-spring/src/main/resources/application.yml new file mode 100644 index 000000000..dfa0066e2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket/pom.xml b/sa-token-demo/sa-token-demo-websocket/pom.xml new file mode 100644 index 000000000..822b772ff --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/pom.xml @@ -0,0 +1,69 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-websocket + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.5.14 + + + + + + + 1.45.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/SaTokenWebSocketApplication.java b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/SaTokenWebSocketApplication.java new file mode 100644 index 000000000..979222897 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/SaTokenWebSocketApplication.java @@ -0,0 +1,32 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +/** + * Sa-Token 整合 WebSocket 鉴权示例 + * @author click33 + * + */ +@SpringBootApplication +public class SaTokenWebSocketApplication { + + /* + * 1、访问登录接口,拿到会话Token: + * http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + * + * 2、找一个WebSocket在线测试页面进行连接, + * 例如: + * https://www.bejson.com/httputil/websocket/ + * 然后连接地址: + * ws://localhost:8081/ws-connect/2e6db38f-1e78-40bc-aa8f-e8f1f77fbef5 + */ + + public static void main(String[] args) { + SpringApplication.run(SaTokenWebSocketApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 000000000..78f2ddc86 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConfig.java b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConfig.java new file mode 100644 index 000000000..356393ef8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConfig.java @@ -0,0 +1,18 @@ +package com.pj.ws; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +/** + * 开启WebSocket支持 + */ +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java new file mode 100644 index 000000000..c99e701f5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java @@ -0,0 +1,102 @@ +package com.pj.ws; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * WebSocket 连接测试 + */ +@Component +@ServerEndpoint("/ws-connect/{satoken}") +public class WebSocketConnect { + + /** + * 固定前缀 + */ + private static final String USER_ID = "user_id_"; + + /** + * 存放Session集合,方便推送消息 (javax.websocket.Session) + */ + private static ConcurrentHashMap sessionMap = new ConcurrentHashMap<>(); + + // 监听:连接成功 + @OnOpen + public void onOpen(Session session, @PathParam("satoken") String satoken) throws IOException { + + // 根据 token 获取对应的 userId + Object loginId = StpUtil.getLoginIdByToken(satoken); + if(loginId == null) { + session.close(); + throw new SaTokenException("连接失败,无效Token:" + satoken); + } + + // put到集合,方便后续操作 + long userId = SaFoxUtil.getValueByType(loginId, long.class); + sessionMap.put(USER_ID + userId, session); + + // 给个提示 + String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId; + System.out.println(tips); + sendMessage(session, tips); + } + + // 监听: 连接关闭 + @OnClose + public void onClose(Session session) { + System.out.println("连接关闭,sid=" + session.getId()); + for (String key : sessionMap.keySet()) { + if(sessionMap.get(key).getId().equals(session.getId())) { + sessionMap.remove(key); + } + } + } + + // 监听:收到客户端发送的消息 + @OnMessage + public void onMessage(Session session, String message) { + System.out.println("sid为:" + session.getId() + ",发来:" + message); + } + + // 监听:发生异常 + @OnError + public void onError(Session session, Throwable error) { + System.out.println("sid为:" + session.getId() + ",发生错误"); + error.printStackTrace(); + } + + // --------- + + // 向指定客户端推送消息 + public static void sendMessage(Session session, String message) { + try { + System.out.println("向sid为:" + session.getId() + ",发送:" + message); + session.getBasicRemote().sendText(message); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // 向指定用户推送消息 + public static void sendMessage(long userId, String message) { + Session session = sessionMap.get(USER_ID + userId); + if(session != null) { + sendMessage(session, message); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-websocket/src/main/resources/application.yml new file mode 100644 index 000000000..dfa0066e2 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # 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 + + + + + \ No newline at end of file diff --git a/sa-token-dependencies/pom.xml b/sa-token-dependencies/pom.xml new file mode 100644 index 000000000..42d4148f4 --- /dev/null +++ b/sa-token-dependencies/pom.xml @@ -0,0 +1,273 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-bom + ${revision} + + ../sa-token-bom/pom.xml + + pom + + sa-token-dependencies + sa-token-dependencies + Sa-Token Dependencies + + + + 2.13.4.1 + 2.11.2 + 3.1.0 + 3.1.0 + 6.0.0 + 3.0.9.RELEASE + 2.3.34 + 3.2.1 + 1.8.5 + 3.2.139 + 4.0.50 + 4.9.17 + 3.14.4 + 1.1.5-java8 + 2.5.0 + 2.7.21 + 2.10.1.RELEASE + 5.8.36 + 0.12.6 + 1.2.83 + 2.0.15 + 3.45.0 + 5.8.36 + 3.2.0 + 1.6.4 + 4.1.0 + + + 3.2.7 + + 0.7.0 + + + + + + + + + javax.servlet + javax.servlet-api + ${servlet-api.version} + + + + + jakarta.servlet + jakarta.servlet-api + ${jakarta-servlet-api.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind.version} + + + + + tools.jackson.core + jackson-databind + ${jackson3-databind.version} + + + + + org.noear + solon + ${solon.version} + + + + + org.noear + snack3 + ${noear-snack3.version} + + + + + org.noear + snack4 + ${noear-snack4.version} + + + + + io.jboot + jboot + ${jboot.version} + + + + + com.jfinal + jfinal + ${jfinal.version} + + + + + com.kfyty + loveqq-core + ${loveqq.version} + + + + + com.kfyty + loveqq-mvc-core + ${loveqq.version} + + + + + com.kfyty + loveqq-boot-starter-redisson + ${loveqq.version} + + + + + + + org.redisson + redisson + ${redisson.version} + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-datatype-jsr310.version} + + + + + com.alibaba + fastjson + ${fastjson.version} + + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + + + org.noear + redisx + ${noear-redisx.version} + + + + + org.noear + solon-test + ${solon.version} + + + + + org.apache.commons + commons-pool2 + ${commons-pool2.version} + + + + + org.thymeleaf + thymeleaf + ${thymeleaf.version} + + + + + org.freemarker + freemarker + ${freemarker.version} + + + + + + cn.hutool + hutool-jwt + ${hutool-jwt.version} + + + + + org.apache.dubbo + dubbo + ${dubbo.version} + + + + + net.devh + grpc-spring-boot-starter + ${grpc-spring-boot-starter.version} + + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + + + cn.hutool + hutool-cache + ${hutool-cache.version} + + + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + + + com.dtflys.forest + forest-core + ${forest.version} + + + + + cn.zhxu + okhttps + ${okhttps.version} + + + + + + \ No newline at end of file diff --git a/sa-token-doc/README.md b/sa-token-doc/README.md new file mode 100644 index 000000000..63a35adf9 --- /dev/null +++ b/sa-token-doc/README.md @@ -0,0 +1,218 @@ +

+ logo +

+

Sa-Token v1.45.0

+
✨ 开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅!
+

+ + + + + + + + +

+ +--- + +## 📝 前言:️️ +为了保证新同学不迷路,请允许我唠叨一下:无论您从何处看到本篇文章,最新开发文档永远在:[https://sa-token.com](https://sa-token.com), +建议收藏在浏览器书签 (快捷键 **ctrl + D**),如果您已经身处本网站下,则请忽略此条说明。 + +回望 2020 年初,我为 Sa-Token 提交第一行代码之际,彼时市面上 Java 缺少的不仅是一个简洁好用的鉴权框架,更是一整套清晰、自洽的权限架构设计思想。 + +因此,这几年间我将大量时间倾注在 Sa-Token 的文档编写,几乎每一章节、每一句话、每一个字都经过反复修改、精细打磨,以求做到最清晰、干练、易懂的表述。用心阅读文档,你学习到的将不止是 Sa-Token 框架本身,更是绝大多数场景下权限设计的最佳实践。 + + +## 🛠️ Sa-Token 介绍 + +**Sa-Token** 是一个轻量级 Java 权限认证框架,主要解决:**登录认证**、**权限认证**、**单点登录**、**OAuth2.0**、**分布式Session会话**、**微服务网关鉴权** +等一系列权限相关问题。 + + + + + +Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分,以登录认证为例,你只需要: + +``` java +// 会话登录,参数填登录人的账号id +StpUtil.login(10001); +``` + +无需实现任何接口,无需创建任何配置文件,只需要这一句静态代码的调用,便可以完成会话登录认证。 + +如果一个接口需要登录后才能访问,我们只需调用以下代码: + +``` java +// 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常 +StpUtil.checkLogin(); +``` + +在 Sa-Token 中,大多数功能都可以一行代码解决: + +踢人下线: + +``` java +// 将账号id为 10077 的会话踢下线 +StpUtil.kickout(10077); +``` + +权限认证: + +``` java +// 注解鉴权:只有具备 `user:add` 权限的会话才可以进入方法 +@SaCheckPermission("user:add") +public String insert(SysUser user) { + // ... + return "用户增加"; +} +``` + +路由拦截鉴权: + +``` java +// 根据路由划分模块,不同模块不同鉴权 +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + // 更多模块... +})).addPathPatterns("/**"); +``` + +当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅! + + +## 🎉 Sa-Token 功能一览 + +Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。 + +- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。 +- **权限认证** —— 权限认证、角色认证、会话二级认证。 +- **踢人下线** —— 根据账号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。 + +功能结构图: + + + + +## 📖❓ 疑问解答 + +**1、Sa-Token 功能全不全?** + +七年磨一剑:五大核心模块(登录、鉴权、SSO、OAuth2、微服务) + 众多实用插件 (短 token、jwt 集成、API 参数签名、API Key 秘钥授权...) 我们提供的不只是权限认证,我们提供的是一站式解决方案。 + + +**2、Sa-Token 好不好学?** + +中文文档 + 中文代码注释 + 中文交流社区 + 大量实战案例博客 + 多个视频教程 + 大量优秀开源项目集成案例。 + + +**3、Sa-Token 用的人多不多?** + +截止统计日 (2026-1-25) 起,Sa-Token 在: + +- Gitee 关注量达到 48627 Star,位列平台所有推荐项目排行榜第一名。 +- GitHub 关注量达到 18523 Star,是主要竞争框架 Spring Security 的 1.97 倍,Apache Shiro 的 4.19 倍。 +- 25+ 微信粉丝群 (500人),8+ QQ粉丝群 (1000人 or 2000人) ,在线文档访问量月PV 20万+。 + +这是众多开发者用脚投票的数据,相信这些数据比任何言语都能证明 Sa-Token 的热度。 + + +**4、Sa-Token 有哪些权威认证?** + +曾获荣誉包括但不限于:Gitee GVP 最有价值开源项目、GitCode G-Star 优质开源项目、OSCHINA 2021 人气指数 TOP 30 开源项目、OSCHINA 2022 年度最火热中国开源项目社区之一、开放原子基金会2023快速成长开源项目、 Dromara 组织顶尖项目(之一)、可信开源社区共同体预备成员、所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。 Gitee High Star 计划项目(5000+star)。Gitee 2025年度开源项目 Web应用开发 Top 2。 + + +**5、Sa-Token 收费吗?** + +Sa-Token 采用 Apache-2.0 开源协议,承诺框架本身与在线文档永久免费开放。当然如果您有心赞助 Sa-Token,我们也不回避:[赞助链接](https://sa-token.com/doc.html#/more/sa-token-donate)。 +我们将定期同步赞助者名单到在线文档展示。(您需要注意的一点是:该赞助仅为友情赞助,不提供任何商业交换) + + +**6、Sa-Token 是封装的 SpringSecurity 吗?是套壳 ApacheShiro 吗?** + +不是。Sa-Token 不是一个后台模板,也不是针对 xx 框架的二次封装套壳,而是从 0 开始的纯血自研框架,核心包零依赖,完全自主可控的架构内核 + 众多主流框架的集成适配。 + + + +## 🌍 其它语言版本 + +Sa-Token 社区成员贡献了多语言实现版本: + +- Rust 版本:[https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust) +- Go 版本:[https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go) +- NodeJS 版本:[https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token) +- PHP 版本:[https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token) + +我们诚邀对上述语言较为熟练的开发者,一起建设相关版本。🤝 + + +## 📈 开源仓库 Star 趋势 + +

+ +[![github-chart](https://starchart.cc/dromara/sa-token.svg 'GitHub')](https://starchart.cc/dromara/sa-token) + +如果 Sa-Token 帮助到了您,希望您可以为其点上一个 `star`: +[码云](https://gitee.com/dromara/sa-token)、 +[AtomGit](https://atomgit.com/dromara/sa-token)、 +[GitHub](https://github.com/dromara/sa-token) + + +## 🚀 使用 Sa-Token 的开源项目 +参考:[Sa-Token 生态](/more/link) + + + +## 📚 示例大全 + +**我们为框架几乎所有技术点均单独制作了对应的集成示例,此压缩包共计 60+ Demo**:涵盖 Sa-Token 登录认证、权限认证、SSO 单点登录、OAUth2 统一认证、微服务鉴权、API Key 认证、JWT集成、跨系统调用参数签名校验 等鉴权认证的方方面面。 + +下载地址:[Sa-Token 集成示例大全下载](/more/download-demos) + + + + + +## 💬 交流群 +加入 Sa-Token 框架 QQ、微信讨论群:[点击加入](/more/join-group.md) + diff --git a/sa-token-doc/_sidebar.md b/sa-token-doc/_sidebar.md new file mode 100644 index 000000000..c36fb95cb --- /dev/null +++ b/sa-token-doc/_sidebar.md @@ -0,0 +1,169 @@ + + +- **开始** + - [框架介绍](/) + - [在 SpringBoot 环境集成](/start/example) + - [在 WebFlux 环境集成](/start/webflux-example) + - [在 Solon 环境集成](/start/solon-example) + - [其它环境集成示例](/start/download) + - [源码运行指南](/start/run-source-code) + - [Sa-Token 集成示例大全下载](/more/download-demos) + +- **基础** + - [登录认证](/use/login-auth) + - [权限认证](/use/jur-auth) + - [踢人下线](/use/kick) + - [注解鉴权](/use/at-check) + - [路由拦截鉴权](/use/route-check) + - [Session会话](/use/session) + - [框架配置](/use/config) + +- **深入** + - [集成 Redis](/up/integ-redis) + - [前后端分离](/up/not-cookie) + - [自定义 Token 风格](/up/token-style) + - [Token 提交前缀](/up/token-prefix) + - [同端互斥登录](/up/mutex-login) + - [记住我模式](/up/remember-me) + - [登录参数 & 注销参数](/up/login-parameter) + - [二级认证](/up/safe-auth) + - [模拟他人 & 身份切换](/up/mock-person) + - [账号封禁](/up/disable) + - [密码加密](/up/password-secure) + - [会话查询](/up/search-session) + - [Http Basic/Digest 认证](/up/basic-auth) + - [全局侦听器](/up/global-listener) + - [全局过滤器](/up/global-filter) + - [多账号认证](/up/many-account) + +- **单点登录** + - [单点登录简述](/sso/readme) + - [搭建统一认证中心:SSO-Server](/sso/sso-server) + - [SSO-Server 认证中心开放 API 接口](/sso/sso-apidoc) + - [SSO模式一 共享Cookie同步会话](/sso/sso-type1) + - [SSO模式二 URL重定向传播会话](/sso/sso-type2) + - [SSO模式三 Http请求获取会话](/sso/sso-type3) + - [配置域名校验](/sso/sso-check-domain) + - [定制化登录页面](/sso/sso-custom-login) + - [自定义API路由](/sso/sso-custom-api) + - [平台中心跳转模式](/sso/sso-home-jump) + - [匿名 client 接入](/sso/anon-client) + - [单点注销](/sso/signout) + - [前后端分离下的整合方案](/sso/sso-h5) + - [消息推送机制](/sso/message-push) + + - [用户数据同步 / 迁移](/sso/user-data-sync) + - [NoSdk、ReSdk 模式与非 java 项目](/sso/sso-nosdk) + - [SSO 代码 API 参考](/sso/sso-dev) + - [常见问题总结](/sso/sso-questions) + - [Sa-Pro:单点登录商业版](https://sa-max.cn?way=st_sso) + +- **OAuth2.0** + - [OAuth2.0简述](/oauth2/readme) + - [OAuth2-Server搭建](/oauth2/oauth2-server) + - [OAuth2-Server端开放 API 接口](/oauth2/oauth2-apidoc) + - [自定义数据加载器](/oauth2/oauth2-data-loader) + - [配置 client 域名校验 ](/oauth2/oauth2-check-domain) + - [自定义 Scope 权限及处理器](/oauth2/oauth2-custom-scope) + - [为 Scope 划分等级](/oauth2/oauth2-scope-level) + - [自定义 grant_type](/oauth2/oauth2-custom-grant_type) + - [定制化登录页面与授权页面](/oauth2/oauth2-custom-login) + - [自定义 API 路由 ](/oauth2/oauth2-custom-api) + - [OAuth2-Server端前后台分离](/oauth2/oauth2-h5) + - [OpenId 与 UnionId](/oauth2/oauth2-openid) + - [开启 OIDC 协议](/oauth2/oauth2-oidc) + - [使用注解校验 Access-Token](/oauth2/oauth2-at-check) + - [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking) + - [OAuth2 代码 API 参考](/oauth2/oauth2-dev) + - [常见问题总结](/oauth2/oauth2-questions) + - [Sa-Max:统一认证商业版](https://sa-max.cn?way=st_oauth2) + + + + +- **微服务** + - [分布式Session会话](/micro/dcs-session) + - [网关统一鉴权](/micro/gateway-auth) + - [内部服务外网隔离](/micro/same-token) + - [依赖引入说明](/micro/import-intro) + +- **插件** + - [AOP注解鉴权](/plugin/aop-at) + - [临时 Token 认证](/plugin/temp-token) + - [Quick-Login快速登录插件](/plugin/quick-login) + - [Alone独立Redis插件](/plugin/alone-redis) + - [Alone独立Redisson插件](/plugin/alone-redisson) + - [缓存层扩展](/plugin/dao-extend) + - [JSON 序列化扩展](/plugin/json-extend) + - [序列化插件扩展包](/plugin/custom-serializer) + - [和 Thymeleaf 集成](/plugin/thymeleaf-extend) + - [和 Freemarker 集成](/plugin/freemarker-extend) + - [注解鉴权 SpEL 表达式](/plugin/spel-at) + - [和 jwt 集成](/plugin/jwt-extend) + - [和 Dubbo 集成](/plugin/dubbo-extend) + - [和 gRPC 集成](/plugin/grpc-extend) + - [API 接口参数签名](/plugin/api-sign) + - [API Key 接口调用秘钥](/plugin/api-key) + - [Sa-Token 插件开发指南](/fun/plugin-dev) + - [自定义 SaTokenContext 指南](/fun/sa-token-context) + + +- **API手册** + - [StpUtil-鉴权工具类](/api/stp-util) + - [SaSession-会话对象](/api/sa-session) + - [SaTokenDao-数据持久接口](/api/sa-token-dao) + - [SaStrategy-全局策略](/api/sa-strategy) + - [全局类、方法](/more/common-action) + + +- **框架设计** + - [仓库目录](/arch/dir-intro) + - [数据结构](/arch/data-structure) + + +- **其它** + - [更新日志](/more/update-log) + - [框架生态](/more/link) + - [框架博客](/more/blog) + - [推荐公众号](/more/tj-gzh) + - [加入讨论群](/more/join-group) + + - [Sa-Token 内容合作群](/more/content-cooperation) + - [赞助 Sa-Token](/more/sa-token-donate) + - [需求提交](/more/demand-commit) + - [问卷调查](/more/wenjuan) + +- **附录** + - [常见问题排查](/more/common-questions) + - [框架名词解释](/more/noun-intro) + - [Sa-Token功能结构图](/fun/auth-flow) + - [全局 Log 输出](/fun/log) + - [异步 & Mock 上下文](/fun/async--mock) + - [未登录场景值详解](/fun/not-login-scene) + - [Token有效期详解](/fun/token-timeout) + - [Session模型详解](/fun/session-model) + - [数据读写三大作用域](/fun/three-scope) + - [TokenInfo参数详解](/fun/token-info) + - [异常细分状态码](/fun/exception-code) + - [自定义注解](/fun/custom-annotations) + - [防火墙](/fun/firewall) + - [参考:把权限放在缓存里](/fun/jur-cache) + - [参考:把路由拦截鉴权动态化](/fun/dynamic-router-check) + - [解决反向代理 uri 丢失的问题](/fun/curr-domain) + - [解决跨域问题](/fun/cors-filter) + - [技术选型:SSO 与 OAuth2 对比](/fun/sso-vs-oauth2) + - [集成 MongoDB 参考一](/up/integ-spring-mongod-1) + - [集成 MongoDB 参考二](/up/integ-spring-mongod-2) + + - [从 Shiro、SpringSecurity、JWT 迁移](/fun/auth-framework-function-test) + - [issue 提问模板](/fun/issue-template) + - [为Sa-Token贡献代码](/fun/git-pr) + - [Sa-Token开源大事记](/fun/timeline) + + - [团队成员](/fun/team) + - [Sa-Token框架掌握度--在线考试](/fun/sa-token-test) + + + +






+

----- 到底线了 -----

\ No newline at end of file diff --git a/sa-token-doc/a/github-stars-vs/echarts.min-5.4.3.js b/sa-token-doc/a/github-stars-vs/echarts.min-5.4.3.js new file mode 100644 index 000000000..044cdafcf --- /dev/null +++ b/sa-token-doc/a/github-stars-vs/echarts.min-5.4.3.js @@ -0,0 +1,45 @@ + +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you 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. +*/ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).echarts={})}(this,(function(t){"use strict"; +/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])},e(t,n)};function n(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function i(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(i.prototype=n.prototype,new i)}var i=function(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1},r=new function(){this.browser=new i,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow="undefined"!=typeof window};"object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?(r.wxa=!0,r.touchEventsSupported=!0):"undefined"==typeof document&&"undefined"!=typeof self?r.worker=!0:"undefined"==typeof navigator?(r.node=!0,r.svgSupported=!0):function(t,e){var n=e.browser,i=t.match(/Firefox\/([\d.]+)/),r=t.match(/MSIE\s([\d.]+)/)||t.match(/Trident\/.+?rv:(([\d.]+))/),o=t.match(/Edge?\/([\d.]+)/),a=/micromessenger/i.test(t);i&&(n.firefox=!0,n.version=i[1]);r&&(n.ie=!0,n.version=r[1]);o&&(n.edge=!0,n.version=o[1],n.newEdge=+o[1].split(".")[0]>18);a&&(n.weChat=!0);e.svgSupported="undefined"!=typeof SVGRect,e.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,e.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11),e.domSupported="undefined"!=typeof document;var s=document.documentElement.style;e.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),e.transformSupported=e.transform3dSupported||n.ie&&+n.version>=9}(navigator.userAgent,r);var o="sans-serif",a="12px "+o;var s,l,u=function(t){var e={};if("undefined"==typeof JSON)return e;for(var n=0;n=0)o=r*t.length;else for(var c=0;c>1)%2;a.style.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",i[s]+":0",r[l]+":0",i[1-s]+":auto",r[1-l]+":auto",""].join("!important;"),t.appendChild(a),n.push(a)}return n}(e,a),l=function(t,e,n){for(var i=n?"invTrans":"trans",r=e[i],o=e.srcCoords,a=[],s=[],l=!0,u=0;u<4;u++){var h=t[u].getBoundingClientRect(),c=2*u,p=h.left,d=h.top;a.push(p,d),l=l&&o&&p===o[c]&&d===o[c+1],s.push(t[u].offsetLeft,t[u].offsetTop)}return l&&r?r:(e.srcCoords=a,e[i]=n?$t(s,a):$t(a,s))}(s,a,o);if(l)return l(t,n,i),!0}return!1}function ee(t){return"CANVAS"===t.nodeName.toUpperCase()}var ne=/([&<>"'])/g,ie={"&":"&","<":"<",">":">",'"':""","'":"'"};function re(t){return null==t?"":(t+"").replace(ne,(function(t,e){return ie[e]}))}var oe=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ae=[],se=r.browser.firefox&&+r.browser.version.split(".")[0]<39;function le(t,e,n,i){return n=n||{},i?ue(t,e,n):se&&null!=e.layerX&&e.layerX!==e.offsetX?(n.zrX=e.layerX,n.zrY=e.layerY):null!=e.offsetX?(n.zrX=e.offsetX,n.zrY=e.offsetY):ue(t,e,n),n}function ue(t,e,n){if(r.domSupported&&t.getBoundingClientRect){var i=e.clientX,o=e.clientY;if(ee(t)){var a=t.getBoundingClientRect();return n.zrX=i-a.left,void(n.zrY=o-a.top)}if(te(ae,t,i,o))return n.zrX=ae[0],void(n.zrY=ae[1])}n.zrX=n.zrY=0}function he(t){return t||window.event}function ce(t,e,n){if(null!=(e=he(e)).zrX)return e;var i=e.type;if(i&&i.indexOf("touch")>=0){var r="touchend"!==i?e.targetTouches[0]:e.changedTouches[0];r&&le(t,r,e,n)}else{le(t,e,e,n);var o=function(t){var e=t.wheelDelta;if(e)return e;var n=t.deltaX,i=t.deltaY;if(null==n||null==i)return e;return 3*(0!==i?Math.abs(i):Math.abs(n))*(i>0?-1:i<0?1:n>0?-1:1)}(e);e.zrDelta=o?o/120:-(e.detail||0)/3}var a=e.button;return null==e.which&&void 0!==a&&oe.test(e.type)&&(e.which=1&a?1:2&a?3:4&a?2:0),e}function pe(t,e,n,i){t.addEventListener(e,n,i)}var de=function(t){t.preventDefault(),t.stopPropagation(),t.cancelBubble=!0};function fe(t){return 2===t.which||3===t.which}var ge=function(){function t(){this._track=[]}return t.prototype.recognize=function(t,e,n){return this._doTrack(t,e,n),this._recognize(t)},t.prototype.clear=function(){return this._track.length=0,this},t.prototype._doTrack=function(t,e,n){var i=t.touches;if(i){for(var r={points:[],touches:[],target:e,event:t},o=0,a=i.length;o1&&r&&r.length>1){var a=ye(r)/ye(o);!isFinite(a)&&(a=1),e.pinchScale=a;var s=[((i=r)[0][0]+i[1][0])/2,(i[0][1]+i[1][1])/2];return e.pinchX=s[0],e.pinchY=s[1],{type:"pinch",target:t[0].target,event:e}}}}};function me(){return[1,0,0,1,0,0]}function xe(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t}function _e(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t}function be(t,e,n){var i=e[0]*n[0]+e[2]*n[1],r=e[1]*n[0]+e[3]*n[1],o=e[0]*n[2]+e[2]*n[3],a=e[1]*n[2]+e[3]*n[3],s=e[0]*n[4]+e[2]*n[5]+e[4],l=e[1]*n[4]+e[3]*n[5]+e[5];return t[0]=i,t[1]=r,t[2]=o,t[3]=a,t[4]=s,t[5]=l,t}function we(t,e,n){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4]+n[0],t[5]=e[5]+n[1],t}function Se(t,e,n){var i=e[0],r=e[2],o=e[4],a=e[1],s=e[3],l=e[5],u=Math.sin(n),h=Math.cos(n);return t[0]=i*h+a*u,t[1]=-i*u+a*h,t[2]=r*h+s*u,t[3]=-r*u+h*s,t[4]=h*o+u*l,t[5]=h*l-u*o,t}function Me(t,e,n){var i=n[0],r=n[1];return t[0]=e[0]*i,t[1]=e[1]*r,t[2]=e[2]*i,t[3]=e[3]*r,t[4]=e[4]*i,t[5]=e[5]*r,t}function Ie(t,e){var n=e[0],i=e[2],r=e[4],o=e[1],a=e[3],s=e[5],l=n*a-o*i;return l?(l=1/l,t[0]=a*l,t[1]=-o*l,t[2]=-i*l,t[3]=n*l,t[4]=(i*s-a*r)*l,t[5]=(o*r-n*s)*l,t):null}function Te(t){var e=[1,0,0,1,0,0];return _e(e,t),e}var Ce=Object.freeze({__proto__:null,create:me,identity:xe,copy:_e,mul:be,translate:we,rotate:Se,scale:Me,invert:Ie,clone:Te}),De=function(){function t(t,e){this.x=t||0,this.y=e||0}return t.prototype.copy=function(t){return this.x=t.x,this.y=t.y,this},t.prototype.clone=function(){return new t(this.x,this.y)},t.prototype.set=function(t,e){return this.x=t,this.y=e,this},t.prototype.equal=function(t){return t.x===this.x&&t.y===this.y},t.prototype.add=function(t){return this.x+=t.x,this.y+=t.y,this},t.prototype.scale=function(t){this.x*=t,this.y*=t},t.prototype.scaleAndAdd=function(t,e){this.x+=t.x*e,this.y+=t.y*e},t.prototype.sub=function(t){return this.x-=t.x,this.y-=t.y,this},t.prototype.dot=function(t){return this.x*t.x+this.y*t.y},t.prototype.len=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},t.prototype.lenSquare=function(){return this.x*this.x+this.y*this.y},t.prototype.normalize=function(){var t=this.len();return this.x/=t,this.y/=t,this},t.prototype.distance=function(t){var e=this.x-t.x,n=this.y-t.y;return Math.sqrt(e*e+n*n)},t.prototype.distanceSquare=function(t){var e=this.x-t.x,n=this.y-t.y;return e*e+n*n},t.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},t.prototype.transform=function(t){if(t){var e=this.x,n=this.y;return this.x=t[0]*e+t[2]*n+t[4],this.y=t[1]*e+t[3]*n+t[5],this}},t.prototype.toArray=function(t){return t[0]=this.x,t[1]=this.y,t},t.prototype.fromArray=function(t){this.x=t[0],this.y=t[1]},t.set=function(t,e,n){t.x=e,t.y=n},t.copy=function(t,e){t.x=e.x,t.y=e.y},t.len=function(t){return Math.sqrt(t.x*t.x+t.y*t.y)},t.lenSquare=function(t){return t.x*t.x+t.y*t.y},t.dot=function(t,e){return t.x*e.x+t.y*e.y},t.add=function(t,e,n){t.x=e.x+n.x,t.y=e.y+n.y},t.sub=function(t,e,n){t.x=e.x-n.x,t.y=e.y-n.y},t.scale=function(t,e,n){t.x=e.x*n,t.y=e.y*n},t.scaleAndAdd=function(t,e,n,i){t.x=e.x+n.x*i,t.y=e.y+n.y*i},t.lerp=function(t,e,n,i){var r=1-i;t.x=r*e.x+i*n.x,t.y=r*e.y+i*n.y},t}(),Ae=Math.min,ke=Math.max,Le=new De,Pe=new De,Oe=new De,Re=new De,Ne=new De,Ee=new De,ze=function(){function t(t,e,n,i){n<0&&(t+=n,n=-n),i<0&&(e+=i,i=-i),this.x=t,this.y=e,this.width=n,this.height=i}return t.prototype.union=function(t){var e=Ae(t.x,this.x),n=Ae(t.y,this.y);isFinite(this.x)&&isFinite(this.width)?this.width=ke(t.x+t.width,this.x+this.width)-e:this.width=t.width,isFinite(this.y)&&isFinite(this.height)?this.height=ke(t.y+t.height,this.y+this.height)-n:this.height=t.height,this.x=e,this.y=n},t.prototype.applyTransform=function(e){t.applyTransform(this,this,e)},t.prototype.calculateTransform=function(t){var e=this,n=t.width/e.width,i=t.height/e.height,r=[1,0,0,1,0,0];return we(r,r,[-e.x,-e.y]),Me(r,r,[n,i]),we(r,r,[t.x,t.y]),r},t.prototype.intersect=function(e,n){if(!e)return!1;e instanceof t||(e=t.create(e));var i=this,r=i.x,o=i.x+i.width,a=i.y,s=i.y+i.height,l=e.x,u=e.x+e.width,h=e.y,c=e.y+e.height,p=!(of&&(f=x,gf&&(f=_,v=n.x&&t<=n.x+n.width&&e>=n.y&&e<=n.y+n.height},t.prototype.clone=function(){return new t(this.x,this.y,this.width,this.height)},t.prototype.copy=function(e){t.copy(this,e)},t.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},t.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},t.prototype.isZero=function(){return 0===this.width||0===this.height},t.create=function(e){return new t(e.x,e.y,e.width,e.height)},t.copy=function(t,e){t.x=e.x,t.y=e.y,t.width=e.width,t.height=e.height},t.applyTransform=function(e,n,i){if(i){if(i[1]<1e-5&&i[1]>-1e-5&&i[2]<1e-5&&i[2]>-1e-5){var r=i[0],o=i[3],a=i[4],s=i[5];return e.x=n.x*r+a,e.y=n.y*o+s,e.width=n.width*r,e.height=n.height*o,e.width<0&&(e.x+=e.width,e.width=-e.width),void(e.height<0&&(e.y+=e.height,e.height=-e.height))}Le.x=Oe.x=n.x,Le.y=Re.y=n.y,Pe.x=Re.x=n.x+n.width,Pe.y=Oe.y=n.y+n.height,Le.transform(i),Re.transform(i),Pe.transform(i),Oe.transform(i),e.x=Ae(Le.x,Pe.x,Oe.x,Re.x),e.y=Ae(Le.y,Pe.y,Oe.y,Re.y);var l=ke(Le.x,Pe.x,Oe.x,Re.x),u=ke(Le.y,Pe.y,Oe.y,Re.y);e.width=l-e.x,e.height=u-e.y}else e!==n&&t.copy(e,n)},t}(),Ve="silent";function Be(){de(this.event)}var Fe=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.handler=null,e}return n(e,t),e.prototype.dispose=function(){},e.prototype.setCursor=function(){},e}(jt),Ge=function(t,e){this.x=t,this.y=e},We=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],He=new ze(0,0,0,0),Ye=function(t){function e(e,n,i,r,o){var a=t.call(this)||this;return a._hovered=new Ge(0,0),a.storage=e,a.painter=n,a.painterRoot=r,a._pointerSize=o,i=i||new Fe,a.proxy=null,a.setHandlerProxy(i),a._draggingMgr=new Zt(a),a}return n(e,t),e.prototype.setHandlerProxy=function(t){this.proxy&&this.proxy.dispose(),t&&(E(We,(function(e){t.on&&t.on(e,this[e],this)}),this),t.handler=this),this.proxy=t},e.prototype.mousemove=function(t){var e=t.zrX,n=t.zrY,i=Ze(this,e,n),r=this._hovered,o=r.target;o&&!o.__zr&&(o=(r=this.findHover(r.x,r.y)).target);var a=this._hovered=i?new Ge(e,n):this.findHover(e,n),s=a.target,l=this.proxy;l.setCursor&&l.setCursor(s?s.cursor:"default"),o&&s!==o&&this.dispatchToElement(r,"mouseout",t),this.dispatchToElement(a,"mousemove",t),s&&s!==o&&this.dispatchToElement(a,"mouseover",t)},e.prototype.mouseout=function(t){var e=t.zrEventControl;"only_globalout"!==e&&this.dispatchToElement(this._hovered,"mouseout",t),"no_globalout"!==e&&this.trigger("globalout",{type:"globalout",event:t})},e.prototype.resize=function(){this._hovered=new Ge(0,0)},e.prototype.dispatch=function(t,e){var n=this[t];n&&n.call(this,e)},e.prototype.dispose=function(){this.proxy.dispose(),this.storage=null,this.proxy=null,this.painter=null},e.prototype.setCursorStyle=function(t){var e=this.proxy;e.setCursor&&e.setCursor(t)},e.prototype.dispatchToElement=function(t,e,n){var i=(t=t||{}).target;if(!i||!i.silent){for(var r="on"+e,o=function(t,e,n){return{type:t,event:n,target:e.target,topTarget:e.topTarget,cancelBubble:!1,offsetX:n.zrX,offsetY:n.zrY,gestureEvent:n.gestureEvent,pinchX:n.pinchX,pinchY:n.pinchY,pinchScale:n.pinchScale,wheelDelta:n.zrDelta,zrByTouch:n.zrByTouch,which:n.which,stop:Be}}(e,t,n);i&&(i[r]&&(o.cancelBubble=!!i[r].call(i,o)),i.trigger(e,o),i=i.__hostTarget?i.__hostTarget:i.parent,!o.cancelBubble););o.cancelBubble||(this.trigger(e,o),this.painter&&this.painter.eachOtherLayer&&this.painter.eachOtherLayer((function(t){"function"==typeof t[r]&&t[r].call(t,o),t.trigger&&t.trigger(e,o)})))}},e.prototype.findHover=function(t,e,n){var i=this.storage.getDisplayList(),r=new Ge(t,e);if(Ue(i,r,t,e,n),this._pointerSize&&!r.target){for(var o=[],a=this._pointerSize,s=a/2,l=new ze(t-s,e-s,a,a),u=i.length-1;u>=0;u--){var h=i[u];h===n||h.ignore||h.ignoreCoarsePointer||h.parent&&h.parent.ignoreCoarsePointer||(He.copy(h.getBoundingRect()),h.transform&&He.applyTransform(h.transform),He.intersect(l)&&o.push(h))}if(o.length)for(var c=Math.PI/12,p=2*Math.PI,d=0;d=0;o--){var a=t[o],s=void 0;if(a!==r&&!a.ignore&&(s=Xe(a,n,i))&&(!e.topTarget&&(e.topTarget=a),s!==Ve)){e.target=a;break}}}function Ze(t,e,n){var i=t.painter;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}E(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],(function(t){Ye.prototype[t]=function(e){var n,i,r=e.zrX,o=e.zrY,a=Ze(this,r,o);if("mouseup"===t&&a||(i=(n=this.findHover(r,o)).target),"mousedown"===t)this._downEl=i,this._downPoint=[e.zrX,e.zrY],this._upEl=i;else if("mouseup"===t)this._upEl=i;else if("click"===t){if(this._downEl!==this._upEl||!this._downPoint||Vt(this._downPoint,[e.zrX,e.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,t,e)}}));function je(t,e,n,i){var r=e+1;if(r===n)return 1;if(i(t[r++],t[e])<0){for(;r=0;)r++;return r-e}function qe(t,e,n,i,r){for(i===e&&i++;i>>1])<0?l=o:s=o+1;var u=i-s;switch(u){case 3:t[s+3]=t[s+2];case 2:t[s+2]=t[s+1];case 1:t[s+1]=t[s];break;default:for(;u>0;)t[s+u]=t[s+u-1],u--}t[s]=a}}function Ke(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])>0){for(s=i-r;l0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}else{for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}for(a++;a>>1);o(t,e[n+h])>0?a=h+1:l=h}return l}function $e(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])<0){for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}else{for(s=i-r;l=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}for(a++;a>>1);o(t,e[n+h])<0?l=h:a=h+1}return l}function Je(t,e){var n,i,r=7,o=0;t.length;var a=[];function s(s){var l=n[s],u=i[s],h=n[s+1],c=i[s+1];i[s]=u+c,s===o-3&&(n[s+1]=n[s+2],i[s+1]=i[s+2]),o--;var p=$e(t[h],t,l,u,0,e);l+=p,0!==(u-=p)&&0!==(c=Ke(t[l+u-1],t,h,c,c-1,e))&&(u<=c?function(n,i,o,s){var l=0;for(l=0;l=7||d>=7);if(f)break;g<0&&(g=0),g+=2}if((r=g)<1&&(r=1),1===i){for(l=0;l=0;l--)t[d+l]=t[p+l];return void(t[c]=a[h])}var f=r;for(;;){var g=0,y=0,v=!1;do{if(e(a[h],t[u])<0){if(t[c--]=t[u--],g++,y=0,0==--i){v=!0;break}}else if(t[c--]=a[h--],y++,g=0,1==--s){v=!0;break}}while((g|y)=0;l--)t[d+l]=t[p+l];if(0===i){v=!0;break}}if(t[c--]=a[h--],1==--s){v=!0;break}if(0!==(y=s-Ke(t[u],a,0,s,s-1,e))){for(s-=y,d=(c-=y)+1,p=(h-=y)+1,l=0;l=7||y>=7);if(v)break;f<0&&(f=0),f+=2}(r=f)<1&&(r=1);if(1===s){for(d=(c-=i)+1,p=(u-=i)+1,l=i-1;l>=0;l--)t[d+l]=t[p+l];t[c]=a[h]}else{if(0===s)throw new Error;for(p=c-(s-1),l=0;l1;){var t=o-2;if(t>=1&&i[t-1]<=i[t]+i[t+1]||t>=2&&i[t-2]<=i[t]+i[t-1])i[t-1]i[t+1])break;s(t)}},forceMergeRuns:function(){for(;o>1;){var t=o-2;t>0&&i[t-1]=32;)e|=1&t,t>>=1;return t+e}(r);do{if((o=je(t,n,i,e))s&&(l=s),qe(t,n,n+l,n+o,e),o=l}a.pushRun(n,o),a.mergeRuns(),r-=o,n+=o}while(0!==r);a.forceMergeRuns()}}}var tn=!1;function en(){tn||(tn=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function nn(t,e){return t.zlevel===e.zlevel?t.z===e.z?t.z2-e.z2:t.z-e.z:t.zlevel-e.zlevel}var rn=function(){function t(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=nn}return t.prototype.traverse=function(t,e){for(var n=0;n0&&(u.__clipPaths=[]),isNaN(u.z)&&(en(),u.z=0),isNaN(u.z2)&&(en(),u.z2=0),isNaN(u.zlevel)&&(en(),u.zlevel=0),this._displayList[this._displayListLen++]=u}var h=t.getDecalElement&&t.getDecalElement();h&&this._updateAndAddDisplayable(h,e,n);var c=t.getTextGuideLine();c&&this._updateAndAddDisplayable(c,e,n);var p=t.getTextContent();p&&this._updateAndAddDisplayable(p,e,n)}},t.prototype.addRoot=function(t){t.__zr&&t.__zr.storage===this||this._roots.push(t)},t.prototype.delRoot=function(t){if(t instanceof Array)for(var e=0,n=t.length;e=0&&this._roots.splice(i,1)}},t.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},t.prototype.getRoots=function(){return this._roots},t.prototype.dispose=function(){this._displayList=null,this._roots=null},t}(),on=r.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(t){return setTimeout(t,16)},an={linear:function(t){return t},quadraticIn:function(t){return t*t},quadraticOut:function(t){return t*(2-t)},quadraticInOut:function(t){return(t*=2)<1?.5*t*t:-.5*(--t*(t-2)-1)},cubicIn:function(t){return t*t*t},cubicOut:function(t){return--t*t*t+1},cubicInOut:function(t){return(t*=2)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},quarticIn:function(t){return t*t*t*t},quarticOut:function(t){return 1- --t*t*t*t},quarticInOut:function(t){return(t*=2)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},quinticIn:function(t){return t*t*t*t*t},quinticOut:function(t){return--t*t*t*t*t+1},quinticInOut:function(t){return(t*=2)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},sinusoidalIn:function(t){return 1-Math.cos(t*Math.PI/2)},sinusoidalOut:function(t){return Math.sin(t*Math.PI/2)},sinusoidalInOut:function(t){return.5*(1-Math.cos(Math.PI*t))},exponentialIn:function(t){return 0===t?0:Math.pow(1024,t-1)},exponentialOut:function(t){return 1===t?1:1-Math.pow(2,-10*t)},exponentialInOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?.5*Math.pow(1024,t-1):.5*(2-Math.pow(2,-10*(t-1)))},circularIn:function(t){return 1-Math.sqrt(1-t*t)},circularOut:function(t){return Math.sqrt(1- --t*t)},circularInOut:function(t){return(t*=2)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},elasticIn:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/.4))},elasticOut:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/.4)+1)},elasticInOut:function(t){var e,n=.1,i=.4;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=i*Math.asin(1/n)/(2*Math.PI),(t*=2)<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},backIn:function(t){var e=1.70158;return t*t*((e+1)*t-e)},backOut:function(t){var e=1.70158;return--t*t*((e+1)*t+e)+1},backInOut:function(t){var e=2.5949095;return(t*=2)<1?t*t*((e+1)*t-e)*.5:.5*((t-=2)*t*((e+1)*t+e)+2)},bounceIn:function(t){return 1-an.bounceOut(1-t)},bounceOut:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},bounceInOut:function(t){return t<.5?.5*an.bounceIn(2*t):.5*an.bounceOut(2*t-1)+.5}},sn=Math.pow,ln=Math.sqrt,un=1e-8,hn=1e-4,cn=ln(3),pn=1/3,dn=Mt(),fn=Mt(),gn=Mt();function yn(t){return t>-1e-8&&tun||t<-1e-8}function mn(t,e,n,i,r){var o=1-r;return o*o*(o*t+3*r*e)+r*r*(r*i+3*o*n)}function xn(t,e,n,i,r){var o=1-r;return 3*(((e-t)*o+2*(n-e)*r)*o+(i-n)*r*r)}function _n(t,e,n,i,r,o){var a=i+3*(e-n)-t,s=3*(n-2*e+t),l=3*(e-t),u=t-r,h=s*s-3*a*l,c=s*l-9*a*u,p=l*l-3*s*u,d=0;if(yn(h)&&yn(c)){if(yn(s))o[0]=0;else(M=-l/s)>=0&&M<=1&&(o[d++]=M)}else{var f=c*c-4*h*p;if(yn(f)){var g=c/h,y=-g/2;(M=-s/a+g)>=0&&M<=1&&(o[d++]=M),y>=0&&y<=1&&(o[d++]=y)}else if(f>0){var v=ln(f),m=h*s+1.5*a*(-c+v),x=h*s+1.5*a*(-c-v);(M=(-s-((m=m<0?-sn(-m,pn):sn(m,pn))+(x=x<0?-sn(-x,pn):sn(x,pn))))/(3*a))>=0&&M<=1&&(o[d++]=M)}else{var _=(2*h*s-3*a*c)/(2*ln(h*h*h)),b=Math.acos(_)/3,w=ln(h),S=Math.cos(b),M=(-s-2*w*S)/(3*a),I=(y=(-s+w*(S+cn*Math.sin(b)))/(3*a),(-s+w*(S-cn*Math.sin(b)))/(3*a));M>=0&&M<=1&&(o[d++]=M),y>=0&&y<=1&&(o[d++]=y),I>=0&&I<=1&&(o[d++]=I)}}return d}function bn(t,e,n,i,r){var o=6*n-12*e+6*t,a=9*e+3*i-3*t-9*n,s=3*e-3*t,l=0;if(yn(a)){if(vn(o))(h=-s/o)>=0&&h<=1&&(r[l++]=h)}else{var u=o*o-4*a*s;if(yn(u))r[0]=-o/(2*a);else if(u>0){var h,c=ln(u),p=(-o-c)/(2*a);(h=(-o+c)/(2*a))>=0&&h<=1&&(r[l++]=h),p>=0&&p<=1&&(r[l++]=p)}}return l}function wn(t,e,n,i,r,o){var a=(e-t)*r+t,s=(n-e)*r+e,l=(i-n)*r+n,u=(s-a)*r+a,h=(l-s)*r+s,c=(h-u)*r+u;o[0]=t,o[1]=a,o[2]=u,o[3]=c,o[4]=c,o[5]=h,o[6]=l,o[7]=i}function Sn(t,e,n,i,r,o,a,s,l,u,h){var c,p,d,f,g,y=.005,v=1/0;dn[0]=l,dn[1]=u;for(var m=0;m<1;m+=.05)fn[0]=mn(t,n,r,a,m),fn[1]=mn(e,i,o,s,m),(f=Ft(dn,fn))=0&&f=0&&y=1?1:_n(0,i,o,1,t,s)&&mn(0,r,a,1,s[0])}}}var On=function(){function t(t){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=t.life||1e3,this._delay=t.delay||0,this.loop=t.loop||!1,this.onframe=t.onframe||bt,this.ondestroy=t.ondestroy||bt,this.onrestart=t.onrestart||bt,t.easing&&this.setEasing(t.easing)}return t.prototype.step=function(t,e){if(this._inited||(this._startTime=t+this._delay,this._inited=!0),!this._paused){var n=this._life,i=t-this._startTime-this._pausedTime,r=i/n;r<0&&(r=0),r=Math.min(r,1);var o=this.easingFunc,a=o?o(r):r;if(this.onframe(a),1===r){if(!this.loop)return!0;var s=i%n;this._startTime=t-s,this._pausedTime=0,this.onrestart()}return!1}this._pausedTime+=e},t.prototype.pause=function(){this._paused=!0},t.prototype.resume=function(){this._paused=!1},t.prototype.setEasing=function(t){this.easing=t,this.easingFunc=X(t)?t:an[t]||Pn(t)},t}(),Rn=function(t){this.value=t},Nn=function(){function t(){this._len=0}return t.prototype.insert=function(t){var e=new Rn(t);return this.insertEntry(e),e},t.prototype.insertEntry=function(t){this.head?(this.tail.next=t,t.prev=this.tail,t.next=null,this.tail=t):this.head=this.tail=t,this._len++},t.prototype.remove=function(t){var e=t.prev,n=t.next;e?e.next=n:this.head=n,n?n.prev=e:this.tail=e,t.next=t.prev=null,this._len--},t.prototype.len=function(){return this._len},t.prototype.clear=function(){this.head=this.tail=null,this._len=0},t}(),En=function(){function t(t){this._list=new Nn,this._maxSize=10,this._map={},this._maxSize=t}return t.prototype.put=function(t,e){var n=this._list,i=this._map,r=null;if(null==i[t]){var o=n.len(),a=this._lastRemovedEntry;if(o>=this._maxSize&&o>0){var s=n.head;n.remove(s),delete i[s.key],r=s.value,this._lastRemovedEntry=s}a?a.value=e:a=new Rn(e),a.key=t,n.insertEntry(a),i[t]=a}return r},t.prototype.get=function(t){var e=this._map[t],n=this._list;if(null!=e)return e!==n.tail&&(n.remove(e),n.insertEntry(e)),e.value},t.prototype.clear=function(){this._list.clear(),this._map={}},t.prototype.len=function(){return this._list.len()},t}(),zn={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function Vn(t){return(t=Math.round(t))<0?0:t>255?255:t}function Bn(t){return t<0?0:t>1?1:t}function Fn(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?Vn(parseFloat(e)/100*255):Vn(parseInt(e,10))}function Gn(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?Bn(parseFloat(e)/100):Bn(parseFloat(e))}function Wn(t,e,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?t+(e-t)*n*6:2*n<1?e:3*n<2?t+(e-t)*(2/3-n)*6:t}function Hn(t,e,n){return t+(e-t)*n}function Yn(t,e,n,i,r){return t[0]=e,t[1]=n,t[2]=i,t[3]=r,t}function Xn(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t}var Un=new En(20),Zn=null;function jn(t,e){Zn&&Xn(Zn,e),Zn=Un.put(t,Zn||e.slice())}function qn(t,e){if(t){e=e||[];var n=Un.get(t);if(n)return Xn(e,n);var i=(t+="").replace(/ /g,"").toLowerCase();if(i in zn)return Xn(e,zn[i]),jn(t,e),e;var r,o=i.length;if("#"===i.charAt(0))return 4===o||5===o?(r=parseInt(i.slice(1,4),16))>=0&&r<=4095?(Yn(e,(3840&r)>>4|(3840&r)>>8,240&r|(240&r)>>4,15&r|(15&r)<<4,5===o?parseInt(i.slice(4),16)/15:1),jn(t,e),e):void Yn(e,0,0,0,1):7===o||9===o?(r=parseInt(i.slice(1,7),16))>=0&&r<=16777215?(Yn(e,(16711680&r)>>16,(65280&r)>>8,255&r,9===o?parseInt(i.slice(7),16)/255:1),jn(t,e),e):void Yn(e,0,0,0,1):void 0;var a=i.indexOf("("),s=i.indexOf(")");if(-1!==a&&s+1===o){var l=i.substr(0,a),u=i.substr(a+1,s-(a+1)).split(","),h=1;switch(l){case"rgba":if(4!==u.length)return 3===u.length?Yn(e,+u[0],+u[1],+u[2],1):Yn(e,0,0,0,1);h=Gn(u.pop());case"rgb":return u.length>=3?(Yn(e,Fn(u[0]),Fn(u[1]),Fn(u[2]),3===u.length?h:Gn(u[3])),jn(t,e),e):void Yn(e,0,0,0,1);case"hsla":return 4!==u.length?void Yn(e,0,0,0,1):(u[3]=Gn(u[3]),Kn(u,e),jn(t,e),e);case"hsl":return 3!==u.length?void Yn(e,0,0,0,1):(Kn(u,e),jn(t,e),e);default:return}}Yn(e,0,0,0,1)}}function Kn(t,e){var n=(parseFloat(t[0])%360+360)%360/360,i=Gn(t[1]),r=Gn(t[2]),o=r<=.5?r*(i+1):r+i-r*i,a=2*r-o;return Yn(e=e||[],Vn(255*Wn(a,o,n+1/3)),Vn(255*Wn(a,o,n)),Vn(255*Wn(a,o,n-1/3)),1),4===t.length&&(e[3]=t[3]),e}function $n(t,e){var n=qn(t);if(n){for(var i=0;i<3;i++)n[i]=e<0?n[i]*(1-e)|0:(255-n[i])*e+n[i]|0,n[i]>255?n[i]=255:n[i]<0&&(n[i]=0);return ri(n,4===n.length?"rgba":"rgb")}}function Jn(t,e,n){if(e&&e.length&&t>=0&&t<=1){n=n||[];var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=e[r],s=e[o],l=i-r;return n[0]=Vn(Hn(a[0],s[0],l)),n[1]=Vn(Hn(a[1],s[1],l)),n[2]=Vn(Hn(a[2],s[2],l)),n[3]=Bn(Hn(a[3],s[3],l)),n}}var Qn=Jn;function ti(t,e,n){if(e&&e.length&&t>=0&&t<=1){var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=qn(e[r]),s=qn(e[o]),l=i-r,u=ri([Vn(Hn(a[0],s[0],l)),Vn(Hn(a[1],s[1],l)),Vn(Hn(a[2],s[2],l)),Bn(Hn(a[3],s[3],l))],"rgba");return n?{color:u,leftIndex:r,rightIndex:o,value:i}:u}}var ei=ti;function ni(t,e,n,i){var r=qn(t);if(t)return r=function(t){if(t){var e,n,i=t[0]/255,r=t[1]/255,o=t[2]/255,a=Math.min(i,r,o),s=Math.max(i,r,o),l=s-a,u=(s+a)/2;if(0===l)e=0,n=0;else{n=u<.5?l/(s+a):l/(2-s-a);var h=((s-i)/6+l/2)/l,c=((s-r)/6+l/2)/l,p=((s-o)/6+l/2)/l;i===s?e=p-c:r===s?e=1/3+h-p:o===s&&(e=2/3+c-h),e<0&&(e+=1),e>1&&(e-=1)}var d=[360*e,n,u];return null!=t[3]&&d.push(t[3]),d}}(r),null!=e&&(r[0]=function(t){return(t=Math.round(t))<0?0:t>360?360:t}(e)),null!=n&&(r[1]=Gn(n)),null!=i&&(r[2]=Gn(i)),ri(Kn(r),"rgba")}function ii(t,e){var n=qn(t);if(n&&null!=e)return n[3]=Bn(e),ri(n,"rgba")}function ri(t,e){if(t&&t.length){var n=t[0]+","+t[1]+","+t[2];return"rgba"!==e&&"hsva"!==e&&"hsla"!==e||(n+=","+t[3]),e+"("+n+")"}}function oi(t,e){var n=qn(t);return n?(.299*n[0]+.587*n[1]+.114*n[2])*n[3]/255+(1-n[3])*e:0}var ai=Object.freeze({__proto__:null,parse:qn,lift:$n,toHex:function(t){var e=qn(t);if(e)return((1<<24)+(e[0]<<16)+(e[1]<<8)+ +e[2]).toString(16).slice(1)},fastLerp:Jn,fastMapToColor:Qn,lerp:ti,mapToColor:ei,modifyHSL:ni,modifyAlpha:ii,stringify:ri,lum:oi,random:function(){return ri([Math.round(255*Math.random()),Math.round(255*Math.random()),Math.round(255*Math.random())],"rgb")}}),si=Math.round;function li(t){var e;if(t&&"transparent"!==t){if("string"==typeof t&&t.indexOf("rgba")>-1){var n=qn(t);n&&(t="rgb("+n[0]+","+n[1]+","+n[2]+")",e=n[3])}}else t="none";return{color:t,opacity:null==e?1:e}}var ui=1e-4;function hi(t){return t-1e-4}function ci(t){return si(1e3*t)/1e3}function pi(t){return si(1e4*t)/1e4}var di={left:"start",right:"end",center:"middle",middle:"middle"};function fi(t){return t&&!!t.image}function gi(t){return fi(t)||function(t){return t&&!!t.svgElement}(t)}function yi(t){return"linear"===t.type}function vi(t){return"radial"===t.type}function mi(t){return t&&("linear"===t.type||"radial"===t.type)}function xi(t){return"url(#"+t+")"}function _i(t){var e=t.getGlobalScale(),n=Math.max(e[0],e[1]);return Math.max(Math.ceil(Math.log(n)/Math.log(10)),1)}function bi(t){var e=t.x||0,n=t.y||0,i=(t.rotation||0)*wt,r=rt(t.scaleX,1),o=rt(t.scaleY,1),a=t.skewX||0,s=t.skewY||0,l=[];return(e||n)&&l.push("translate("+e+"px,"+n+"px)"),i&&l.push("rotate("+i+")"),1===r&&1===o||l.push("scale("+r+","+o+")"),(a||s)&&l.push("skew("+si(a*wt)+"deg, "+si(s*wt)+"deg)"),l.join(" ")}var wi=r.hasGlobalWindow&&X(window.btoa)?function(t){return window.btoa(unescape(encodeURIComponent(t)))}:"undefined"!=typeof Buffer?function(t){return Buffer.from(t).toString("base64")}:function(t){return null},Si=Array.prototype.slice;function Mi(t,e,n){return(e-t)*n+t}function Ii(t,e,n,i){for(var r=e.length,o=0;oi?e:t,o=Math.min(n,i),a=r[o-1]||{color:[0,0,0,0],offset:0},s=o;sa)i.length=a;else for(var s=o;s=1},t.prototype.getAdditiveTrack=function(){return this._additiveTrack},t.prototype.addKeyframe=function(t,e,n){this._needsSort=!0;var i=this.keyframes,r=i.length,o=!1,a=6,s=e;if(N(e)){var l=function(t){return N(t&&t[0])?2:1}(e);a=l,(1===l&&!j(e[0])||2===l&&!j(e[0][0]))&&(o=!0)}else if(j(e)&&!nt(e))a=0;else if(U(e))if(isNaN(+e)){var u=qn(e);u&&(s=u,a=3)}else a=0;else if(Q(e)){var h=A({},s);h.colorStops=z(e.colorStops,(function(t){return{offset:t.offset,color:qn(t.color)}})),yi(e)?a=4:vi(e)&&(a=5),s=h}0===r?this.valType=a:a===this.valType&&6!==a||(o=!0),this.discrete=this.discrete||o;var c={time:t,value:s,rawValue:e,percent:0};return n&&(c.easing=n,c.easingFunc=X(n)?n:an[n]||Pn(n)),i.push(c),c},t.prototype.prepare=function(t,e){var n=this.keyframes;this._needsSort&&n.sort((function(t,e){return t.time-e.time}));for(var i=this.valType,r=n.length,o=n[r-1],a=this.discrete,s=Oi(i),l=Pi(i),u=0;u=0&&!(l[n].percent<=e);n--);n=d(n,u-2)}else{for(n=p;ne);n++);n=d(n-1,u-2)}r=l[n+1],i=l[n]}if(i&&r){this._lastFr=n,this._lastFrP=e;var f=r.percent-i.percent,g=0===f?1:d((e-i.percent)/f,1);r.easingFunc&&(g=r.easingFunc(g));var y=o?this._additiveValue:c?Ri:t[h];if(!Oi(s)&&!c||y||(y=this._additiveValue=[]),this.discrete)t[h]=g<1?i.rawValue:r.rawValue;else if(Oi(s))1===s?Ii(y,i[a],r[a],g):function(t,e,n,i){for(var r=e.length,o=r&&e[0].length,a=0;a0&&s.addKeyframe(0,ki(l),i),this._trackKeys.push(a)}s.addKeyframe(t,ki(e[a]),i)}return this._maxTime=Math.max(this._maxTime,t),this},t.prototype.pause=function(){this._clip.pause(),this._paused=!0},t.prototype.resume=function(){this._clip.resume(),this._paused=!1},t.prototype.isPaused=function(){return!!this._paused},t.prototype.duration=function(t){return this._maxTime=t,this._force=!0,this},t.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var t=this._doneCbs;if(t)for(var e=t.length,n=0;n0)){this._started=1;for(var e=this,n=[],i=this._maxTime||0,r=0;r1){var a=o.pop();r.addKeyframe(a.time,t[i]),r.prepare(this._maxTime,r.getAdditiveTrack())}}}},t}();function zi(){return(new Date).getTime()}var Vi,Bi,Fi=function(t){function e(e){var n=t.call(this)||this;return n._running=!1,n._time=0,n._pausedTime=0,n._pauseStart=0,n._paused=!1,e=e||{},n.stage=e.stage||{},n}return n(e,t),e.prototype.addClip=function(t){t.animation&&this.removeClip(t),this._head?(this._tail.next=t,t.prev=this._tail,t.next=null,this._tail=t):this._head=this._tail=t,t.animation=this},e.prototype.addAnimator=function(t){t.animation=this;var e=t.getClip();e&&this.addClip(e)},e.prototype.removeClip=function(t){if(t.animation){var e=t.prev,n=t.next;e?e.next=n:this._head=n,n?n.prev=e:this._tail=e,t.next=t.prev=t.animation=null}},e.prototype.removeAnimator=function(t){var e=t.getClip();e&&this.removeClip(e),t.animation=null},e.prototype.update=function(t){for(var e=zi()-this._pausedTime,n=e-this._time,i=this._head;i;){var r=i.next;i.step(e,n)?(i.ondestroy(),this.removeClip(i),i=r):i=r}this._time=e,t||(this.trigger("frame",n),this.stage.update&&this.stage.update())},e.prototype._startLoop=function(){var t=this;this._running=!0,on((function e(){t._running&&(on(e),!t._paused&&t.update())}))},e.prototype.start=function(){this._running||(this._time=zi(),this._pausedTime=0,this._startLoop())},e.prototype.stop=function(){this._running=!1},e.prototype.pause=function(){this._paused||(this._pauseStart=zi(),this._paused=!0)},e.prototype.resume=function(){this._paused&&(this._pausedTime+=zi()-this._pauseStart,this._paused=!1)},e.prototype.clear=function(){for(var t=this._head;t;){var e=t.next;t.prev=t.next=t.animation=null,t=e}this._head=this._tail=null},e.prototype.isFinished=function(){return null==this._head},e.prototype.animate=function(t,e){e=e||{},this.start();var n=new Ei(t,e.loop);return this.addAnimator(n),n},e}(jt),Gi=r.domSupported,Wi=(Bi={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},{mouse:Vi=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],touch:["touchstart","touchend","touchmove"],pointer:z(Vi,(function(t){var e=t.replace("mouse","pointer");return Bi.hasOwnProperty(e)?e:t}))}),Hi=["mousemove","mouseup"],Yi=["pointermove","pointerup"],Xi=!1;function Ui(t){var e=t.pointerType;return"pen"===e||"touch"===e}function Zi(t){t&&(t.zrByTouch=!0)}function ji(t,e){for(var n=e,i=!1;n&&9!==n.nodeType&&!(i=n.domBelongToZr||n!==e&&n===t.painterRoot);)n=n.parentNode;return i}var qi=function(t,e){this.stopPropagation=bt,this.stopImmediatePropagation=bt,this.preventDefault=bt,this.type=e.type,this.target=this.currentTarget=t.dom,this.pointerType=e.pointerType,this.clientX=e.clientX,this.clientY=e.clientY},Ki={mousedown:function(t){t=ce(this.dom,t),this.__mayPointerCapture=[t.zrX,t.zrY],this.trigger("mousedown",t)},mousemove:function(t){t=ce(this.dom,t);var e=this.__mayPointerCapture;!e||t.zrX===e[0]&&t.zrY===e[1]||this.__togglePointerCapture(!0),this.trigger("mousemove",t)},mouseup:function(t){t=ce(this.dom,t),this.__togglePointerCapture(!1),this.trigger("mouseup",t)},mouseout:function(t){ji(this,(t=ce(this.dom,t)).toElement||t.relatedTarget)||(this.__pointerCapturing&&(t.zrEventControl="no_globalout"),this.trigger("mouseout",t))},wheel:function(t){Xi=!0,t=ce(this.dom,t),this.trigger("mousewheel",t)},mousewheel:function(t){Xi||(t=ce(this.dom,t),this.trigger("mousewheel",t))},touchstart:function(t){Zi(t=ce(this.dom,t)),this.__lastTouchMoment=new Date,this.handler.processGesture(t,"start"),Ki.mousemove.call(this,t),Ki.mousedown.call(this,t)},touchmove:function(t){Zi(t=ce(this.dom,t)),this.handler.processGesture(t,"change"),Ki.mousemove.call(this,t)},touchend:function(t){Zi(t=ce(this.dom,t)),this.handler.processGesture(t,"end"),Ki.mouseup.call(this,t),+new Date-+this.__lastTouchMoment<300&&Ki.click.call(this,t)},pointerdown:function(t){Ki.mousedown.call(this,t)},pointermove:function(t){Ui(t)||Ki.mousemove.call(this,t)},pointerup:function(t){Ki.mouseup.call(this,t)},pointerout:function(t){Ui(t)||Ki.mouseout.call(this,t)}};E(["click","dblclick","contextmenu"],(function(t){Ki[t]=function(e){e=ce(this.dom,e),this.trigger(t,e)}}));var $i={pointermove:function(t){Ui(t)||$i.mousemove.call(this,t)},pointerup:function(t){$i.mouseup.call(this,t)},mousemove:function(t){this.trigger("mousemove",t)},mouseup:function(t){var e=this.__pointerCapturing;this.__togglePointerCapture(!1),this.trigger("mouseup",t),e&&(t.zrEventControl="only_globalout",this.trigger("mouseout",t))}};function Ji(t,e){var n=e.domHandlers;r.pointerEventsSupported?E(Wi.pointer,(function(i){tr(e,i,(function(e){n[i].call(t,e)}))})):(r.touchEventsSupported&&E(Wi.touch,(function(i){tr(e,i,(function(r){n[i].call(t,r),function(t){t.touching=!0,null!=t.touchTimer&&(clearTimeout(t.touchTimer),t.touchTimer=null),t.touchTimer=setTimeout((function(){t.touching=!1,t.touchTimer=null}),700)}(e)}))})),E(Wi.mouse,(function(i){tr(e,i,(function(r){r=he(r),e.touching||n[i].call(t,r)}))})))}function Qi(t,e){function n(n){tr(e,n,(function(i){i=he(i),ji(t,i.target)||(i=function(t,e){return ce(t.dom,new qi(t,e),!0)}(t,i),e.domHandlers[n].call(t,i))}),{capture:!0})}r.pointerEventsSupported?E(Yi,n):r.touchEventsSupported||E(Hi,n)}function tr(t,e,n,i){t.mounted[e]=n,t.listenerOpts[e]=i,pe(t.domTarget,e,n,i)}function er(t){var e,n,i,r,o=t.mounted;for(var a in o)o.hasOwnProperty(a)&&(e=t.domTarget,n=a,i=o[a],r=t.listenerOpts[a],e.removeEventListener(n,i,r));t.mounted={}}var nr=function(t,e){this.mounted={},this.listenerOpts={},this.touching=!1,this.domTarget=t,this.domHandlers=e},ir=function(t){function e(e,n){var i=t.call(this)||this;return i.__pointerCapturing=!1,i.dom=e,i.painterRoot=n,i._localHandlerScope=new nr(e,Ki),Gi&&(i._globalHandlerScope=new nr(document,$i)),Ji(i,i._localHandlerScope),i}return n(e,t),e.prototype.dispose=function(){er(this._localHandlerScope),Gi&&er(this._globalHandlerScope)},e.prototype.setCursor=function(t){this.dom.style&&(this.dom.style.cursor=t||"default")},e.prototype.__togglePointerCapture=function(t){if(this.__mayPointerCapture=null,Gi&&+this.__pointerCapturing^+t){this.__pointerCapturing=t;var e=this._globalHandlerScope;t?Qi(this,e):er(e)}},e}(jt),rr=1;r.hasGlobalWindow&&(rr=Math.max(window.devicePixelRatio||window.screen&&window.screen.deviceXDPI/window.screen.logicalXDPI||1,1));var or=rr,ar="#333",sr="#ccc",lr=xe,ur=5e-5;function hr(t){return t>ur||t<-5e-5}var cr=[],pr=[],dr=[1,0,0,1,0,0],fr=Math.abs,gr=function(){function t(){}return t.prototype.getLocalTransform=function(e){return t.getLocalTransform(this,e)},t.prototype.setPosition=function(t){this.x=t[0],this.y=t[1]},t.prototype.setScale=function(t){this.scaleX=t[0],this.scaleY=t[1]},t.prototype.setSkew=function(t){this.skewX=t[0],this.skewY=t[1]},t.prototype.setOrigin=function(t){this.originX=t[0],this.originY=t[1]},t.prototype.needLocalTransform=function(){return hr(this.rotation)||hr(this.x)||hr(this.y)||hr(this.scaleX-1)||hr(this.scaleY-1)||hr(this.skewX)||hr(this.skewY)},t.prototype.updateTransform=function(){var t=this.parent&&this.parent.transform,e=this.needLocalTransform(),n=this.transform;e||t?(n=n||[1,0,0,1,0,0],e?this.getLocalTransform(n):lr(n),t&&(e?be(n,t,n):_e(n,t)),this.transform=n,this._resolveGlobalScaleRatio(n)):n&&(lr(n),this.invTransform=null)},t.prototype._resolveGlobalScaleRatio=function(t){var e=this.globalScaleRatio;if(null!=e&&1!==e){this.getGlobalScale(cr);var n=cr[0]<0?-1:1,i=cr[1]<0?-1:1,r=((cr[0]-n)*e+n)/cr[0]||0,o=((cr[1]-i)*e+i)/cr[1]||0;t[0]*=r,t[1]*=r,t[2]*=o,t[3]*=o}this.invTransform=this.invTransform||[1,0,0,1,0,0],Ie(this.invTransform,t)},t.prototype.getComputedTransform=function(){for(var t=this,e=[];t;)e.push(t),t=t.parent;for(;t=e.pop();)t.updateTransform();return this.transform},t.prototype.setLocalTransform=function(t){if(t){var e=t[0]*t[0]+t[1]*t[1],n=t[2]*t[2]+t[3]*t[3],i=Math.atan2(t[1],t[0]),r=Math.PI/2+i-Math.atan2(t[3],t[2]);n=Math.sqrt(n)*Math.cos(r),e=Math.sqrt(e),this.skewX=r,this.skewY=0,this.rotation=-i,this.x=+t[4],this.y=+t[5],this.scaleX=e,this.scaleY=n,this.originX=0,this.originY=0}},t.prototype.decomposeTransform=function(){if(this.transform){var t=this.parent,e=this.transform;t&&t.transform&&(be(pr,t.invTransform,e),e=pr);var n=this.originX,i=this.originY;(n||i)&&(dr[4]=n,dr[5]=i,be(pr,e,dr),pr[4]-=n,pr[5]-=i,e=pr),this.setLocalTransform(e)}},t.prototype.getGlobalScale=function(t){var e=this.transform;return t=t||[],e?(t[0]=Math.sqrt(e[0]*e[0]+e[1]*e[1]),t[1]=Math.sqrt(e[2]*e[2]+e[3]*e[3]),e[0]<0&&(t[0]=-t[0]),e[3]<0&&(t[1]=-t[1]),t):(t[0]=1,t[1]=1,t)},t.prototype.transformCoordToLocal=function(t,e){var n=[t,e],i=this.invTransform;return i&&Wt(n,n,i),n},t.prototype.transformCoordToGlobal=function(t,e){var n=[t,e],i=this.transform;return i&&Wt(n,n,i),n},t.prototype.getLineScale=function(){var t=this.transform;return t&&fr(t[0]-1)>1e-10&&fr(t[3]-1)>1e-10?Math.sqrt(fr(t[0]*t[3]-t[2]*t[1])):1},t.prototype.copyTransform=function(t){vr(this,t)},t.getLocalTransform=function(t,e){e=e||[];var n=t.originX||0,i=t.originY||0,r=t.scaleX,o=t.scaleY,a=t.anchorX,s=t.anchorY,l=t.rotation||0,u=t.x,h=t.y,c=t.skewX?Math.tan(t.skewX):0,p=t.skewY?Math.tan(-t.skewY):0;if(n||i||a||s){var d=n+a,f=i+s;e[4]=-d*r-c*f*o,e[5]=-f*o-p*d*r}else e[4]=e[5]=0;return e[0]=r,e[3]=o,e[1]=p*r,e[2]=c*o,l&&Se(e,e,l),e[4]+=n+u,e[5]+=i+h,e},t.initDefaultProps=function(){var e=t.prototype;e.scaleX=e.scaleY=e.globalScaleRatio=1,e.x=e.y=e.originX=e.originY=e.skewX=e.skewY=e.rotation=e.anchorX=e.anchorY=0}(),t}(),yr=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function vr(t,e){for(var n=0;n=0?parseFloat(t)/100*e:parseFloat(t):t}function Tr(t,e,n){var i=e.position||"inside",r=null!=e.distance?e.distance:5,o=n.height,a=n.width,s=o/2,l=n.x,u=n.y,h="left",c="top";if(i instanceof Array)l+=Ir(i[0],n.width),u+=Ir(i[1],n.height),h=null,c=null;else switch(i){case"left":l-=r,u+=s,h="right",c="middle";break;case"right":l+=r+a,u+=s,c="middle";break;case"top":l+=a/2,u-=r,h="center",c="bottom";break;case"bottom":l+=a/2,u+=o+r,h="center";break;case"inside":l+=a/2,u+=s,h="center",c="middle";break;case"insideLeft":l+=r,u+=s,c="middle";break;case"insideRight":l+=a-r,u+=s,h="right",c="middle";break;case"insideTop":l+=a/2,u+=r,h="center";break;case"insideBottom":l+=a/2,u+=o-r,h="center",c="bottom";break;case"insideTopLeft":l+=r,u+=r;break;case"insideTopRight":l+=a-r,u+=r,h="right";break;case"insideBottomLeft":l+=r,u+=o-r,c="bottom";break;case"insideBottomRight":l+=a-r,u+=o-r,h="right",c="bottom"}return(t=t||{}).x=l,t.y=u,t.align=h,t.verticalAlign=c,t}var Cr="__zr_normal__",Dr=yr.concat(["ignore"]),Ar=V(yr,(function(t,e){return t[e]=!0,t}),{ignore:!1}),kr={},Lr=new ze(0,0,0,0),Pr=function(){function t(t){this.id=M(),this.animators=[],this.currentStates=[],this.states={},this._init(t)}return t.prototype._init=function(t){this.attr(t)},t.prototype.drift=function(t,e,n){switch(this.draggable){case"horizontal":e=0;break;case"vertical":t=0}var i=this.transform;i||(i=this.transform=[1,0,0,1,0,0]),i[4]+=t,i[5]+=e,this.decomposeTransform(),this.markRedraw()},t.prototype.beforeUpdate=function(){},t.prototype.afterUpdate=function(){},t.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},t.prototype.updateInnerText=function(t){var e=this._textContent;if(e&&(!e.ignore||t)){this.textConfig||(this.textConfig={});var n=this.textConfig,i=n.local,r=e.innerTransformable,o=void 0,a=void 0,s=!1;r.parent=i?this:null;var l=!1;if(r.copyTransform(e),null!=n.position){var u=Lr;n.layoutRect?u.copy(n.layoutRect):u.copy(this.getBoundingRect()),i||u.applyTransform(this.transform),this.calculateTextPosition?this.calculateTextPosition(kr,n,u):Tr(kr,n,u),r.x=kr.x,r.y=kr.y,o=kr.align,a=kr.verticalAlign;var h=n.origin;if(h&&null!=n.rotation){var c=void 0,p=void 0;"center"===h?(c=.5*u.width,p=.5*u.height):(c=Ir(h[0],u.width),p=Ir(h[1],u.height)),l=!0,r.originX=-r.x+c+(i?0:u.x),r.originY=-r.y+p+(i?0:u.y)}}null!=n.rotation&&(r.rotation=n.rotation);var d=n.offset;d&&(r.x+=d[0],r.y+=d[1],l||(r.originX=-d[0],r.originY=-d[1]));var f=null==n.inside?"string"==typeof n.position&&n.position.indexOf("inside")>=0:n.inside,g=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={}),y=void 0,v=void 0,m=void 0;f&&this.canBeInsideText()?(y=n.insideFill,v=n.insideStroke,null!=y&&"auto"!==y||(y=this.getInsideTextFill()),null!=v&&"auto"!==v||(v=this.getInsideTextStroke(y),m=!0)):(y=n.outsideFill,v=n.outsideStroke,null!=y&&"auto"!==y||(y=this.getOutsideFill()),null!=v&&"auto"!==v||(v=this.getOutsideStroke(y),m=!0)),(y=y||"#000")===g.fill&&v===g.stroke&&m===g.autoStroke&&o===g.align&&a===g.verticalAlign||(s=!0,g.fill=y,g.stroke=v,g.autoStroke=m,g.align=o,g.verticalAlign=a,e.setDefaultTextStyle(g)),e.__dirty|=1,s&&e.dirtyStyle(!0)}},t.prototype.canBeInsideText=function(){return!0},t.prototype.getInsideTextFill=function(){return"#fff"},t.prototype.getInsideTextStroke=function(t){return"#000"},t.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?sr:ar},t.prototype.getOutsideStroke=function(t){var e=this.__zr&&this.__zr.getBackgroundColor(),n="string"==typeof e&&qn(e);n||(n=[255,255,255,1]);for(var i=n[3],r=this.__zr.isDarkMode(),o=0;o<3;o++)n[o]=n[o]*i+(r?0:255)*(1-i);return n[3]=1,ri(n,"rgba")},t.prototype.traverse=function(t,e){},t.prototype.attrKV=function(t,e){"textConfig"===t?this.setTextConfig(e):"textContent"===t?this.setTextContent(e):"clipPath"===t?this.setClipPath(e):"extra"===t?(this.extra=this.extra||{},A(this.extra,e)):this[t]=e},t.prototype.hide=function(){this.ignore=!0,this.markRedraw()},t.prototype.show=function(){this.ignore=!1,this.markRedraw()},t.prototype.attr=function(t,e){if("string"==typeof t)this.attrKV(t,e);else if(q(t))for(var n=G(t),i=0;i0},t.prototype.getState=function(t){return this.states[t]},t.prototype.ensureState=function(t){var e=this.states;return e[t]||(e[t]={}),e[t]},t.prototype.clearStates=function(t){this.useState(Cr,!1,t)},t.prototype.useState=function(t,e,n,i){var r=t===Cr;if(this.hasState()||!r){var o=this.currentStates,a=this.stateTransition;if(!(P(o,t)>=0)||!e&&1!==o.length){var s;if(this.stateProxy&&!r&&(s=this.stateProxy(t)),s||(s=this.states&&this.states[t]),s||r){r||this.saveCurrentToNormalState(s);var l=!!(s&&s.hoverLayer||i);l&&this._toggleHoverLayerFlag(!0),this._applyStateObj(t,s,this._normalState,e,!n&&!this.__inHover&&a&&a.duration>0,a);var u=this._textContent,h=this._textGuide;return u&&u.useState(t,e,n,l),h&&h.useState(t,e,n,l),r?(this.currentStates=[],this._normalState={}):e?this.currentStates.push(t):this.currentStates=[t],this._updateAnimationTargets(),this.markRedraw(),!l&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2),s}I("State "+t+" not exists.")}}},t.prototype.useStates=function(t,e,n){if(t.length){var i=[],r=this.currentStates,o=t.length,a=o===r.length;if(a)for(var s=0;s0,d);var f=this._textContent,g=this._textGuide;f&&f.useStates(t,e,c),g&&g.useStates(t,e,c),this._updateAnimationTargets(),this.currentStates=t.slice(),this.markRedraw(),!c&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2)}else this.clearStates()},t.prototype._updateAnimationTargets=function(){for(var t=0;t=0){var n=this.currentStates.slice();n.splice(e,1),this.useStates(n)}},t.prototype.replaceState=function(t,e,n){var i=this.currentStates.slice(),r=P(i,t),o=P(i,e)>=0;r>=0?o?i.splice(r,1):i[r]=e:n&&!o&&i.push(e),this.useStates(i)},t.prototype.toggleState=function(t,e){e?this.useState(t,!0):this.removeState(t)},t.prototype._mergeStates=function(t){for(var e,n={},i=0;i=0&&e.splice(n,1)})),this.animators.push(t),n&&n.animation.addAnimator(t),n&&n.wakeUp()},t.prototype.updateDuringAnimation=function(t){this.markRedraw()},t.prototype.stopAnimation=function(t,e){for(var n=this.animators,i=n.length,r=[],o=0;o0&&n.during&&o[0].during((function(t,e){n.during(e)}));for(var p=0;p0||r.force&&!a.length){var w,S=void 0,M=void 0,I=void 0;if(s){M={},p&&(S={});for(_=0;_=0&&(n.splice(i,0,t),this._doAdd(t))}return this},e.prototype.replace=function(t,e){var n=P(this._children,t);return n>=0&&this.replaceAt(e,n),this},e.prototype.replaceAt=function(t,e){var n=this._children,i=n[e];if(t&&t!==this&&t.parent!==this&&t!==i){n[e]=t,i.parent=null;var r=this.__zr;r&&i.removeSelfFromZr(r),this._doAdd(t)}return this},e.prototype._doAdd=function(t){t.parent&&t.parent.remove(t),t.parent=this;var e=this.__zr;e&&e!==t.__zr&&t.addSelfToZr(e),e&&e.refresh()},e.prototype.remove=function(t){var e=this.__zr,n=this._children,i=P(n,t);return i<0||(n.splice(i,1),t.parent=null,e&&t.removeSelfFromZr(e),e&&e.refresh()),this},e.prototype.removeAll=function(){for(var t=this._children,e=this.__zr,n=0;n0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},t.prototype.setSleepAfterStill=function(t){this._sleepAfterStill=t},t.prototype.wakeUp=function(){this.animation.start(),this._stillFrameAccum=0},t.prototype.refreshHover=function(){this._needsRefreshHover=!0},t.prototype.refreshHoverImmediately=function(){this._needsRefreshHover=!1,this.painter.refreshHover&&"canvas"===this.painter.getType()&&this.painter.refreshHover()},t.prototype.resize=function(t){t=t||{},this.painter.resize(t.width,t.height),this.handler.resize()},t.prototype.clearAnimation=function(){this.animation.clear()},t.prototype.getWidth=function(){return this.painter.getWidth()},t.prototype.getHeight=function(){return this.painter.getHeight()},t.prototype.setCursorStyle=function(t){this.handler.setCursorStyle(t)},t.prototype.findHover=function(t,e){return this.handler.findHover(t,e)},t.prototype.on=function(t,e,n){return this.handler.on(t,e,n),this},t.prototype.off=function(t,e){this.handler.off(t,e)},t.prototype.trigger=function(t,e){this.handler.trigger(t,e)},t.prototype.clear=function(){for(var t=this.storage.getRoots(),e=0;e0){if(t<=r)return a;if(t>=o)return s}else{if(t>=r)return a;if(t<=o)return s}else{if(t===r)return a;if(t===o)return s}return(t-r)/l*u+a}function Ur(t,e){switch(t){case"center":case"middle":t="50%";break;case"left":case"top":t="0%";break;case"right":case"bottom":t="100%"}return U(t)?(n=t,n.replace(/^\s+|\s+$/g,"")).match(/%$/)?parseFloat(t)/100*e:parseFloat(t):null==t?NaN:+t;var n}function Zr(t,e,n){return null==e&&(e=10),e=Math.min(Math.max(0,e),20),t=(+t).toFixed(e),n?t:+t}function jr(t){return t.sort((function(t,e){return t-e})),t}function qr(t){if(t=+t,isNaN(t))return 0;if(t>1e-14)for(var e=1,n=0;n<15;n++,e*=10)if(Math.round(t*e)/e===t)return n;return Kr(t)}function Kr(t){var e=t.toString().toLowerCase(),n=e.indexOf("e"),i=n>0?+e.slice(n+1):0,r=n>0?n:e.length,o=e.indexOf("."),a=o<0?0:r-1-o;return Math.max(0,a-i)}function $r(t,e){var n=Math.log,i=Math.LN10,r=Math.floor(n(t[1]-t[0])/i),o=Math.round(n(Math.abs(e[1]-e[0]))/i),a=Math.min(Math.max(-r+o,0),20);return isFinite(a)?a:20}function Jr(t,e){var n=V(t,(function(t,e){return t+(isNaN(e)?0:e)}),0);if(0===n)return[];for(var i=Math.pow(10,e),r=z(t,(function(t){return(isNaN(t)?0:t)/n*i*100})),o=100*i,a=z(r,(function(t){return Math.floor(t)})),s=V(a,(function(t,e){return t+e}),0),l=z(r,(function(t,e){return t-a[e]}));su&&(u=l[c],h=c);++a[h],l[h]=0,++s}return z(a,(function(t){return t/i}))}function Qr(t,e){var n=Math.max(qr(t),qr(e)),i=t+e;return n>20?i:Zr(i,n)}var to=9007199254740991;function eo(t){var e=2*Math.PI;return(t%e+e)%e}function no(t){return t>-1e-4&&t=10&&e++,e}function so(t,e){var n=ao(t),i=Math.pow(10,n),r=t/i;return t=(e?r<1.5?1:r<2.5?2:r<4?3:r<7?5:10:r<1?1:r<2?2:r<3?3:r<5?5:10)*i,n>=-20?+t.toFixed(n<0?-n:0):t}function lo(t,e){var n=(t.length-1)*e+1,i=Math.floor(n),r=+t[i-1],o=n-i;return o?r+o*(t[i]-r):r}function uo(t){t.sort((function(t,e){return s(t,e,0)?-1:1}));for(var e=-1/0,n=1,i=0;i=0||r&&P(r,s)<0)){var l=n.getShallow(s,e);null!=l&&(o[t[a][0]]=l)}}return o}}var Qo=Jo([["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]]),ta=function(){function t(){}return t.prototype.getAreaStyle=function(t,e){return Qo(this,t,e)},t}(),ea=new En(50);function na(t){if("string"==typeof t){var e=ea.get(t);return e&&e.image}return t}function ia(t,e,n,i,r){if(t){if("string"==typeof t){if(e&&e.__zrImageSrc===t||!n)return e;var o=ea.get(t),a={hostEl:n,cb:i,cbPayload:r};return o?!oa(e=o.image)&&o.pending.push(a):((e=h.loadImage(t,ra,ra)).__zrImageSrc=t,ea.put(t,e.__cachedImgObj={image:e,pending:[a]})),e}return t}return e}function ra(){var t=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var e=0;e=a;l++)s-=a;var u=xr(n,e);return u>s&&(n="",u=0),s=t-u,r.ellipsis=n,r.ellipsisWidth=u,r.contentWidth=s,r.containerWidth=t,r}function ua(t,e){var n=e.containerWidth,i=e.font,r=e.contentWidth;if(!n)return"";var o=xr(t,i);if(o<=n)return t;for(var a=0;;a++){if(o<=r||a>=e.maxIterations){t+=e.ellipsis;break}var s=0===a?ha(t,r,e.ascCharWidth,e.cnCharWidth):o>0?Math.floor(t.length*r/o):0;o=xr(t=t.substr(0,s),i)}return""===t&&(t=e.placeholder),t}function ha(t,e,n,i){for(var r=0,o=0,a=t.length;o0&&f+i.accumWidth>i.width&&(o=e.split("\n"),c=!0),i.accumWidth=f}else{var g=va(e,h,i.width,i.breakAll,i.accumWidth);i.accumWidth=g.accumWidth+d,a=g.linesWidths,o=g.lines}}else o=e.split("\n");for(var y=0;y=32&&e<=591||e>=880&&e<=4351||e>=4608&&e<=5119||e>=7680&&e<=8303}(t)||!!ga[t]}function va(t,e,n,i,r){for(var o=[],a=[],s="",l="",u=0,h=0,c=0;cn:r+h+d>n)?h?(s||l)&&(f?(s||(s=l,l="",h=u=0),o.push(s),a.push(h-u),l+=p,s="",h=u+=d):(l&&(s+=l,l="",u=0),o.push(s),a.push(h),s=p,h=d)):f?(o.push(l),a.push(u),l=p,u=d):(o.push(p),a.push(d)):(h+=d,f?(l+=p,u+=d):(l&&(s+=l,l="",u=0),s+=p))}else l&&(s+=l,h+=u),o.push(s),a.push(h),s="",l="",u=0,h=0}return o.length||s||(s=t,l="",u=0),l&&(s+=l),s&&(o.push(s),a.push(h)),1===o.length&&(h+=r),{accumWidth:h,lines:o,linesWidths:a}}var ma="__zr_style_"+Math.round(10*Math.random()),xa={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},_a={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};xa[ma]=!0;var ba=["z","z2","invisible"],wa=["invisible"],Sa=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype._init=function(e){for(var n=G(e),i=0;i1e-4)return s[0]=t-n,s[1]=e-i,l[0]=t+n,void(l[1]=e+i);if(La[0]=Aa(r)*n+t,La[1]=Da(r)*i+e,Pa[0]=Aa(o)*n+t,Pa[1]=Da(o)*i+e,u(s,La,Pa),h(l,La,Pa),(r%=ka)<0&&(r+=ka),(o%=ka)<0&&(o+=ka),r>o&&!a?o+=ka:rr&&(Oa[0]=Aa(d)*n+t,Oa[1]=Da(d)*i+e,u(s,Oa,s),h(l,Oa,l))}var Ga={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},Wa=[],Ha=[],Ya=[],Xa=[],Ua=[],Za=[],ja=Math.min,qa=Math.max,Ka=Math.cos,$a=Math.sin,Ja=Math.abs,Qa=Math.PI,ts=2*Qa,es="undefined"!=typeof Float32Array,ns=[];function is(t){return Math.round(t/Qa*1e8)/1e8%2*Qa}function rs(t,e){var n=is(t[0]);n<0&&(n+=ts);var i=n-t[0],r=t[1];r+=i,!e&&r-n>=ts?r=n+ts:e&&n-r>=ts?r=n-ts:!e&&n>r?r=n+(ts-is(n-r)):e&&n0&&(this._ux=Ja(n/or/t)||0,this._uy=Ja(n/or/e)||0)},t.prototype.setDPR=function(t){this.dpr=t},t.prototype.setContext=function(t){this._ctx=t},t.prototype.getContext=function(){return this._ctx},t.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},t.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},t.prototype.moveTo=function(t,e){return this._drawPendingPt(),this.addData(Ga.M,t,e),this._ctx&&this._ctx.moveTo(t,e),this._x0=t,this._y0=e,this._xi=t,this._yi=e,this},t.prototype.lineTo=function(t,e){var n=Ja(t-this._xi),i=Ja(e-this._yi),r=n>this._ux||i>this._uy;if(this.addData(Ga.L,t,e),this._ctx&&r&&this._ctx.lineTo(t,e),r)this._xi=t,this._yi=e,this._pendingPtDist=0;else{var o=n*n+i*i;o>this._pendingPtDist&&(this._pendingPtX=t,this._pendingPtY=e,this._pendingPtDist=o)}return this},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){return this._drawPendingPt(),this.addData(Ga.C,t,e,n,i,r,o),this._ctx&&this._ctx.bezierCurveTo(t,e,n,i,r,o),this._xi=r,this._yi=o,this},t.prototype.quadraticCurveTo=function(t,e,n,i){return this._drawPendingPt(),this.addData(Ga.Q,t,e,n,i),this._ctx&&this._ctx.quadraticCurveTo(t,e,n,i),this._xi=n,this._yi=i,this},t.prototype.arc=function(t,e,n,i,r,o){this._drawPendingPt(),ns[0]=i,ns[1]=r,rs(ns,o),i=ns[0];var a=(r=ns[1])-i;return this.addData(Ga.A,t,e,n,n,i,a,0,o?0:1),this._ctx&&this._ctx.arc(t,e,n,i,r,o),this._xi=Ka(r)*n+t,this._yi=$a(r)*n+e,this},t.prototype.arcTo=function(t,e,n,i,r){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(t,e,n,i,r),this},t.prototype.rect=function(t,e,n,i){return this._drawPendingPt(),this._ctx&&this._ctx.rect(t,e,n,i),this.addData(Ga.R,t,e,n,i),this},t.prototype.closePath=function(){this._drawPendingPt(),this.addData(Ga.Z);var t=this._ctx,e=this._x0,n=this._y0;return t&&t.closePath(),this._xi=e,this._yi=n,this},t.prototype.fill=function(t){t&&t.fill(),this.toStatic()},t.prototype.stroke=function(t){t&&t.stroke(),this.toStatic()},t.prototype.len=function(){return this._len},t.prototype.setData=function(t){var e=t.length;this.data&&this.data.length===e||!es||(this.data=new Float32Array(e));for(var n=0;nu.length&&(this._expandData(),u=this.data);for(var h=0;h0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},t.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var t=[],e=0;e11&&(this.data=new Float32Array(t)))}},t.prototype.getBoundingRect=function(){Ya[0]=Ya[1]=Ua[0]=Ua[1]=Number.MAX_VALUE,Xa[0]=Xa[1]=Za[0]=Za[1]=-Number.MAX_VALUE;var t,e=this.data,n=0,i=0,r=0,o=0;for(t=0;tn||Ja(y)>i||c===e-1)&&(f=Math.sqrt(A*A+y*y),r=g,o=x);break;case Ga.C:var v=t[c++],m=t[c++],x=(g=t[c++],t[c++]),_=t[c++],b=t[c++];f=Mn(r,o,v,m,g,x,_,b,10),r=_,o=b;break;case Ga.Q:f=kn(r,o,v=t[c++],m=t[c++],g=t[c++],x=t[c++],10),r=g,o=x;break;case Ga.A:var w=t[c++],S=t[c++],M=t[c++],I=t[c++],T=t[c++],C=t[c++],D=C+T;c+=1;t[c++];d&&(a=Ka(T)*M+w,s=$a(T)*I+S),f=qa(M,I)*ja(ts,Math.abs(C)),r=Ka(D)*M+w,o=$a(D)*I+S;break;case Ga.R:a=r=t[c++],s=o=t[c++],f=2*t[c++]+2*t[c++];break;case Ga.Z:var A=a-r;y=s-o;f=Math.sqrt(A*A+y*y),r=a,o=s}f>=0&&(l[h++]=f,u+=f)}return this._pathLen=u,u},t.prototype.rebuildPath=function(t,e){var n,i,r,o,a,s,l,u,h,c,p=this.data,d=this._ux,f=this._uy,g=this._len,y=e<1,v=0,m=0,x=0;if(!y||(this._pathSegLen||this._calculateLength(),l=this._pathSegLen,u=e*this._pathLen))t:for(var _=0;_0&&(t.lineTo(h,c),x=0),b){case Ga.M:n=r=p[_++],i=o=p[_++],t.moveTo(r,o);break;case Ga.L:a=p[_++],s=p[_++];var S=Ja(a-r),M=Ja(s-o);if(S>d||M>f){if(y){if(v+(j=l[m++])>u){var I=(u-v)/j;t.lineTo(r*(1-I)+a*I,o*(1-I)+s*I);break t}v+=j}t.lineTo(a,s),r=a,o=s,x=0}else{var T=S*S+M*M;T>x&&(h=a,c=s,x=T)}break;case Ga.C:var C=p[_++],D=p[_++],A=p[_++],k=p[_++],L=p[_++],P=p[_++];if(y){if(v+(j=l[m++])>u){wn(r,C,A,L,I=(u-v)/j,Wa),wn(o,D,k,P,I,Ha),t.bezierCurveTo(Wa[1],Ha[1],Wa[2],Ha[2],Wa[3],Ha[3]);break t}v+=j}t.bezierCurveTo(C,D,A,k,L,P),r=L,o=P;break;case Ga.Q:C=p[_++],D=p[_++],A=p[_++],k=p[_++];if(y){if(v+(j=l[m++])>u){Dn(r,C,A,I=(u-v)/j,Wa),Dn(o,D,k,I,Ha),t.quadraticCurveTo(Wa[1],Ha[1],Wa[2],Ha[2]);break t}v+=j}t.quadraticCurveTo(C,D,A,k),r=A,o=k;break;case Ga.A:var O=p[_++],R=p[_++],N=p[_++],E=p[_++],z=p[_++],V=p[_++],B=p[_++],F=!p[_++],G=N>E?N:E,W=Ja(N-E)>.001,H=z+V,Y=!1;if(y)v+(j=l[m++])>u&&(H=z+V*(u-v)/j,Y=!0),v+=j;if(W&&t.ellipse?t.ellipse(O,R,N,E,B,z,H,F):t.arc(O,R,G,z,H,F),Y)break t;w&&(n=Ka(z)*N+O,i=$a(z)*E+R),r=Ka(H)*N+O,o=$a(H)*E+R;break;case Ga.R:n=r=p[_],i=o=p[_+1],a=p[_++],s=p[_++];var X=p[_++],U=p[_++];if(y){if(v+(j=l[m++])>u){var Z=u-v;t.moveTo(a,s),t.lineTo(a+ja(Z,X),s),(Z-=X)>0&&t.lineTo(a+X,s+ja(Z,U)),(Z-=U)>0&&t.lineTo(a+qa(X-Z,0),s+U),(Z-=X)>0&&t.lineTo(a,s+qa(U-Z,0));break t}v+=j}t.rect(a,s,X,U);break;case Ga.Z:if(y){var j;if(v+(j=l[m++])>u){I=(u-v)/j;t.lineTo(r*(1-I)+n*I,o*(1-I)+i*I);break t}v+=j}t.closePath(),r=n,o=i}}},t.prototype.clone=function(){var e=new t,n=this.data;return e.data=n.slice?n.slice():Array.prototype.slice.call(n),e._len=this._len,e},t.CMD=Ga,t.initDefaultProps=function(){var e=t.prototype;e._saveData=!0,e._ux=0,e._uy=0,e._pendingPtDist=0,e._version=0}(),t}();function as(t,e,n,i,r,o,a){if(0===r)return!1;var s=r,l=0;if(a>e+s&&a>i+s||at+s&&o>n+s||oe+c&&h>i+c&&h>o+c&&h>s+c||ht+c&&u>n+c&&u>r+c&&u>a+c||ue+u&&l>i+u&&l>o+u||lt+u&&s>n+u&&s>r+u||sn||h+ur&&(r+=cs);var p=Math.atan2(l,s);return p<0&&(p+=cs),p>=i&&p<=r||p+cs>=i&&p+cs<=r}function ds(t,e,n,i,r,o){if(o>e&&o>i||or?s:0}var fs=os.CMD,gs=2*Math.PI;var ys=[-1,-1,-1],vs=[-1,-1];function ms(t,e,n,i,r,o,a,s,l,u){if(u>e&&u>i&&u>o&&u>s||u1&&(h=void 0,h=vs[0],vs[0]=vs[1],vs[1]=h),f=mn(e,i,o,s,vs[0]),d>1&&(g=mn(e,i,o,s,vs[1]))),2===d?ve&&s>i&&s>o||s=0&&h<=1&&(r[l++]=h);else{var u=a*a-4*o*s;if(yn(u))(h=-a/(2*o))>=0&&h<=1&&(r[l++]=h);else if(u>0){var h,c=ln(u),p=(-a-c)/(2*o);(h=(-a+c)/(2*o))>=0&&h<=1&&(r[l++]=h),p>=0&&p<=1&&(r[l++]=p)}}return l}(e,i,o,s,ys);if(0===l)return 0;var u=Cn(e,i,o);if(u>=0&&u<=1){for(var h=0,c=In(e,i,o,u),p=0;pn||s<-n)return 0;var l=Math.sqrt(n*n-s*s);ys[0]=-l,ys[1]=l;var u=Math.abs(i-r);if(u<1e-4)return 0;if(u>=gs-1e-4){i=0,r=gs;var h=o?1:-1;return a>=ys[0]+t&&a<=ys[1]+t?h:0}if(i>r){var c=i;i=r,r=c}i<0&&(i+=gs,r+=gs);for(var p=0,d=0;d<2;d++){var f=ys[d];if(f+t>a){var g=Math.atan2(s,f);h=o?1:-1;g<0&&(g=gs+g),(g>=i&&g<=r||g+gs>=i&&g+gs<=r)&&(g>Math.PI/2&&g<1.5*Math.PI&&(h=-h),p+=h)}}return p}function bs(t,e,n,i,r){for(var o,a,s,l,u=t.data,h=t.len(),c=0,p=0,d=0,f=0,g=0,y=0;y1&&(n||(c+=ds(p,d,f,g,i,r))),m&&(f=p=u[y],g=d=u[y+1]),v){case fs.M:p=f=u[y++],d=g=u[y++];break;case fs.L:if(n){if(as(p,d,u[y],u[y+1],e,i,r))return!0}else c+=ds(p,d,u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case fs.C:if(n){if(ss(p,d,u[y++],u[y++],u[y++],u[y++],u[y],u[y+1],e,i,r))return!0}else c+=ms(p,d,u[y++],u[y++],u[y++],u[y++],u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case fs.Q:if(n){if(ls(p,d,u[y++],u[y++],u[y],u[y+1],e,i,r))return!0}else c+=xs(p,d,u[y++],u[y++],u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case fs.A:var x=u[y++],_=u[y++],b=u[y++],w=u[y++],S=u[y++],M=u[y++];y+=1;var I=!!(1-u[y++]);o=Math.cos(S)*b+x,a=Math.sin(S)*w+_,m?(f=o,g=a):c+=ds(p,d,o,a,i,r);var T=(i-x)*w/b+x;if(n){if(ps(x,_,w,S,S+M,I,e,T,r))return!0}else c+=_s(x,_,w,S,S+M,I,T,r);p=Math.cos(S+M)*b+x,d=Math.sin(S+M)*w+_;break;case fs.R:if(f=p=u[y++],g=d=u[y++],o=f+u[y++],a=g+u[y++],n){if(as(f,g,o,g,e,i,r)||as(o,g,o,a,e,i,r)||as(o,a,f,a,e,i,r)||as(f,a,f,g,e,i,r))return!0}else c+=ds(o,g,o,a,i,r),c+=ds(f,a,f,g,i,r);break;case fs.Z:if(n){if(as(p,d,f,g,e,i,r))return!0}else c+=ds(p,d,f,g,i,r);p=f,d=g}}return n||(s=d,l=g,Math.abs(s-l)<1e-4)||(c+=ds(p,d,f,g,i,r)||0),0!==c}var ws=k({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},xa),Ss={style:k({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},_a.style)},Ms=yr.concat(["invisible","culling","z","z2","zlevel","parent"]),Is=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype.update=function(){var n=this;t.prototype.update.call(this);var i=this.style;if(i.decal){var r=this._decalEl=this._decalEl||new e;r.buildPath===e.prototype.buildPath&&(r.buildPath=function(t){n.buildPath(t,n.shape)}),r.silent=!0;var o=r.style;for(var a in i)o[a]!==i[a]&&(o[a]=i[a]);o.fill=i.fill?i.decal:null,o.decal=null,o.shadowColor=null,i.strokeFirst&&(o.stroke=null);for(var s=0;s.5?ar:e>.2?"#eee":sr}if(t)return sr}return ar},e.prototype.getInsideTextStroke=function(t){var e=this.style.fill;if(U(e)){var n=this.__zr;if(!(!n||!n.isDarkMode())===oi(t,0)<.4)return e}},e.prototype.buildPath=function(t,e,n){},e.prototype.pathUpdated=function(){this.__dirty&=-5},e.prototype.getUpdatedPathProxy=function(t){return!this.path&&this.createPathProxy(),this.path.beginPath(),this.buildPath(this.path,this.shape,t),this.path},e.prototype.createPathProxy=function(){this.path=new os(!1)},e.prototype.hasStroke=function(){var t=this.style,e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.getBoundingRect=function(){var t=this._rect,e=this.style,n=!t;if(n){var i=!1;this.path||(i=!0,this.createPathProxy());var r=this.path;(i||4&this.__dirty)&&(r.beginPath(),this.buildPath(r,this.shape,!1),this.pathUpdated()),t=r.getBoundingRect()}if(this._rect=t,this.hasStroke()&&this.path&&this.path.len()>0){var o=this._rectStroke||(this._rectStroke=t.clone());if(this.__dirty||n){o.copy(t);var a=e.strokeNoScale?this.getLineScale():1,s=e.lineWidth;if(!this.hasFill()){var l=this.strokeContainThreshold;s=Math.max(s,null==l?4:l)}a>1e-10&&(o.width+=s/a,o.height+=s/a,o.x-=s/a/2,o.y-=s/a/2)}return o}return t},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect(),r=this.style;if(t=n[0],e=n[1],i.contain(t,e)){var o=this.path;if(this.hasStroke()){var a=r.lineWidth,s=r.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(this.hasFill()||(a=Math.max(a,this.strokeContainThreshold)),function(t,e,n,i){return bs(t,e,!0,n,i)}(o,a/s,t,e)))return!0}if(this.hasFill())return function(t,e,n){return bs(t,0,!1,e,n)}(o,t,e)}return!1},e.prototype.dirtyShape=function(){this.__dirty|=4,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},e.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},e.prototype.animateShape=function(t){return this.animate("shape",t)},e.prototype.updateDuringAnimation=function(t){"style"===t?this.dirtyStyle():"shape"===t?this.dirtyShape():this.markRedraw()},e.prototype.attrKV=function(e,n){"shape"===e?this.setShape(n):t.prototype.attrKV.call(this,e,n)},e.prototype.setShape=function(t,e){var n=this.shape;return n||(n=this.shape={}),"string"==typeof t?n[t]=e:A(n,t),this.dirtyShape(),this},e.prototype.shapeChanged=function(){return!!(4&this.__dirty)},e.prototype.createStyle=function(t){return mt(ws,t)},e.prototype._innerSaveToNormal=function(e){t.prototype._innerSaveToNormal.call(this,e);var n=this._normalState;e.shape&&!n.shape&&(n.shape=A({},this.shape))},e.prototype._applyStateObj=function(e,n,i,r,o,a){t.prototype._applyStateObj.call(this,e,n,i,r,o,a);var s,l=!(n&&r);if(n&&n.shape?o?r?s=n.shape:(s=A({},i.shape),A(s,n.shape)):(s=A({},r?this.shape:i.shape),A(s,n.shape)):l&&(s=i.shape),s)if(o){this.shape=A({},this.shape);for(var u={},h=G(s),c=0;c0},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.createStyle=function(t){return mt(Ts,t)},e.prototype.setBoundingRect=function(t){this._rect=t},e.prototype.getBoundingRect=function(){var t=this.style;if(!this._rect){var e=t.text;null!=e?e+="":e="";var n=br(e,t.font,t.textAlign,t.textBaseline);if(n.x+=t.x||0,n.y+=t.y||0,this.hasStroke()){var i=t.lineWidth;n.x-=i/2,n.y-=i/2,n.width+=i,n.height+=i}this._rect=n}return this._rect},e.initDefaultProps=void(e.prototype.dirtyRectTolerance=10),e}(Sa);Cs.prototype.type="tspan";var Ds=k({x:0,y:0},xa),As={style:k({x:!0,y:!0,width:!0,height:!0,sx:!0,sy:!0,sWidth:!0,sHeight:!0},_a.style)};var ks=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.createStyle=function(t){return mt(Ds,t)},e.prototype._getSize=function(t){var e=this.style,n=e[t];if(null!=n)return n;var i,r=(i=e.image)&&"string"!=typeof i&&i.width&&i.height?e.image:this.__image;if(!r)return 0;var o="width"===t?"height":"width",a=e[o];return null==a?r[t]:r[t]/r[o]*a},e.prototype.getWidth=function(){return this._getSize("width")},e.prototype.getHeight=function(){return this._getSize("height")},e.prototype.getAnimationStyleProps=function(){return As},e.prototype.getBoundingRect=function(){var t=this.style;return this._rect||(this._rect=new ze(t.x||0,t.y||0,this.getWidth(),this.getHeight())),this._rect},e}(Sa);ks.prototype.type="image";var Ls=Math.round;function Ps(t,e,n){if(e){var i=e.x1,r=e.x2,o=e.y1,a=e.y2;t.x1=i,t.x2=r,t.y1=o,t.y2=a;var s=n&&n.lineWidth;return s?(Ls(2*i)===Ls(2*r)&&(t.x1=t.x2=Rs(i,s,!0)),Ls(2*o)===Ls(2*a)&&(t.y1=t.y2=Rs(o,s,!0)),t):t}}function Os(t,e,n){if(e){var i=e.x,r=e.y,o=e.width,a=e.height;t.x=i,t.y=r,t.width=o,t.height=a;var s=n&&n.lineWidth;return s?(t.x=Rs(i,s,!0),t.y=Rs(r,s,!0),t.width=Math.max(Rs(i+o,s,!1)-t.x,0===o?0:1),t.height=Math.max(Rs(r+a,s,!1)-t.y,0===a?0:1),t):t}}function Rs(t,e,n){if(!e)return t;var i=Ls(2*t);return(i+Ls(e))%2==0?i/2:(i+(n?1:-1))/2}var Ns=function(){this.x=0,this.y=0,this.width=0,this.height=0},Es={},zs=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Ns},e.prototype.buildPath=function(t,e){var n,i,r,o;if(this.subPixelOptimize){var a=Os(Es,e,this.style);n=a.x,i=a.y,r=a.width,o=a.height,a.r=e.r,e=a}else n=e.x,i=e.y,r=e.width,o=e.height;e.r?function(t,e){var n,i,r,o,a,s=e.x,l=e.y,u=e.width,h=e.height,c=e.r;u<0&&(s+=u,u=-u),h<0&&(l+=h,h=-h),"number"==typeof c?n=i=r=o=c:c instanceof Array?1===c.length?n=i=r=o=c[0]:2===c.length?(n=r=c[0],i=o=c[1]):3===c.length?(n=c[0],i=o=c[1],r=c[2]):(n=c[0],i=c[1],r=c[2],o=c[3]):n=i=r=o=0,n+i>u&&(n*=u/(a=n+i),i*=u/a),r+o>u&&(r*=u/(a=r+o),o*=u/a),i+r>h&&(i*=h/(a=i+r),r*=h/a),n+o>h&&(n*=h/(a=n+o),o*=h/a),t.moveTo(s+n,l),t.lineTo(s+u-i,l),0!==i&&t.arc(s+u-i,l+i,i,-Math.PI/2,0),t.lineTo(s+u,l+h-r),0!==r&&t.arc(s+u-r,l+h-r,r,0,Math.PI/2),t.lineTo(s+o,l+h),0!==o&&t.arc(s+o,l+h-o,o,Math.PI/2,Math.PI),t.lineTo(s,l+n),0!==n&&t.arc(s+n,l+n,n,Math.PI,1.5*Math.PI)}(t,e):t.rect(n,i,r,o)},e.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},e}(Is);zs.prototype.type="rect";var Vs={fill:"#000"},Bs={style:k({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},_a.style)},Fs=function(t){function e(e){var n=t.call(this)||this;return n.type="text",n._children=[],n._defaultStyle=Vs,n.attr(e),n}return n(e,t),e.prototype.childrenRef=function(){return this._children},e.prototype.update=function(){t.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var e=0;ed&&h){var f=Math.floor(d/l);n=n.slice(0,f)}if(t&&a&&null!=c)for(var g=la(c,o,e.ellipsis,{minChar:e.truncateMinChar,placeholder:e.placeholder}),y=0;y0,T=null!=t.width&&("truncate"===t.overflow||"break"===t.overflow||"breakAll"===t.overflow),C=i.calculatedLineHeight,D=0;Dl&&fa(n,t.substring(l,u),e,s),fa(n,i[2],e,s,i[1]),l=aa.lastIndex}lo){b>0?(m.tokens=m.tokens.slice(0,b),y(m,_,x),n.lines=n.lines.slice(0,v+1)):n.lines=n.lines.slice(0,v);break t}var C=w.width,D=null==C||"auto"===C;if("string"==typeof C&&"%"===C.charAt(C.length-1))P.percentWidth=C,h.push(P),P.contentWidth=xr(P.text,I);else{if(D){var A=w.backgroundColor,k=A&&A.image;k&&oa(k=na(k))&&(P.width=Math.max(P.width,k.width*T/k.height))}var L=f&&null!=r?r-_:null;null!=L&&L=0&&"right"===(C=x[T]).align;)this._placeToken(C,t,b,f,I,"right",y),w-=C.width,I-=C.width,T--;for(M+=(n-(M-d)-(g-I)-w)/2;S<=T;)C=x[S],this._placeToken(C,t,b,f,M+C.width/2,"center",y),M+=C.width,S++;f+=b}},e.prototype._placeToken=function(t,e,n,i,r,o,s){var l=e.rich[t.styleName]||{};l.text=t.text;var u=t.verticalAlign,h=i+n/2;"top"===u?h=i+t.height/2:"bottom"===u&&(h=i+n-t.height/2),!t.isLineHolder&&Js(l)&&this._renderBackground(l,e,"right"===o?r-t.width:"center"===o?r-t.width/2:r,h-t.height/2,t.width,t.height);var c=!!l.backgroundColor,p=t.textPadding;p&&(r=Ks(r,o,p),h-=t.height/2-p[0]-t.innerHeight/2);var d=this._getOrCreateChild(Cs),f=d.createStyle();d.useStyle(f);var g=this._defaultStyle,y=!1,v=0,m=qs("fill"in l?l.fill:"fill"in e?e.fill:(y=!0,g.fill)),x=js("stroke"in l?l.stroke:"stroke"in e?e.stroke:c||s||g.autoStroke&&!y?null:(v=2,g.stroke)),_=l.textShadowBlur>0||e.textShadowBlur>0;f.text=t.text,f.x=r,f.y=h,_&&(f.shadowBlur=l.textShadowBlur||e.textShadowBlur||0,f.shadowColor=l.textShadowColor||e.textShadowColor||"transparent",f.shadowOffsetX=l.textShadowOffsetX||e.textShadowOffsetX||0,f.shadowOffsetY=l.textShadowOffsetY||e.textShadowOffsetY||0),f.textAlign=o,f.textBaseline="middle",f.font=t.font||a,f.opacity=ot(l.opacity,e.opacity,1),Xs(f,l),x&&(f.lineWidth=ot(l.lineWidth,e.lineWidth,v),f.lineDash=rt(l.lineDash,e.lineDash),f.lineDashOffset=e.lineDashOffset||0,f.stroke=x),m&&(f.fill=m);var b=t.contentWidth,w=t.contentHeight;d.setBoundingRect(new ze(wr(f.x,b,f.textAlign),Sr(f.y,w,f.textBaseline),b,w))},e.prototype._renderBackground=function(t,e,n,i,r,o){var a,s,l,u=t.backgroundColor,h=t.borderWidth,c=t.borderColor,p=u&&u.image,d=u&&!p,f=t.borderRadius,g=this;if(d||t.lineHeight||h&&c){(a=this._getOrCreateChild(zs)).useStyle(a.createStyle()),a.style.fill=null;var y=a.shape;y.x=n,y.y=i,y.width=r,y.height=o,y.r=f,a.dirtyShape()}if(d)(l=a.style).fill=u||null,l.fillOpacity=rt(t.fillOpacity,1);else if(p){(s=this._getOrCreateChild(ks)).onload=function(){g.dirtyStyle()};var v=s.style;v.image=u.image,v.x=n,v.y=i,v.width=r,v.height=o}h&&c&&((l=a.style).lineWidth=h,l.stroke=c,l.strokeOpacity=rt(t.strokeOpacity,1),l.lineDash=t.borderDash,l.lineDashOffset=t.borderDashOffset||0,a.strokeContainThreshold=0,a.hasFill()&&a.hasStroke()&&(l.strokeFirst=!0,l.lineWidth*=2));var m=(a||s).style;m.shadowBlur=t.shadowBlur||0,m.shadowColor=t.shadowColor||"transparent",m.shadowOffsetX=t.shadowOffsetX||0,m.shadowOffsetY=t.shadowOffsetY||0,m.opacity=ot(t.opacity,e.opacity,1)},e.makeFont=function(t){var e="";return Us(t)&&(e=[t.fontStyle,t.fontWeight,Ys(t.fontSize),t.fontFamily||"sans-serif"].join(" ")),e&&ut(e)||t.textFont||t.font},e}(Sa),Gs={left:!0,right:1,center:1},Ws={top:1,bottom:1,middle:1},Hs=["fontStyle","fontWeight","fontSize","fontFamily"];function Ys(t){return"string"!=typeof t||-1===t.indexOf("px")&&-1===t.indexOf("rem")&&-1===t.indexOf("em")?isNaN(+t)?"12px":t+"px":t}function Xs(t,e){for(var n=0;n=0,o=!1;if(t instanceof Is){var a=il(t),s=r&&a.selectFill||a.normalFill,l=r&&a.selectStroke||a.normalStroke;if(dl(s)||dl(l)){var u=(i=i||{}).style||{};"inherit"===u.fill?(o=!0,i=A({},i),(u=A({},u)).fill=s):!dl(u.fill)&&dl(s)?(o=!0,i=A({},i),(u=A({},u)).fill=gl(s)):!dl(u.stroke)&&dl(l)&&(o||(i=A({},i),u=A({},u)),u.stroke=gl(l)),i.style=u}}if(i&&null==i.z2){o||(i=A({},i));var h=t.z2EmphasisLift;i.z2=t.z2+(null!=h?h:sl)}return i}(this,0,e,n);if("blur"===t)return function(t,e,n){var i=P(t.currentStates,e)>=0,r=t.style.opacity,o=i?null:function(t,e,n,i){for(var r=t.style,o={},a=0;a0){var o={dataIndex:r,seriesIndex:t.seriesIndex};null!=i&&(o.dataType=i),e.push(o)}}))})),e}function Hl(t,e,n){ql(t,!0),Ml(t,Cl),Xl(t,e,n)}function Yl(t,e,n,i){i?function(t){ql(t,!1)}(t):Hl(t,e,n)}function Xl(t,e,n){var i=Qs(t);null!=e?(i.focus=e,i.blurScope=n):i.focus&&(i.focus=null)}var Ul=["emphasis","blur","select"],Zl={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function jl(t,e,n,i){n=n||"itemStyle";for(var r=0;r1&&(a*=ru(f),s*=ru(f));var g=(r===o?-1:1)*ru((a*a*(s*s)-a*a*(d*d)-s*s*(p*p))/(a*a*(d*d)+s*s*(p*p)))||0,y=g*a*d/s,v=g*-s*p/a,m=(t+n)/2+au(c)*y-ou(c)*v,x=(e+i)/2+ou(c)*y+au(c)*v,_=hu([1,0],[(p-y)/a,(d-v)/s]),b=[(p-y)/a,(d-v)/s],w=[(-1*p-y)/a,(-1*d-v)/s],S=hu(b,w);if(uu(b,w)<=-1&&(S=su),uu(b,w)>=1&&(S=0),S<0){var M=Math.round(S/su*1e6)/1e6;S=2*su+M%2*su}h.addData(u,m,x,a,s,_,S,c,o)}var pu=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/gi,du=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;var fu=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.applyTransform=function(t){},e}(Is);function gu(t){return null!=t.setData}function yu(t,e){var n=function(t){var e=new os;if(!t)return e;var n,i=0,r=0,o=i,a=r,s=os.CMD,l=t.match(pu);if(!l)return e;for(var u=0;uk*k+L*L&&(M=T,I=C),{cx:M,cy:I,x0:-h,y0:-c,x1:M*(r/b-1),y1:I*(r/b-1)}}function Nu(t,e){var n,i=Lu(e.r,0),r=Lu(e.r0||0,0),o=i>0;if(o||r>0){if(o||(i=r,r=0),r>i){var a=i;i=r,r=a}var s=e.startAngle,l=e.endAngle;if(!isNaN(s)&&!isNaN(l)){var u=e.cx,h=e.cy,c=!!e.clockwise,p=Au(l-s),d=p>Mu&&p%Mu;if(d>Ou&&(p=d),i>Ou)if(p>Mu-Ou)t.moveTo(u+i*Tu(s),h+i*Iu(s)),t.arc(u,h,i,s,l,!c),r>Ou&&(t.moveTo(u+r*Tu(l),h+r*Iu(l)),t.arc(u,h,r,l,s,c));else{var f=void 0,g=void 0,y=void 0,v=void 0,m=void 0,x=void 0,_=void 0,b=void 0,w=void 0,S=void 0,M=void 0,I=void 0,T=void 0,C=void 0,D=void 0,A=void 0,k=i*Tu(s),L=i*Iu(s),P=r*Tu(l),O=r*Iu(l),R=p>Ou;if(R){var N=e.cornerRadius;N&&(n=function(t){var e;if(Y(t)){var n=t.length;if(!n)return t;e=1===n?[t[0],t[0],0,0]:2===n?[t[0],t[0],t[1],t[1]]:3===n?t.concat(t[2]):t}else e=[t,t,t,t];return e}(N),f=n[0],g=n[1],y=n[2],v=n[3]);var E=Au(i-r)/2;if(m=Pu(E,y),x=Pu(E,v),_=Pu(E,f),b=Pu(E,g),M=w=Lu(m,x),I=S=Lu(_,b),(w>Ou||S>Ou)&&(T=i*Tu(l),C=i*Iu(l),D=r*Tu(s),A=r*Iu(s),pOu){var X=Pu(y,M),U=Pu(v,M),Z=Ru(D,A,k,L,i,X,c),j=Ru(T,C,P,O,i,U,c);t.moveTo(u+Z.cx+Z.x0,h+Z.cy+Z.y0),M0&&t.arc(u+Z.cx,h+Z.cy,X,Du(Z.y0,Z.x0),Du(Z.y1,Z.x1),!c),t.arc(u,h,i,Du(Z.cy+Z.y1,Z.cx+Z.x1),Du(j.cy+j.y1,j.cx+j.x1),!c),U>0&&t.arc(u+j.cx,h+j.cy,U,Du(j.y1,j.x1),Du(j.y0,j.x0),!c))}else t.moveTo(u+k,h+L),t.arc(u,h,i,s,l,!c);else t.moveTo(u+k,h+L);if(r>Ou&&R)if(I>Ou){X=Pu(f,I),Z=Ru(P,O,T,C,r,-(U=Pu(g,I)),c),j=Ru(k,L,D,A,r,-X,c);t.lineTo(u+Z.cx+Z.x0,h+Z.cy+Z.y0),I0&&t.arc(u+Z.cx,h+Z.cy,U,Du(Z.y0,Z.x0),Du(Z.y1,Z.x1),!c),t.arc(u,h,r,Du(Z.cy+Z.y1,Z.cx+Z.x1),Du(j.cy+j.y1,j.cx+j.x1),c),X>0&&t.arc(u+j.cx,h+j.cy,X,Du(j.y1,j.x1),Du(j.y0,j.x0),!c))}else t.lineTo(u+P,h+O),t.arc(u,h,r,l,s,c);else t.lineTo(u+P,h+O)}else t.moveTo(u,h);t.closePath()}}}var Eu=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0,this.cornerRadius=0},zu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Eu},e.prototype.buildPath=function(t,e){Nu(t,e)},e.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},e}(Is);zu.prototype.type="sector";var Vu=function(){this.cx=0,this.cy=0,this.r=0,this.r0=0},Bu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Vu},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=2*Math.PI;t.moveTo(n+e.r,i),t.arc(n,i,e.r,0,r,!1),t.moveTo(n+e.r0,i),t.arc(n,i,e.r0,0,r,!0)},e}(Is);function Fu(t,e,n){var i=e.smooth,r=e.points;if(r&&r.length>=2){if(i){var o=function(t,e,n,i){var r,o,a,s,l=[],u=[],h=[],c=[];if(i){a=[1/0,1/0],s=[-1/0,-1/0];for(var p=0,d=t.length;poh[1]){if(a=!1,r)return a;var u=Math.abs(oh[0]-rh[1]),h=Math.abs(rh[0]-oh[1]);Math.min(u,h)>i.len()&&(u0){var c={duration:h.duration,delay:h.delay||0,easing:h.easing,done:o,force:!!o||!!a,setToFinal:!u,scope:t,during:a};l?e.animateFrom(n,c):e.animateTo(n,c)}else e.stopAnimation(),!l&&e.attr(n),a&&a(1),o&&o()}function fh(t,e,n,i,r,o){dh("update",t,e,n,i,r,o)}function gh(t,e,n,i,r,o){dh("enter",t,e,n,i,r,o)}function yh(t){if(!t.__zr)return!0;for(var e=0;eMath.abs(o[1])?o[0]>0?"right":"left":o[1]>0?"bottom":"top"}function Bh(t){return!t.isGroup}function Fh(t,e,n){if(t&&e){var i,r=(i={},t.traverse((function(t){Bh(t)&&t.anid&&(i[t.anid]=t)})),i);e.traverse((function(t){if(Bh(t)&&t.anid){var e=r[t.anid];if(e){var i=o(t);t.attr(o(e)),fh(t,i,n,Qs(t).dataIndex)}}}))}function o(t){var e={x:t.x,y:t.y,rotation:t.rotation};return function(t){return null!=t.shape}(t)&&(e.shape=A({},t.shape)),e}}function Gh(t,e){return z(t,(function(t){var n=t[0];n=bh(n,e.x),n=wh(n,e.x+e.width);var i=t[1];return i=bh(i,e.y),[n,i=wh(i,e.y+e.height)]}))}function Wh(t,e){var n=bh(t.x,e.x),i=wh(t.x+t.width,e.x+e.width),r=bh(t.y,e.y),o=wh(t.y+t.height,e.y+e.height);if(i>=n&&o>=r)return{x:n,y:r,width:i-n,height:o-r}}function Hh(t,e,n){var i=A({rectHover:!0},e),r=i.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},t)return 0===t.indexOf("image://")?(r.image=t.slice(8),k(r,n),new ks(i)):Ah(t.replace("path://",""),i,n,"center")}function Yh(t,e,n,i,r){for(var o=0,a=r[r.length-1];o=-1e-6)return!1;var f=t-r,g=e-o,y=Uh(f,g,u,h)/d;if(y<0||y>1)return!1;var v=Uh(f,g,c,p)/d;return!(v<0||v>1)}function Uh(t,e,n,i){return t*i-n*e}function Zh(t){var e=t.itemTooltipOption,n=t.componentModel,i=t.itemName,r=U(e)?{formatter:e}:e,o=n.mainType,a=n.componentIndex,s={componentType:o,name:i,$vars:["name"]};s[o+"Index"]=a;var l=t.formatterParamsExtra;l&&E(G(l),(function(t){_t(s,t)||(s[t]=l[t],s.$vars.push(t))}));var u=Qs(t.el);u.componentMainType=o,u.componentIndex=a,u.tooltipConfig={name:i,option:k({content:i,formatterParams:s},r)}}function jh(t,e){var n;t.isGroup&&(n=e(t)),n||t.traverse(e)}function qh(t,e){if(t)if(Y(t))for(var n=0;n-1?Dc:kc;function Rc(t,e){t=t.toUpperCase(),Pc[t]=new Mc(e),Lc[t]=e}function Nc(t){return Pc[t]}Rc(Ac,{time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}}),Rc(Dc,{time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}});var Ec=1e3,zc=6e4,Vc=36e5,Bc=864e5,Fc=31536e6,Gc={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}",none:"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}"},Wc="{yyyy}-{MM}-{dd}",Hc={year:"{yyyy}",month:"{yyyy}-{MM}",day:Wc,hour:Wc+" "+Gc.hour,minute:Wc+" "+Gc.minute,second:Wc+" "+Gc.second,millisecond:Gc.none},Yc=["year","month","day","hour","minute","second","millisecond"],Xc=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function Uc(t,e){return"0000".substr(0,e-(t+="").length)+t}function Zc(t){switch(t){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return t}}function jc(t){return t===Zc(t)}function qc(t,e,n,i){var r=ro(t),o=r[Jc(n)](),a=r[Qc(n)]()+1,s=Math.floor((a-1)/3)+1,l=r[tp(n)](),u=r["get"+(n?"UTC":"")+"Day"](),h=r[ep(n)](),c=(h-1)%12+1,p=r[np(n)](),d=r[ip(n)](),f=r[rp(n)](),g=(i instanceof Mc?i:Nc(i||Oc)||Pc[kc]).getModel("time"),y=g.get("month"),v=g.get("monthAbbr"),m=g.get("dayOfWeek"),x=g.get("dayOfWeekAbbr");return(e||"").replace(/{yyyy}/g,o+"").replace(/{yy}/g,Uc(o%100+"",2)).replace(/{Q}/g,s+"").replace(/{MMMM}/g,y[a-1]).replace(/{MMM}/g,v[a-1]).replace(/{MM}/g,Uc(a,2)).replace(/{M}/g,a+"").replace(/{dd}/g,Uc(l,2)).replace(/{d}/g,l+"").replace(/{eeee}/g,m[u]).replace(/{ee}/g,x[u]).replace(/{e}/g,u+"").replace(/{HH}/g,Uc(h,2)).replace(/{H}/g,h+"").replace(/{hh}/g,Uc(c+"",2)).replace(/{h}/g,c+"").replace(/{mm}/g,Uc(p,2)).replace(/{m}/g,p+"").replace(/{ss}/g,Uc(d,2)).replace(/{s}/g,d+"").replace(/{SSS}/g,Uc(f,3)).replace(/{S}/g,f+"")}function Kc(t,e){var n=ro(t),i=n[Qc(e)]()+1,r=n[tp(e)](),o=n[ep(e)](),a=n[np(e)](),s=n[ip(e)](),l=0===n[rp(e)](),u=l&&0===s,h=u&&0===a,c=h&&0===o,p=c&&1===r;return p&&1===i?"year":p?"month":c?"day":h?"hour":u?"minute":l?"second":"millisecond"}function $c(t,e,n){var i=j(t)?ro(t):t;switch(e=e||Kc(t,n)){case"year":return i[Jc(n)]();case"half-year":return i[Qc(n)]()>=6?1:0;case"quarter":return Math.floor((i[Qc(n)]()+1)/4);case"month":return i[Qc(n)]();case"day":return i[tp(n)]();case"half-day":return i[ep(n)]()/24;case"hour":return i[ep(n)]();case"minute":return i[np(n)]();case"second":return i[ip(n)]();case"millisecond":return i[rp(n)]()}}function Jc(t){return t?"getUTCFullYear":"getFullYear"}function Qc(t){return t?"getUTCMonth":"getMonth"}function tp(t){return t?"getUTCDate":"getDate"}function ep(t){return t?"getUTCHours":"getHours"}function np(t){return t?"getUTCMinutes":"getMinutes"}function ip(t){return t?"getUTCSeconds":"getSeconds"}function rp(t){return t?"getUTCMilliseconds":"getMilliseconds"}function op(t){return t?"setUTCFullYear":"setFullYear"}function ap(t){return t?"setUTCMonth":"setMonth"}function sp(t){return t?"setUTCDate":"setDate"}function lp(t){return t?"setUTCHours":"setHours"}function up(t){return t?"setUTCMinutes":"setMinutes"}function hp(t){return t?"setUTCSeconds":"setSeconds"}function cp(t){return t?"setUTCMilliseconds":"setMilliseconds"}function pp(t){if(!co(t))return U(t)?t:"-";var e=(t+"").split(".");return e[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(e.length>1?"."+e[1]:"")}function dp(t,e){return t=(t||"").toLowerCase().replace(/-(.)/g,(function(t,e){return e.toUpperCase()})),e&&t&&(t=t.charAt(0).toUpperCase()+t.slice(1)),t}var fp=st;function gp(t,e,n){function i(t){return t&&ut(t)?t:"-"}function r(t){return!(null==t||isNaN(t)||!isFinite(t))}var o="time"===e,a=t instanceof Date;if(o||a){var s=o?ro(t):t;if(!isNaN(+s))return qc(s,"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}",n);if(a)return"-"}if("ordinal"===e)return Z(t)?i(t):j(t)&&r(t)?t+"":"-";var l=ho(t);return r(l)?pp(l):Z(t)?i(t):"boolean"==typeof t?t+"":"-"}var yp=["a","b","c","d","e","f","g"],vp=function(t,e){return"{"+t+(null==e?"":e)+"}"};function mp(t,e,n){Y(e)||(e=[e]);var i=e.length;if(!i)return"";for(var r=e[0].$vars||[],o=0;o':'':{renderMode:o,content:"{"+(n.markerId||"markerX")+"|} ",style:"subItem"===r?{width:4,height:4,borderRadius:2,backgroundColor:i}:{width:10,height:10,borderRadius:5,backgroundColor:i}}:""}function _p(t,e){return e=e||"transparent",U(t)?t:q(t)&&t.colorStops&&(t.colorStops[0]||{}).color||e}function bp(t,e){if("_blank"===e||"blank"===e){var n=window.open();n.opener=null,n.location.href=t}else window.open(t,e)}var wp=E,Sp=["left","right","top","bottom","width","height"],Mp=[["width","left","right"],["height","top","bottom"]];function Ip(t,e,n,i,r){var o=0,a=0;null==i&&(i=1/0),null==r&&(r=1/0);var s=0;e.eachChild((function(l,u){var h,c,p=l.getBoundingRect(),d=e.childAt(u+1),f=d&&d.getBoundingRect();if("horizontal"===t){var g=p.width+(f?-f.x+p.x:0);(h=o+g)>i||l.newline?(o=0,h=g,a+=s+n,s=p.height):s=Math.max(s,p.height)}else{var y=p.height+(f?-f.y+p.y:0);(c=a+y)>r||l.newline?(o+=s+n,a=0,c=y,s=p.width):s=Math.max(s,p.width)}l.newline||(l.x=o,l.y=a,l.markRedraw(),"horizontal"===t?o=h+n:a=c+n)}))}var Tp=Ip;H(Ip,"vertical"),H(Ip,"horizontal");function Cp(t,e,n){n=fp(n||0);var i=e.width,r=e.height,o=Ur(t.left,i),a=Ur(t.top,r),s=Ur(t.right,i),l=Ur(t.bottom,r),u=Ur(t.width,i),h=Ur(t.height,r),c=n[2]+n[0],p=n[1]+n[3],d=t.aspect;switch(isNaN(u)&&(u=i-s-p-o),isNaN(h)&&(h=r-l-c-a),null!=d&&(isNaN(u)&&isNaN(h)&&(d>i/r?u=.8*i:h=.8*r),isNaN(u)&&(u=d*h),isNaN(h)&&(h=u/d)),isNaN(o)&&(o=i-s-u-p),isNaN(a)&&(a=r-l-h-c),t.left||t.right){case"center":o=i/2-u/2-n[3];break;case"right":o=i-u-p}switch(t.top||t.bottom){case"middle":case"center":a=r/2-h/2-n[0];break;case"bottom":a=r-h-c}o=o||0,a=a||0,isNaN(u)&&(u=i-p-o-(s||0)),isNaN(h)&&(h=r-c-a-(l||0));var f=new ze(o+n[3],a+n[0],u,h);return f.margin=n,f}function Dp(t,e,n,i,r,o){var a,s=!r||!r.hv||r.hv[0],l=!r||!r.hv||r.hv[1],u=r&&r.boundingMode||"all";if((o=o||t).x=t.x,o.y=t.y,!s&&!l)return!1;if("raw"===u)a="group"===t.type?new ze(0,0,+e.width||0,+e.height||0):t.getBoundingRect();else if(a=t.getBoundingRect(),t.needLocalTransform()){var h=t.getLocalTransform();(a=a.clone()).applyTransform(h)}var c=Cp(k({width:a.width,height:a.height},e),n,i),p=s?c.x-a.x:0,d=l?c.y-a.y:0;return"raw"===u?(o.x=p,o.y=d):(o.x+=p,o.y+=d),o===t&&t.markRedraw(),!0}function Ap(t){var e=t.layoutMode||t.constructor.layoutMode;return q(e)?e:e?{type:e}:null}function kp(t,e,n){var i=n&&n.ignoreSize;!Y(i)&&(i=[i,i]);var r=a(Mp[0],0),o=a(Mp[1],1);function a(n,r){var o={},a=0,u={},h=0;if(wp(n,(function(e){u[e]=t[e]})),wp(n,(function(t){s(e,t)&&(o[t]=u[t]=e[t]),l(o,t)&&a++,l(u,t)&&h++})),i[r])return l(e,n[1])?u[n[2]]=null:l(e,n[2])&&(u[n[1]]=null),u;if(2!==h&&a){if(a>=2)return o;for(var c=0;c=0;a--)o=C(o,n[a],!0);e.defaultOption=o}return e.defaultOption},e.prototype.getReferringComponents=function(t,e){var n=t+"Index",i=t+"Id";return Bo(this.ecModel,t,{index:this.get(n,!0),id:this.get(i,!0)},e)},e.prototype.getBoxLayoutParams=function(){var t=this;return{left:t.get("left"),top:t.get("top"),right:t.get("right"),bottom:t.get("bottom"),width:t.get("width"),height:t.get("height")}},e.prototype.getZLevelKey=function(){return""},e.prototype.setZLevel=function(t){this.option.zlevel=t},e.protoInitialize=function(){var t=e.prototype;t.type="component",t.id="",t.name="",t.mainType="",t.subType="",t.componentIndex=0}(),e}(Mc);Zo(Rp,Mc),$o(Rp),function(t){var e={};t.registerSubTypeDefaulter=function(t,n){var i=Xo(t);e[i.main]=n},t.determineSubType=function(n,i){var r=i.type;if(!r){var o=Xo(n).main;t.hasSubTypes(n)&&e[o]&&(r=e[o](i))}return r}}(Rp),function(t,e){function n(t,e){return t[e]||(t[e]={predecessor:[],successor:[]}),t[e]}t.topologicalTravel=function(t,i,r,o){if(t.length){var a=function(t){var i={},r=[];return E(t,(function(o){var a=n(i,o),s=function(t,e){var n=[];return E(t,(function(t){P(e,t)>=0&&n.push(t)})),n}(a.originalDeps=e(o),t);a.entryCount=s.length,0===a.entryCount&&r.push(o),E(s,(function(t){P(a.predecessor,t)<0&&a.predecessor.push(t);var e=n(i,t);P(e.successor,t)<0&&e.successor.push(o)}))})),{graph:i,noEntryList:r}}(i),s=a.graph,l=a.noEntryList,u={};for(E(t,(function(t){u[t]=!0}));l.length;){var h=l.pop(),c=s[h],p=!!u[h];p&&(r.call(o,h,c.originalDeps.slice()),delete u[h]),E(c.successor,p?f:d)}E(u,(function(){var t="";throw new Error(t)}))}function d(t){s[t].entryCount--,0===s[t].entryCount&&l.push(t)}function f(t){u[t]=!0,d(t)}}}(Rp,(function(t){var e=[];E(Rp.getClassesByMainType(t),(function(t){e=e.concat(t.dependencies||t.prototype.dependencies||[])})),e=z(e,(function(t){return Xo(t).main})),"dataset"!==t&&P(e,"dataset")<=0&&e.unshift("dataset");return e}));var Np="";"undefined"!=typeof navigator&&(Np=navigator.platform||"");var Ep="rgba(0, 0, 0, 0.2)",zp={darkMode:"auto",colorBy:"series",color:["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],gradientColor:["#f6efa6","#d88273","#bf444c"],aria:{decal:{decals:[{color:Ep,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:Ep,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:Ep,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:Ep,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:Ep,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:Ep,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:Np.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1},Vp=yt(["tooltip","label","itemName","itemId","itemGroupId","seriesName"]),Bp="original",Fp="arrayRows",Gp="objectRows",Wp="keyedColumns",Hp="typedArray",Yp="unknown",Xp="column",Up="row",Zp=1,jp=2,qp=3,Kp=Oo();function $p(t,e,n){var i={},r=Qp(e);if(!r||!t)return i;var o,a,s=[],l=[],u=e.ecModel,h=Kp(u).datasetMap,c=r.uid+"_"+n.seriesLayoutBy;E(t=t.slice(),(function(e,n){var r=q(e)?e:t[n]={name:e};"ordinal"===r.type&&null==o&&(o=n,a=f(r)),i[r.name]=[]}));var p=h.get(c)||h.set(c,{categoryWayDim:a,valueWayDim:0});function d(t,e,n){for(var i=0;ie)return t[i];return t[n-1]}(i,a):n;if((h=h||n)&&h.length){var c=h[l];return r&&(u[r]=c),s.paletteIdx=(l+1)%h.length,c}}var cd="\0_ec_inner";var pd=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(t,e,n,i,r,o){i=i||{},this.option=null,this._theme=new Mc(i),this._locale=new Mc(r),this._optionManager=o},e.prototype.setOption=function(t,e,n){var i=gd(e);this._optionManager.setOption(t,n,i),this._resetOption(null,i)},e.prototype.resetOption=function(t,e){return this._resetOption(t,gd(e))},e.prototype._resetOption=function(t,e){var n=!1,i=this._optionManager;if(!t||"recreate"===t){var r=i.mountOption("recreate"===t);0,this.option&&"recreate"!==t?(this.restoreData(),this._mergeOption(r,e)):od(this,r),n=!0}if("timeline"!==t&&"media"!==t||this.restoreData(),!t||"recreate"===t||"timeline"===t){var o=i.getTimelineOption(this);o&&(n=!0,this._mergeOption(o,e))}if(!t||"recreate"===t||"media"===t){var a=i.getMediaOption(this);a.length&&E(a,(function(t){n=!0,this._mergeOption(t,e)}),this)}return n},e.prototype.mergeOption=function(t){this._mergeOption(t,null)},e.prototype._mergeOption=function(t,e){var n=this.option,i=this._componentsMap,r=this._componentsCount,o=[],a=yt(),s=e&&e.replaceMergeMainTypeMap;Kp(this).datasetMap=yt(),E(t,(function(t,e){null!=t&&(Rp.hasClass(e)?e&&(o.push(e),a.set(e,!0)):n[e]=null==n[e]?T(t):C(n[e],t,!0))})),s&&s.each((function(t,e){Rp.hasClass(e)&&!a.get(e)&&(o.push(e),a.set(e,!0))})),Rp.topologicalTravel(o,Rp.getAllClassMainTypes(),(function(e){var o=function(t,e,n){var i=nd.get(e);if(!i)return n;var r=i(t);return r?n.concat(r):n}(this,e,bo(t[e])),a=i.get(e),l=a?s&&s.get(e)?"replaceMerge":"normalMerge":"replaceAll",u=To(a,o,l);(function(t,e,n){E(t,(function(t){var i=t.newOption;q(i)&&(t.keyInfo.mainType=e,t.keyInfo.subType=function(t,e,n,i){return e.type?e.type:n?n.subType:i.determineSubType(t,e)}(e,i,t.existing,n))}))})(u,e,Rp),n[e]=null,i.set(e,null),r.set(e,0);var h,c=[],p=[],d=0;E(u,(function(t,n){var i=t.existing,r=t.newOption;if(r){var o="series"===e,a=Rp.getClass(e,t.keyInfo.subType,!o);if(!a)return;if("tooltip"===e){if(h)return void 0;h=!0}if(i&&i.constructor===a)i.name=t.keyInfo.name,i.mergeOption(r,this),i.optionUpdated(r,!1);else{var s=A({componentIndex:n},t.keyInfo);A(i=new a(r,this,this,s),s),t.brandNew&&(i.__requireNewView=!0),i.init(r,this,this),i.optionUpdated(null,!0)}}else i&&(i.mergeOption({},this),i.optionUpdated({},!1));i?(c.push(i.option),p.push(i),d++):(c.push(void 0),p.push(void 0))}),this),n[e]=c,i.set(e,p),r.set(e,d),"series"===e&&id(this)}),this),this._seriesIndices||id(this)},e.prototype.getOption=function(){var t=T(this.option);return E(t,(function(e,n){if(Rp.hasClass(n)){for(var i=bo(e),r=i.length,o=!1,a=r-1;a>=0;a--)i[a]&&!Lo(i[a])?o=!0:(i[a]=null,!o&&r--);i.length=r,t[n]=i}})),delete t[cd],t},e.prototype.getTheme=function(){return this._theme},e.prototype.getLocaleModel=function(){return this._locale},e.prototype.setUpdatePayload=function(t){this._payload=t},e.prototype.getUpdatePayload=function(){return this._payload},e.prototype.getComponent=function(t,e){var n=this._componentsMap.get(t);if(n){var i=n[e||0];if(i)return i;if(null==e)for(var r=0;r=e:"max"===n?t<=e:t===e})(i[a],t,o)||(r=!1)}})),r}var Sd=E,Md=q,Id=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function Td(t){var e=t&&t.itemStyle;if(e)for(var n=0,i=Id.length;n=0;g--){var y=t[g];if(s||(p=y.data.rawIndexOf(y.stackedByDimension,c)),p>=0){var v=y.data.getByRawIndex(y.stackResultDimension,p);if("all"===l||"positive"===l&&v>0||"negative"===l&&v<0||"samesign"===l&&d>=0&&v>0||"samesign"===l&&d<=0&&v<0){d=Qr(d,v),f=v;break}}}return i[0]=d,i[1]=f,i}))}))}var Yd,Xd,Ud,Zd,jd,qd=function(t){this.data=t.data||(t.sourceFormat===Wp?{}:[]),this.sourceFormat=t.sourceFormat||Yp,this.seriesLayoutBy=t.seriesLayoutBy||Xp,this.startIndex=t.startIndex||0,this.dimensionsDetectedCount=t.dimensionsDetectedCount,this.metaRawOption=t.metaRawOption;var e=this.dimensionsDefine=t.dimensionsDefine;if(e)for(var n=0;nu&&(u=d)}s[0]=l,s[1]=u}},i=function(){return this._data?this._data.length/this._dimSize:0};function r(t){for(var e=0;e=0&&(s=o.interpolatedValue[l])}return null!=s?s+"":""})):void 0},t.prototype.getRawValue=function(t,e){return gf(this.getData(e),t)},t.prototype.formatTooltip=function(t,e,n){},t}();function mf(t){var e,n;return q(t)?t.type&&(n=t):e=t,{text:e,frag:n}}function xf(t){return new _f(t)}var _f=function(){function t(t){t=t||{},this._reset=t.reset,this._plan=t.plan,this._count=t.count,this._onDirty=t.onDirty,this._dirty=!0}return t.prototype.perform=function(t){var e,n=this._upstream,i=t&&t.skip;if(this._dirty&&n){var r=this.context;r.data=r.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this),this._plan&&!i&&(e=this._plan(this.context));var o,a=h(this._modBy),s=this._modDataCount||0,l=h(t&&t.modBy),u=t&&t.modDataCount||0;function h(t){return!(t>=1)&&(t=1),t}a===l&&s===u||(e="reset"),(this._dirty||"reset"===e)&&(this._dirty=!1,o=this._doReset(i)),this._modBy=l,this._modDataCount=u;var c=t&&t.step;if(this._dueEnd=n?n._outputDueEnd:this._count?this._count(this.context):1/0,this._progress){var p=this._dueIndex,d=Math.min(null!=c?this._dueIndex+c:1/0,this._dueEnd);if(!i&&(o||p1&&i>0?s:a}};return o;function a(){return e=t?null:oe},gte:function(t,e){return t>=e}},Tf=function(){function t(t,e){if(!j(e)){var n="";0,vo(n)}this._opFn=If[t],this._rvalFloat=ho(e)}return t.prototype.evaluate=function(t){return j(t)?this._opFn(t,this._rvalFloat):this._opFn(ho(t),this._rvalFloat)},t}(),Cf=function(){function t(t,e){var n="desc"===t;this._resultLT=n?1:-1,null==e&&(e=n?"min":"max"),this._incomparable="min"===e?-1/0:1/0}return t.prototype.evaluate=function(t,e){var n=j(t)?t:ho(t),i=j(e)?e:ho(e),r=isNaN(n),o=isNaN(i);if(r&&(n=this._incomparable),o&&(i=this._incomparable),r&&o){var a=U(t),s=U(e);a&&(n=s?t:0),s&&(i=a?e:0)}return ni?-this._resultLT:0},t}(),Df=function(){function t(t,e){this._rval=e,this._isEQ=t,this._rvalTypeof=typeof e,this._rvalFloat=ho(e)}return t.prototype.evaluate=function(t){var e=t===this._rval;if(!e){var n=typeof t;n===this._rvalTypeof||"number"!==n&&"number"!==this._rvalTypeof||(e=ho(t)===this._rvalFloat)}return this._isEQ?e:!e},t}();function Af(t,e){return"eq"===t||"ne"===t?new Df("eq"===t,e):_t(If,t)?new Tf(t,e):null}var kf=function(){function t(){}return t.prototype.getRawData=function(){throw new Error("not supported")},t.prototype.getRawDataItem=function(t){throw new Error("not supported")},t.prototype.cloneRawData=function(){},t.prototype.getDimensionInfo=function(t){},t.prototype.cloneAllDimensionInfo=function(){},t.prototype.count=function(){},t.prototype.retrieveValue=function(t,e){},t.prototype.retrieveValueFromItem=function(t,e){},t.prototype.convertValue=function(t,e){return wf(t,e)},t}();function Lf(t){var e=t.sourceFormat;if(!zf(e)){var n="";0,vo(n)}return t.data}function Pf(t){var e=t.sourceFormat,n=t.data;if(!zf(e)){var i="";0,vo(i)}if(e===Fp){for(var r=[],o=0,a=n.length;o65535?Ff:Gf}function Uf(t,e,n,i,r){var o=Yf[n||"float"];if(r){var a=t[e],s=a&&a.length;if(s!==i){for(var l=new o(i),u=0;ug[1]&&(g[1]=f)}return this._rawCount=this._count=s,{start:a,end:s}},t.prototype._initDataFromProvider=function(t,e,n){for(var i=this._provider,r=this._chunks,o=this._dimensions,a=o.length,s=this._rawExtent,l=z(o,(function(t){return t.property})),u=0;uy[1]&&(y[1]=g)}}!i.persistent&&i.clean&&i.clean(),this._rawCount=this._count=e,this._extent=[]},t.prototype.count=function(){return this._count},t.prototype.get=function(t,e){if(!(e>=0&&e=0&&e=this._rawCount||t<0)return-1;if(!this._indices)return t;var e=this._indices,n=e[t];if(null!=n&&nt))return o;r=o-1}}return-1},t.prototype.indicesOfNearest=function(t,e,n){var i=this._chunks[t],r=[];if(!i)return r;null==n&&(n=1/0);for(var o=1/0,a=-1,s=0,l=0,u=this.count();l=0&&a<0)&&(o=c,a=h,s=0),h===a&&(r[s++]=l))}return r.length=s,r},t.prototype.getIndices=function(){var t,e=this._indices;if(e){var n=e.constructor,i=this._count;if(n===Array){t=new n(i);for(var r=0;r=u&&x<=h||isNaN(x))&&(a[s++]=d),d++}p=!0}else if(2===r){f=c[i[0]];var y=c[i[1]],v=t[i[1]][0],m=t[i[1]][1];for(g=0;g=u&&x<=h||isNaN(x))&&(_>=v&&_<=m||isNaN(_))&&(a[s++]=d),d++}p=!0}}if(!p)if(1===r)for(g=0;g=u&&x<=h||isNaN(x))&&(a[s++]=b)}else for(g=0;gt[M][1])&&(w=!1)}w&&(a[s++]=e.getRawIndex(g))}return sy[1]&&(y[1]=g)}}}},t.prototype.lttbDownSample=function(t,e){var n,i,r,o=this.clone([t],!0),a=o._chunks[t],s=this.count(),l=0,u=Math.floor(1/e),h=this.getRawIndex(0),c=new(Xf(this._rawCount))(Math.min(2*(Math.ceil(s/u)+2),s));c[l++]=h;for(var p=1;pn&&(n=i,r=I)}M>0&&M<_-x&&(c[l++]=Math.min(S,r),r=Math.max(S,r)),c[l++]=r,h=r}return c[l++]=this.getRawIndex(s-1),o._count=l,o._indices=c,o.getRawIndex=this._getRawIdx,o},t.prototype.downSample=function(t,e,n,i){for(var r=this.clone([t],!0),o=r._chunks,a=[],s=Math.floor(1/e),l=o[t],u=this.count(),h=r._rawExtent[t]=[1/0,-1/0],c=new(Xf(this._rawCount))(Math.ceil(u/s)),p=0,d=0;du-d&&(s=u-d,a.length=s);for(var f=0;fh[1]&&(h[1]=y),c[p++]=v}return r._count=p,r._indices=c,r._updateGetRawIdx(),r},t.prototype.each=function(t,e){if(this._count)for(var n=t.length,i=this._chunks,r=0,o=this.count();ra&&(a=l)}return i=[o,a],this._extent[t]=i,i},t.prototype.getRawDataItem=function(t){var e=this.getRawIndex(t);if(this._provider.persistent)return this._provider.getItem(e);for(var n=[],i=this._chunks,r=0;r=0?this._indices[t]:-1},t.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},t.internalField=function(){function t(t,e,n,i){return wf(t[i],this._dimensions[i])}Vf={arrayRows:t,objectRows:function(t,e,n,i){return wf(t[e],this._dimensions[i])},keyedColumns:t,original:function(t,e,n,i){var r=t&&(null==t.value?t:t.value);return wf(r instanceof Array?r[i]:r,this._dimensions[i])},typedArray:function(t,e,n,i){return t[i]}}}(),t}(),jf=function(){function t(t){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=t}return t.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},t.prototype._setLocalSource=function(t,e){this._sourceList=t,this._upstreamSignList=e,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},t.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},t.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},t.prototype._createSource=function(){this._setLocalSource([],[]);var t,e,n=this._sourceHost,i=this._getUpstreamSourceManagers(),r=!!i.length;if(Kf(n)){var o=n,a=void 0,s=void 0,l=void 0;if(r){var u=i[0];u.prepareSource(),a=(l=u.getSource()).data,s=l.sourceFormat,e=[u._getVersionSign()]}else s=$(a=o.get("data",!0))?Hp:Bp,e=[];var h=this._getSourceMetaRawOption()||{},c=l&&l.metaRawOption||{},p=rt(h.seriesLayoutBy,c.seriesLayoutBy)||null,d=rt(h.sourceHeader,c.sourceHeader),f=rt(h.dimensions,c.dimensions);t=p!==c.seriesLayoutBy||!!d!=!!c.sourceHeader||f?[$d(a,{seriesLayoutBy:p,sourceHeader:d,dimensions:f},s)]:[]}else{var g=n;if(r){var y=this._applyTransform(i);t=y.sourceList,e=y.upstreamSignList}else{t=[$d(g.get("source",!0),this._getSourceMetaRawOption(),null)],e=[]}}this._setLocalSource(t,e)},t.prototype._applyTransform=function(t){var e,n=this._sourceHost,i=n.get("transform",!0),r=n.get("fromTransformResult",!0);if(null!=r){var o="";1!==t.length&&$f(o)}var a,s=[],l=[];return E(t,(function(t){t.prepareSource();var e=t.getSource(r||0),n="";null==r||e||$f(n),s.push(e),l.push(t._getVersionSign())})),i?e=function(t,e,n){var i=bo(t),r=i.length,o="";r||vo(o);for(var a=0,s=r;a1||n>0&&!t.noHeader;return E(t.blocks,(function(t){var n=og(t);n>=e&&(e=n+ +(i&&(!n||ig(t)&&!t.noHeader)))})),e}return 0}function ag(t,e,n,i){var r,o=e.noHeader,a=(r=og(e),{html:tg[r],richText:eg[r]}),s=[],l=e.blocks||[];lt(!l||Y(l)),l=l||[];var u=t.orderMode;if(e.sortBlocks&&u){l=l.slice();var h={valueAsc:"asc",valueDesc:"desc"};if(_t(h,u)){var c=new Cf(h[u],null);l.sort((function(t,e){return c.evaluate(t.sortParam,e.sortParam)}))}else"seriesDesc"===u&&l.reverse()}E(l,(function(n,r){var o=e.valueFormatter,l=rg(n)(o?A(A({},t),{valueFormatter:o}):t,n,r>0?a.html:0,i);null!=l&&s.push(l)}));var p="richText"===t.renderMode?s.join(a.richText):ug(s.join(""),o?n:a.html);if(o)return p;var d=gp(e.header,"ordinal",t.useUTC),f=Qf(i,t.renderMode).nameStyle;return"richText"===t.renderMode?hg(t,d,f)+a.richText+p:ug('
'+re(d)+"
"+p,n)}function sg(t,e,n,i){var r=t.renderMode,o=e.noName,a=e.noValue,s=!e.markerType,l=e.name,u=t.useUTC,h=e.valueFormatter||t.valueFormatter||function(t){return z(t=Y(t)?t:[t],(function(t,e){return gp(t,Y(d)?d[e]:d,u)}))};if(!o||!a){var c=s?"":t.markupStyleCreator.makeTooltipMarker(e.markerType,e.markerColor||"#333",r),p=o?"":gp(l,"ordinal",u),d=e.valueType,f=a?[]:h(e.value),g=!s||!o,y=!s&&o,v=Qf(i,r),m=v.nameStyle,x=v.valueStyle;return"richText"===r?(s?"":c)+(o?"":hg(t,p,m))+(a?"":function(t,e,n,i,r){var o=[r],a=i?10:20;return n&&o.push({padding:[0,0,0,a],align:"right"}),t.markupStyleCreator.wrapRichTextStyle(Y(e)?e.join(" "):e,o)}(t,f,g,y,x)):ug((s?"":c)+(o?"":function(t,e,n){return''+re(t)+""}(p,!s,m))+(a?"":function(t,e,n,i){var r=n?"10px":"20px",o=e?"float:right;margin-left:"+r:"";return t=Y(t)?t:[t],''+z(t,(function(t){return re(t)})).join("  ")+""}(f,g,y,x)),n)}}function lg(t,e,n,i,r,o){if(t)return rg(t)({useUTC:r,renderMode:n,orderMode:i,markupStyleCreator:e,valueFormatter:t.valueFormatter},t,0,o)}function ug(t,e){return'
'+t+'
'}function hg(t,e,n){return t.markupStyleCreator.wrapRichTextStyle(e,n)}function cg(t,e){return _p(t.getData().getItemVisual(e,"style")[t.visualDrawType])}function pg(t,e){var n=t.get("padding");return null!=n?n:"richText"===e?[8,10]:10}var dg=function(){function t(){this.richTextStyles={},this._nextStyleNameId=po()}return t.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},t.prototype.makeTooltipMarker=function(t,e,n){var i="richText"===n?this._generateStyleName():null,r=xp({color:e,type:t,renderMode:n,markerId:i});return U(r)?r:(this.richTextStyles[i]=r.style,r.content)},t.prototype.wrapRichTextStyle=function(t,e){var n={};Y(e)?E(e,(function(t){return A(n,t)})):A(n,e);var i=this._generateStyleName();return this.richTextStyles[i]=n,"{"+i+"|"+t+"}"},t}();function fg(t){var e,n,i,r,o=t.series,a=t.dataIndex,s=t.multipleSeries,l=o.getData(),u=l.mapDimensionsAll("defaultedTooltip"),h=u.length,c=o.getRawValue(a),p=Y(c),d=cg(o,a);if(h>1||p&&!h){var f=function(t,e,n,i,r){var o=e.getData(),a=V(t,(function(t,e,n){var i=o.getDimensionInfo(n);return t||i&&!1!==i.tooltip&&null!=i.displayName}),!1),s=[],l=[],u=[];function h(t,e){var n=o.getDimensionInfo(e);n&&!1!==n.otherDims.tooltip&&(a?u.push(ng("nameValue",{markerType:"subItem",markerColor:r,name:n.displayName,value:t,valueType:n.type})):(s.push(t),l.push(n.type)))}return i.length?E(i,(function(t){h(gf(o,n,t),t)})):E(t,h),{inlineValues:s,inlineValueTypes:l,blocks:u}}(c,o,a,u,d);e=f.inlineValues,n=f.inlineValueTypes,i=f.blocks,r=f.inlineValues[0]}else if(h){var g=l.getDimensionInfo(u[0]);r=e=gf(l,a,u[0]),n=g.type}else r=e=p?c[0]:c;var y=ko(o),v=y&&o.name||"",m=l.getName(a),x=s?v:m;return ng("section",{header:v,noHeader:s||!y,sortParam:r,blocks:[ng("nameValue",{markerType:"item",markerColor:d,name:x,noName:!ut(x),value:e,valueType:n})].concat(i||[])})}var gg=Oo();function yg(t,e){return t.getName(e)||t.getId(e)}var vg="__universalTransitionEnabled",mg=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._selectedDataIndicesMap={},e}return n(e,t),e.prototype.init=function(t,e,n){this.seriesIndex=this.componentIndex,this.dataTask=xf({count:_g,reset:bg}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(t,n),(gg(this).sourceManager=new jf(this)).prepareSource();var i=this.getInitialData(t,n);Sg(i,this),this.dataTask.context.data=i,gg(this).dataBeforeProcessed=i,xg(this),this._initSelectedMapFromData(i)},e.prototype.mergeDefaultAndTheme=function(t,e){var n=Ap(this),i=n?Lp(t):{},r=this.subType;Rp.hasClass(r)&&(r+="Series"),C(t,e.getTheme().get(this.subType)),C(t,this.getDefaultOption()),wo(t,"label",["show"]),this.fillDataTextStyle(t.data),n&&kp(t,i,n)},e.prototype.mergeOption=function(t,e){t=C(this.option,t,!0),this.fillDataTextStyle(t.data);var n=Ap(this);n&&kp(this.option,t,n);var i=gg(this).sourceManager;i.dirty(),i.prepareSource();var r=this.getInitialData(t,e);Sg(r,this),this.dataTask.dirty(),this.dataTask.context.data=r,gg(this).dataBeforeProcessed=r,xg(this),this._initSelectedMapFromData(r)},e.prototype.fillDataTextStyle=function(t){if(t&&!$(t))for(var e=["show"],n=0;nthis.getShallow("animationThreshold")&&(e=!1),!!e},e.prototype.restoreData=function(){this.dataTask.dirty()},e.prototype.getColorFromPalette=function(t,e,n){var i=this.ecModel,r=ld.prototype.getColorFromPalette.call(this,t,e,n);return r||(r=i.getColorFromPalette(t,e,n)),r},e.prototype.coordDimToDataDim=function(t){return this.getRawData().mapDimensionsAll(t)},e.prototype.getProgressive=function(){return this.get("progressive")},e.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},e.prototype.select=function(t,e){this._innerSelect(this.getData(e),t)},e.prototype.unselect=function(t,e){var n=this.option.selectedMap;if(n){var i=this.option.selectedMode,r=this.getData(e);if("series"===i||"all"===n)return this.option.selectedMap={},void(this._selectedDataIndicesMap={});for(var o=0;o=0&&n.push(r)}return n},e.prototype.isSelected=function(t,e){var n=this.option.selectedMap;if(!n)return!1;var i=this.getData(e);return("all"===n||n[yg(i,t)])&&!i.getItemModel(t).get(["select","disabled"])},e.prototype.isUniversalTransitionEnabled=function(){if(this[vg])return!0;var t=this.option.universalTransition;return!!t&&(!0===t||t&&t.enabled)},e.prototype._innerSelect=function(t,e){var n,i,r=this.option,o=r.selectedMode,a=e.length;if(o&&a)if("series"===o)r.selectedMap="all";else if("multiple"===o){q(r.selectedMap)||(r.selectedMap={});for(var s=r.selectedMap,l=0;l0&&this._innerSelect(t,e)}},e.registerClass=function(t){return Rp.registerClass(t)},e.protoInitialize=function(){var t=e.prototype;t.type="series.__base__",t.seriesIndex=0,t.ignoreStyleOnData=!1,t.hasSymbolVisual=!1,t.defaultSymbol="circle",t.visualStyleAccessPath="itemStyle",t.visualDrawType="fill"}(),e}(Rp);function xg(t){var e=t.name;ko(t)||(t.name=function(t){var e=t.getRawData(),n=e.mapDimensionsAll("seriesName"),i=[];return E(n,(function(t){var n=e.getDimensionInfo(t);n.displayName&&i.push(n.displayName)})),i.join(" ")}(t)||e)}function _g(t){return t.model.getRawData().count()}function bg(t){var e=t.model;return e.setData(e.getRawData().cloneShallow()),wg}function wg(t,e){e.outputData&&t.end>e.outputData.count()&&e.model.getRawData().cloneShallow(e.outputData)}function Sg(t,e){E(vt(t.CHANGABLE_METHODS,t.DOWNSAMPLE_METHODS),(function(n){t.wrapMethod(n,H(Mg,e))}))}function Mg(t,e){var n=Ig(t);return n&&n.setOutputEnd((e||this).count()),e}function Ig(t){var e=(t.ecModel||{}).scheduler,n=e&&e.getPipeline(t.uid);if(n){var i=n.currentTask;if(i){var r=i.agentStubMap;r&&(i=r.get(t.uid))}return i}}R(mg,vf),R(mg,ld),Zo(mg,Rp);var Tg=function(){function t(){this.group=new zr,this.uid=Tc("viewComponent")}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){},t.prototype.updateLayout=function(t,e,n,i){},t.prototype.updateVisual=function(t,e,n,i){},t.prototype.toggleBlurSeries=function(t,e,n){},t.prototype.eachRendered=function(t){var e=this.group;e&&e.traverse(t)},t}();function Cg(){var t=Oo();return function(e){var n=t(e),i=e.pipelineContext,r=!!n.large,o=!!n.progressiveRender,a=n.large=!(!i||!i.large),s=n.progressiveRender=!(!i||!i.progressiveRender);return!(r===a&&o===s)&&"reset"}}Uo(Tg),$o(Tg);var Dg=Oo(),Ag=Cg(),kg=function(){function t(){this.group=new zr,this.uid=Tc("viewChart"),this.renderTask=xf({plan:Og,reset:Rg}),this.renderTask.context={view:this}}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){0},t.prototype.highlight=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&Pg(r,i,"emphasis")},t.prototype.downplay=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&Pg(r,i,"normal")},t.prototype.remove=function(t,e){this.group.removeAll()},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateLayout=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateVisual=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.eachRendered=function(t){qh(this.group,t)},t.markUpdateMethod=function(t,e){Dg(t).updateMethod=e},t.protoInitialize=void(t.prototype.type="chart"),t}();function Lg(t,e,n){t&&Kl(t)&&("emphasis"===e?kl:Ll)(t,n)}function Pg(t,e,n){var i=Po(t,e),r=e&&null!=e.highlightKey?function(t){var e=nl[t];return null==e&&el<=32&&(e=nl[t]=el++),e}(e.highlightKey):null;null!=i?E(bo(i),(function(e){Lg(t.getItemGraphicEl(e),n,r)})):t.eachItemGraphicEl((function(t){Lg(t,n,r)}))}function Og(t){return Ag(t.model)}function Rg(t){var e=t.model,n=t.ecModel,i=t.api,r=t.payload,o=e.pipelineContext.progressiveRender,a=t.view,s=r&&Dg(r).updateMethod,l=o?"incrementalPrepareRender":s&&a[s]?s:"render";return"render"!==l&&a[l](e,n,i,r),Ng[l]}Uo(kg),$o(kg);var Ng={incrementalPrepareRender:{progress:function(t,e){e.view.incrementalRender(t,e.model,e.ecModel,e.api,e.payload)}},render:{forceFirstProgress:!0,progress:function(t,e){e.view.render(e.model,e.ecModel,e.api,e.payload)}}},Eg="\0__throttleOriginMethod",zg="\0__throttleRate",Vg="\0__throttleType";function Bg(t,e,n){var i,r,o,a,s,l=0,u=0,h=null;function c(){u=(new Date).getTime(),h=null,t.apply(o,a||[])}e=e||0;var p=function(){for(var t=[],p=0;p=0?c():h=setTimeout(c,-r),l=i};return p.clear=function(){h&&(clearTimeout(h),h=null)},p.debounceNextCall=function(t){s=t},p}function Fg(t,e,n,i){var r=t[e];if(r){var o=r[Eg]||r,a=r[Vg];if(r[zg]!==n||a!==i){if(null==n||!i)return t[e]=o;(r=t[e]=Bg(o,n,"debounce"===i))[Eg]=o,r[Vg]=i,r[zg]=n}return r}}function Gg(t,e){var n=t[e];n&&n[Eg]&&(n.clear&&n.clear(),t[e]=n[Eg])}var Wg=Oo(),Hg={itemStyle:Jo(bc,!0),lineStyle:Jo(mc,!0)},Yg={lineStyle:"stroke",itemStyle:"fill"};function Xg(t,e){var n=t.visualStyleMapper||Hg[e];return n||(console.warn("Unknown style type '"+e+"'."),Hg.itemStyle)}function Ug(t,e){var n=t.visualDrawType||Yg[e];return n||(console.warn("Unknown style type '"+e+"'."),"fill")}var Zg={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=t.getModel(i),o=Xg(t,i)(r),a=r.getShallow("decal");a&&(n.setVisual("decal",a),a.dirty=!0);var s=Ug(t,i),l=o[s],u=X(l)?l:null,h="auto"===o.fill||"auto"===o.stroke;if(!o[s]||u||h){var c=t.getColorFromPalette(t.name,null,e.getSeriesCount());o[s]||(o[s]=c,n.setVisual("colorFromPalette",!0)),o.fill="auto"===o.fill||X(o.fill)?c:o.fill,o.stroke="auto"===o.stroke||X(o.stroke)?c:o.stroke}if(n.setVisual("style",o),n.setVisual("drawType",s),!e.isSeriesFiltered(t)&&u)return n.setVisual("colorFromPalette",!1),{dataEach:function(e,n){var i=t.getDataParams(n),r=A({},o);r[s]=u(i),e.setItemVisual(n,"style",r)}}}},jg=new Mc,qg={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){if(!t.ignoreStyleOnData&&!e.isSeriesFiltered(t)){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=Xg(t,i),o=n.getVisual("drawType");return{dataEach:n.hasItemOption?function(t,e){var n=t.getRawDataItem(e);if(n&&n[i]){jg.option=n[i];var a=r(jg);A(t.ensureUniqueItemVisual(e,"style"),a),jg.option.decal&&(t.setItemVisual(e,"decal",jg.option.decal),jg.option.decal.dirty=!0),o in a&&t.setItemVisual(e,"colorFromPalette",!1)}}:null}}}},Kg={performRawSeries:!0,overallReset:function(t){var e=yt();t.eachSeries((function(t){var n=t.getColorBy();if(!t.isColorBySeries()){var i=t.type+"-"+n,r=e.get(i);r||(r={},e.set(i,r)),Wg(t).scope=r}})),t.eachSeries((function(e){if(!e.isColorBySeries()&&!t.isSeriesFiltered(e)){var n=e.getRawData(),i={},r=e.getData(),o=Wg(e).scope,a=e.visualStyleAccessPath||"itemStyle",s=Ug(e,a);r.each((function(t){var e=r.getRawIndex(t);i[e]=t})),n.each((function(t){var a=i[t];if(r.getItemVisual(a,"colorFromPalette")){var l=r.ensureUniqueItemVisual(a,"style"),u=n.getName(t)||t+"",h=n.count();l[s]=e.getColorFromPalette(u,o,h)}}))}}))}},$g=Math.PI;var Jg=function(){function t(t,e,n,i){this._stageTaskMap=yt(),this.ecInstance=t,this.api=e,n=this._dataProcessorHandlers=n.slice(),i=this._visualHandlers=i.slice(),this._allHandlers=n.concat(i)}return t.prototype.restoreData=function(t,e){t.restoreData(e),this._stageTaskMap.each((function(t){var e=t.overallTask;e&&e.dirty()}))},t.prototype.getPerformArgs=function(t,e){if(t.__pipeline){var n=this._pipelineMap.get(t.__pipeline.id),i=n.context,r=!e&&n.progressiveEnabled&&(!i||i.progressiveRender)&&t.__idxInPipeline>n.blockIndex?n.step:null,o=i&&i.modDataCount;return{step:r,modBy:null!=o?Math.ceil(o/r):null,modDataCount:o}}},t.prototype.getPipeline=function(t){return this._pipelineMap.get(t)},t.prototype.updateStreamModes=function(t,e){var n=this._pipelineMap.get(t.uid),i=t.getData().count(),r=n.progressiveEnabled&&e.incrementalPrepareRender&&i>=n.threshold,o=t.get("large")&&i>=t.get("largeThreshold"),a="mod"===t.get("progressiveChunkMode")?i:null;t.pipelineContext=n.context={progressiveRender:r,modDataCount:a,large:o}},t.prototype.restorePipelines=function(t){var e=this,n=e._pipelineMap=yt();t.eachSeries((function(t){var i=t.getProgressive(),r=t.uid;n.set(r,{id:r,head:null,tail:null,threshold:t.getProgressiveThreshold(),progressiveEnabled:i&&!(t.preventIncremental&&t.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),e._pipe(t,t.dataTask)}))},t.prototype.prepareStageTasks=function(){var t=this._stageTaskMap,e=this.api.getModel(),n=this.api;E(this._allHandlers,(function(i){var r=t.get(i.uid)||t.set(i.uid,{}),o="";lt(!(i.reset&&i.overallReset),o),i.reset&&this._createSeriesStageTask(i,r,e,n),i.overallReset&&this._createOverallStageTask(i,r,e,n)}),this)},t.prototype.prepareView=function(t,e,n,i){var r=t.renderTask,o=r.context;o.model=e,o.ecModel=n,o.api=i,r.__block=!t.incrementalPrepareRender,this._pipe(e,r)},t.prototype.performDataProcessorTasks=function(t,e){this._performStageTasks(this._dataProcessorHandlers,t,e,{block:!0})},t.prototype.performVisualTasks=function(t,e,n){this._performStageTasks(this._visualHandlers,t,e,n)},t.prototype._performStageTasks=function(t,e,n,i){i=i||{};var r=!1,o=this;function a(t,e){return t.setDirty&&(!t.dirtyMap||t.dirtyMap.get(e.__pipeline.id))}E(t,(function(t,s){if(!i.visualType||i.visualType===t.visualType){var l=o._stageTaskMap.get(t.uid),u=l.seriesTaskMap,h=l.overallTask;if(h){var c,p=h.agentStubMap;p.each((function(t){a(i,t)&&(t.dirty(),c=!0)})),c&&h.dirty(),o.updatePayload(h,n);var d=o.getPerformArgs(h,i.block);p.each((function(t){t.perform(d)})),h.perform(d)&&(r=!0)}else u&&u.each((function(s,l){a(i,s)&&s.dirty();var u=o.getPerformArgs(s,i.block);u.skip=!t.performRawSeries&&e.isSeriesFiltered(s.context.model),o.updatePayload(s,n),s.perform(u)&&(r=!0)}))}})),this.unfinished=r||this.unfinished},t.prototype.performSeriesTasks=function(t){var e;t.eachSeries((function(t){e=t.dataTask.perform()||e})),this.unfinished=e||this.unfinished},t.prototype.plan=function(){this._pipelineMap.each((function(t){var e=t.tail;do{if(e.__block){t.blockIndex=e.__idxInPipeline;break}e=e.getUpstream()}while(e)}))},t.prototype.updatePayload=function(t,e){"remain"!==e&&(t.context.payload=e)},t.prototype._createSeriesStageTask=function(t,e,n,i){var r=this,o=e.seriesTaskMap,a=e.seriesTaskMap=yt(),s=t.seriesType,l=t.getTargetSeries;function u(e){var s=e.uid,l=a.set(s,o&&o.get(s)||xf({plan:iy,reset:ry,count:sy}));l.context={model:e,ecModel:n,api:i,useClearVisual:t.isVisual&&!t.isLayout,plan:t.plan,reset:t.reset,scheduler:r},r._pipe(e,l)}t.createOnAllSeries?n.eachRawSeries(u):s?n.eachRawSeriesByType(s,u):l&&l(n,i).each(u)},t.prototype._createOverallStageTask=function(t,e,n,i){var r=this,o=e.overallTask=e.overallTask||xf({reset:Qg});o.context={ecModel:n,api:i,overallReset:t.overallReset,scheduler:r};var a=o.agentStubMap,s=o.agentStubMap=yt(),l=t.seriesType,u=t.getTargetSeries,h=!0,c=!1,p="";function d(t){var e=t.uid,n=s.set(e,a&&a.get(e)||(c=!0,xf({reset:ty,onDirty:ny})));n.context={model:t,overallProgress:h},n.agent=o,n.__block=h,r._pipe(t,n)}lt(!t.createOnAllSeries,p),l?n.eachRawSeriesByType(l,d):u?u(n,i).each(d):(h=!1,E(n.getSeries(),d)),c&&o.dirty()},t.prototype._pipe=function(t,e){var n=t.uid,i=this._pipelineMap.get(n);!i.head&&(i.head=e),i.tail&&i.tail.pipe(e),i.tail=e,e.__idxInPipeline=i.count++,e.__pipeline=i},t.wrapStageHandler=function(t,e){return X(t)&&(t={overallReset:t,seriesType:ly(t)}),t.uid=Tc("stageHandler"),e&&(t.visualType=e),t},t}();function Qg(t){t.overallReset(t.ecModel,t.api,t.payload)}function ty(t){return t.overallProgress&&ey}function ey(){this.agent.dirty(),this.getDownstream().dirty()}function ny(){this.agent&&this.agent.dirty()}function iy(t){return t.plan?t.plan(t.model,t.ecModel,t.api,t.payload):null}function ry(t){t.useClearVisual&&t.data.clearAllVisual();var e=t.resetDefines=bo(t.reset(t.model,t.ecModel,t.api,t.payload));return e.length>1?z(e,(function(t,e){return ay(e)})):oy}var oy=ay(0);function ay(t){return function(e,n){var i=n.data,r=n.resetDefines[t];if(r&&r.dataEach)for(var o=e.start;o0&&h===r.length-u.length){var c=r.slice(0,h);"data"!==c&&(e.mainType=c,e[u.toLowerCase()]=t,s=!0)}}a.hasOwnProperty(r)&&(n[r]=t,s=!0),s||(i[r]=t)}))}return{cptQuery:e,dataQuery:n,otherQuery:i}},t.prototype.filter=function(t,e){var n=this.eventInfo;if(!n)return!0;var i=n.targetEl,r=n.packedEvent,o=n.model,a=n.view;if(!o||!a)return!0;var s=e.cptQuery,l=e.dataQuery;return u(s,o,"mainType")&&u(s,o,"subType")&&u(s,o,"index","componentIndex")&&u(s,o,"name")&&u(s,o,"id")&&u(l,r,"name")&&u(l,r,"dataIndex")&&u(l,r,"dataType")&&(!a.filterForExposedEvent||a.filterForExposedEvent(t,e.otherQuery,i,r));function u(t,e,n,i){return null==t[n]||e[i||n]===t[n]}},t.prototype.afterTrigger=function(){this.eventInfo=null},t}(),by=["symbol","symbolSize","symbolRotate","symbolOffset"],wy=by.concat(["symbolKeepAspect"]),Sy={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData();if(t.legendIcon&&n.setVisual("legendIcon",t.legendIcon),t.hasSymbolVisual){for(var i={},r={},o=!1,a=0;a=0&&Xy(l)?l:.5,t.createRadialGradient(a,s,0,a,s,l)}(t,e,n):function(t,e,n){var i=null==e.x?0:e.x,r=null==e.x2?1:e.x2,o=null==e.y?0:e.y,a=null==e.y2?0:e.y2;return e.global||(i=i*n.width+n.x,r=r*n.width+n.x,o=o*n.height+n.y,a=a*n.height+n.y),i=Xy(i)?i:0,r=Xy(r)?r:1,o=Xy(o)?o:0,a=Xy(a)?a:0,t.createLinearGradient(i,o,r,a)}(t,e,n),r=e.colorStops,o=0;o0&&(e=i.lineDash,n=i.lineWidth,e&&"solid"!==e&&n>0?"dashed"===e?[4*n,2*n]:"dotted"===e?[n]:j(e)?[e]:Y(e)?e:null:null),o=i.lineDashOffset;if(r){var a=i.strokeNoScale&&t.getLineScale?t.getLineScale():1;a&&1!==a&&(r=z(r,(function(t){return t/a})),o/=a)}return[r,o]}var Ky=new os(!0);function $y(t){var e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))}function Jy(t){return"string"==typeof t&&"none"!==t}function Qy(t){var e=t.fill;return null!=e&&"none"!==e}function tv(t,e){if(null!=e.fillOpacity&&1!==e.fillOpacity){var n=t.globalAlpha;t.globalAlpha=e.fillOpacity*e.opacity,t.fill(),t.globalAlpha=n}else t.fill()}function ev(t,e){if(null!=e.strokeOpacity&&1!==e.strokeOpacity){var n=t.globalAlpha;t.globalAlpha=e.strokeOpacity*e.opacity,t.stroke(),t.globalAlpha=n}else t.stroke()}function nv(t,e,n){var i=ia(e.image,e.__image,n);if(oa(i)){var r=t.createPattern(i,e.repeat||"repeat");if("function"==typeof DOMMatrix&&r&&r.setTransform){var o=new DOMMatrix;o.translateSelf(e.x||0,e.y||0),o.rotateSelf(0,0,(e.rotation||0)*wt),o.scaleSelf(e.scaleX||1,e.scaleY||1),r.setTransform(o)}return r}}var iv=["shadowBlur","shadowOffsetX","shadowOffsetY"],rv=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function ov(t,e,n,i,r){var o=!1;if(!i&&e===(n=n||{}))return!1;if(i||e.opacity!==n.opacity){lv(t,r),o=!0;var a=Math.max(Math.min(e.opacity,1),0);t.globalAlpha=isNaN(a)?xa.opacity:a}(i||e.blend!==n.blend)&&(o||(lv(t,r),o=!0),t.globalCompositeOperation=e.blend||xa.blend);for(var s=0;s0&&t.unfinished);t.unfinished||this._zr.flush()}}},e.prototype.getDom=function(){return this._dom},e.prototype.getId=function(){return this.id},e.prototype.getZr=function(){return this._zr},e.prototype.isSSR=function(){return this._ssr},e.prototype.setOption=function(t,e,n){if(!this[Iv])if(this._disposed)nm(this.id);else{var i,r,o;if(q(e)&&(n=e.lazyUpdate,i=e.silent,r=e.replaceMerge,o=e.transition,e=e.notMerge),this[Iv]=!0,!this._model||e){var a=new bd(this._api),s=this._theme,l=this._model=new pd;l.scheduler=this._scheduler,l.ssr=this._ssr,l.init(null,null,null,s,this._locale,a)}this._model.setOption(t,{replaceMerge:r},am);var u={seriesTransition:o,optionChanged:!0};if(n)this[Tv]={silent:i,updateParams:u},this[Iv]=!1,this.getZr().wakeUp();else{try{Ov(this),Ev.update.call(this,null,u)}catch(t){throw this[Tv]=null,this[Iv]=!1,t}this._ssr||this._zr.flush(),this[Tv]=null,this[Iv]=!1,Fv.call(this,i),Gv.call(this,i)}}},e.prototype.setTheme=function(){yo()},e.prototype.getModel=function(){return this._model},e.prototype.getOption=function(){return this._model&&this._model.getOption()},e.prototype.getWidth=function(){return this._zr.getWidth()},e.prototype.getHeight=function(){return this._zr.getHeight()},e.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||r.hasGlobalWindow&&window.devicePixelRatio||1},e.prototype.getRenderedCanvas=function(t){return this.renderToCanvas(t)},e.prototype.renderToCanvas=function(t){t=t||{};var e=this._zr.painter;return e.getRenderedCanvas({backgroundColor:t.backgroundColor||this._model.get("backgroundColor"),pixelRatio:t.pixelRatio||this.getDevicePixelRatio()})},e.prototype.renderToSVGString=function(t){t=t||{};var e=this._zr.painter;return e.renderToString({useViewBox:t.useViewBox})},e.prototype.getSvgDataURL=function(){if(r.svgSupported){var t=this._zr;return E(t.storage.getDisplayList(),(function(t){t.stopAnimation(null,!0)})),t.painter.toDataURL()}},e.prototype.getDataURL=function(t){if(!this._disposed){var e=(t=t||{}).excludeComponents,n=this._model,i=[],r=this;E(e,(function(t){n.eachComponent({mainType:t},(function(t){var e=r._componentsMap[t.__viewId];e.group.ignore||(i.push(e),e.group.ignore=!0)}))}));var o="svg"===this._zr.painter.getType()?this.getSvgDataURL():this.renderToCanvas(t).toDataURL("image/"+(t&&t.type||"png"));return E(i,(function(t){t.group.ignore=!1})),o}nm(this.id)},e.prototype.getConnectedDataURL=function(t){if(!this._disposed){var e="svg"===t.type,n=this.group,i=Math.min,r=Math.max,o=1/0;if(cm[n]){var a=o,s=o,l=-1/0,u=-1/0,c=[],p=t&&t.pixelRatio||this.getDevicePixelRatio();E(hm,(function(o,h){if(o.group===n){var p=e?o.getZr().painter.getSvgDom().innerHTML:o.renderToCanvas(T(t)),d=o.getDom().getBoundingClientRect();a=i(d.left,a),s=i(d.top,s),l=r(d.right,l),u=r(d.bottom,u),c.push({dom:p,left:d.left,top:d.top})}}));var d=(l*=p)-(a*=p),f=(u*=p)-(s*=p),g=h.createCanvas(),y=Gr(g,{renderer:e?"svg":"canvas"});if(y.resize({width:d,height:f}),e){var v="";return E(c,(function(t){var e=t.left-a,n=t.top-s;v+=''+t.dom+""})),y.painter.getSvgRoot().innerHTML=v,t.connectedBackgroundColor&&y.painter.setBackgroundColor(t.connectedBackgroundColor),y.refreshImmediately(),y.painter.toDataURL()}return t.connectedBackgroundColor&&y.add(new zs({shape:{x:0,y:0,width:d,height:f},style:{fill:t.connectedBackgroundColor}})),E(c,(function(t){var e=new ks({style:{x:t.left*p-a,y:t.top*p-s,image:t.dom}});y.add(e)})),y.refreshImmediately(),g.toDataURL("image/"+(t&&t.type||"png"))}return this.getDataURL(t)}nm(this.id)},e.prototype.convertToPixel=function(t,e){return zv(this,"convertToPixel",t,e)},e.prototype.convertFromPixel=function(t,e){return zv(this,"convertFromPixel",t,e)},e.prototype.containPixel=function(t,e){var n;if(!this._disposed)return E(No(this._model,t),(function(t,i){i.indexOf("Models")>=0&&E(t,(function(t){var r=t.coordinateSystem;if(r&&r.containPoint)n=n||!!r.containPoint(e);else if("seriesModels"===i){var o=this._chartsMap[t.__viewId];o&&o.containPoint&&(n=n||o.containPoint(e,t))}else 0}),this)}),this),!!n;nm(this.id)},e.prototype.getVisual=function(t,e){var n=No(this._model,t,{defaultMainType:"series"}),i=n.seriesModel;var r=i.getData(),o=n.hasOwnProperty("dataIndexInside")?n.dataIndexInside:n.hasOwnProperty("dataIndex")?r.indexOfRawIndex(n.dataIndex):null;return null!=o?Iy(r,o,e):Ty(r,e)},e.prototype.getViewOfComponentModel=function(t){return this._componentsMap[t.__viewId]},e.prototype.getViewOfSeriesModel=function(t){return this._chartsMap[t.__viewId]},e.prototype._initEvents=function(){var t,e,n,i=this;E(em,(function(t){var e=function(e){var n,r=i.getModel(),o=e.target,a="globalout"===t;if(a?n={}:o&&ky(o,(function(t){var e=Qs(t);if(e&&null!=e.dataIndex){var i=e.dataModel||r.getSeriesByIndex(e.seriesIndex);return n=i&&i.getDataParams(e.dataIndex,e.dataType,o)||{},!0}if(e.eventData)return n=A({},e.eventData),!0}),!0),n){var s=n.componentType,l=n.componentIndex;"markLine"!==s&&"markPoint"!==s&&"markArea"!==s||(s="series",l=n.seriesIndex);var u=s&&null!=l&&r.getComponent(s,l),h=u&&i["series"===u.mainType?"_chartsMap":"_componentsMap"][u.__viewId];0,n.event=e,n.type=t,i._$eventProcessor.eventInfo={targetEl:o,packedEvent:n,model:u,view:h},i.trigger(t,n)}};e.zrEventfulCallAtLast=!0,i._zr.on(t,e,i)})),E(rm,(function(t,e){i._messageCenter.on(e,(function(t){this.trigger(e,t)}),i)})),E(["selectchanged"],(function(t){i._messageCenter.on(t,(function(e){this.trigger(t,e)}),i)})),t=this._messageCenter,e=this,n=this._api,t.on("selectchanged",(function(t){var i=n.getModel();t.isFromClick?(Ay("map","selectchanged",e,i,t),Ay("pie","selectchanged",e,i,t)):"select"===t.fromAction?(Ay("map","selected",e,i,t),Ay("pie","selected",e,i,t)):"unselect"===t.fromAction&&(Ay("map","unselected",e,i,t),Ay("pie","unselected",e,i,t))}))},e.prototype.isDisposed=function(){return this._disposed},e.prototype.clear=function(){this._disposed?nm(this.id):this.setOption({series:[]},!0)},e.prototype.dispose=function(){if(this._disposed)nm(this.id);else{this._disposed=!0,this.getDom()&&Fo(this.getDom(),fm,"");var t=this,e=t._api,n=t._model;E(t._componentsViews,(function(t){t.dispose(n,e)})),E(t._chartsViews,(function(t){t.dispose(n,e)})),t._zr.dispose(),t._dom=t._model=t._chartsMap=t._componentsMap=t._chartsViews=t._componentsViews=t._scheduler=t._api=t._zr=t._throttledZrFlush=t._theme=t._coordSysMgr=t._messageCenter=null,delete hm[t.id]}},e.prototype.resize=function(t){if(!this[Iv])if(this._disposed)nm(this.id);else{this._zr.resize(t);var e=this._model;if(this._loadingFX&&this._loadingFX.resize(),e){var n=e.resetOption("media"),i=t&&t.silent;this[Tv]&&(null==i&&(i=this[Tv].silent),n=!0,this[Tv]=null),this[Iv]=!0;try{n&&Ov(this),Ev.update.call(this,{type:"resize",animation:A({duration:0},t&&t.animation)})}catch(t){throw this[Iv]=!1,t}this[Iv]=!1,Fv.call(this,i),Gv.call(this,i)}}},e.prototype.showLoading=function(t,e){if(this._disposed)nm(this.id);else if(q(t)&&(e=t,t=""),t=t||"default",this.hideLoading(),um[t]){var n=um[t](this._api,e),i=this._zr;this._loadingFX=n,i.add(n)}},e.prototype.hideLoading=function(){this._disposed?nm(this.id):(this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null)},e.prototype.makeActionFromEvent=function(t){var e=A({},t);return e.type=rm[t.type],e},e.prototype.dispatchAction=function(t,e){if(this._disposed)nm(this.id);else if(q(e)||(e={silent:!!e}),im[t.type]&&this._model)if(this[Iv])this._pendingActions.push(t);else{var n=e.silent;Bv.call(this,t,n);var i=e.flush;i?this._zr.flush():!1!==i&&r.browser.weChat&&this._throttledZrFlush(),Fv.call(this,n),Gv.call(this,n)}},e.prototype.updateLabelLayout=function(){xv.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},e.prototype.appendData=function(t){if(this._disposed)nm(this.id);else{var e=t.seriesIndex,n=this.getModel().getSeriesByIndex(e);0,n.appendData(t),this._scheduler.unfinished=!0,this.getZr().wakeUp()}},e.internalField=function(){function t(t){t.clearColorPalette(),t.eachSeries((function(t){t.clearColorPalette()}))}function e(t){for(var e=[],n=t.currentStates,i=0;i0?{duration:o,delay:i.get("delay"),easing:i.get("easing")}:null;n.eachRendered((function(t){if(t.states&&t.states.emphasis){if(yh(t))return;if(t instanceof Is&&function(t){var e=il(t);e.normalFill=t.style.fill,e.normalStroke=t.style.stroke;var n=t.states.select||{};e.selectFill=n.style&&n.style.fill||null,e.selectStroke=n.style&&n.style.stroke||null}(t),t.__dirty){var n=t.prevStates;n&&t.useStates(n)}if(r){t.stateTransition=a;var i=t.getTextContent(),o=t.getTextGuideLine();i&&(i.stateTransition=a),o&&(o.stateTransition=a)}t.__dirty&&e(t)}}))}Ov=function(t){var e=t._scheduler;e.restorePipelines(t._model),e.prepareStageTasks(),Rv(t,!0),Rv(t,!1),e.plan()},Rv=function(t,e){for(var n=t._model,i=t._scheduler,r=e?t._componentsViews:t._chartsViews,o=e?t._componentsMap:t._chartsMap,a=t._zr,s=t._api,l=0;le.get("hoverLayerThreshold")&&!r.node&&!r.worker&&e.eachSeries((function(e){if(!e.preventUsingHoverLayer){var n=t._chartsMap[e.__viewId];n.__alive&&n.eachRendered((function(t){t.states.emphasis&&(t.states.emphasis.hoverLayer=!0)}))}}))}(t,e),xv.trigger("series:afterupdate",e,n,l)},qv=function(t){t[Cv]=!0,t.getZr().wakeUp()},Kv=function(t){t[Cv]&&(t.getZr().storage.traverse((function(t){yh(t)||e(t)})),t[Cv]=!1)},Zv=function(t){return new(function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return n(i,e),i.prototype.getCoordinateSystems=function(){return t._coordSysMgr.getCoordinateSystems()},i.prototype.getComponentByElement=function(e){for(;e;){var n=e.__ecComponentInfo;if(null!=n)return t._model.getComponent(n.mainType,n.index);e=e.parent}},i.prototype.enterEmphasis=function(e,n){kl(e,n),qv(t)},i.prototype.leaveEmphasis=function(e,n){Ll(e,n),qv(t)},i.prototype.enterBlur=function(e){Pl(e),qv(t)},i.prototype.leaveBlur=function(e){Ol(e),qv(t)},i.prototype.enterSelect=function(e){Rl(e),qv(t)},i.prototype.leaveSelect=function(e){Nl(e),qv(t)},i.prototype.getModel=function(){return t.getModel()},i.prototype.getViewOfComponentModel=function(e){return t.getViewOfComponentModel(e)},i.prototype.getViewOfSeriesModel=function(e){return t.getViewOfSeriesModel(e)},i}(vd))(t)},jv=function(t){function e(t,e){for(var n=0;n=0)){Dm.push(n);var o=Jg.wrapStageHandler(n,r);o.__prio=e,o.__raw=n,t.push(o)}}function km(t,e){um[t]=e}function Lm(t,e,n){var i=bv("registerMap");i&&i(t,e,n)}var Pm=function(t){var e=(t=T(t)).type,n="";e||vo(n);var i=e.split(":");2!==i.length&&vo(n);var r=!1;"echarts"===i[0]&&(e=i[1],r=!0),t.__isBuiltIn=r,Nf.set(e,t)};Cm(wv,Zg),Cm(Sv,qg),Cm(Sv,Kg),Cm(wv,Sy),Cm(Sv,My),Cm(7e3,(function(t,e){t.eachRawSeries((function(n){if(!t.isSeriesFiltered(n)){var i=n.getData();i.hasItemVisual()&&i.each((function(t){var n=i.getItemVisual(t,"decal");n&&(i.ensureUniqueItemVisual(t,"style").decal=gv(n,e))}));var r=i.getVisual("decal");if(r)i.getVisual("style").decal=gv(r,e)}}))})),xm(Wd),_m(900,(function(t){var e=yt();t.eachSeries((function(t){var n=t.get("stack");if(n){var i=e.get(n)||e.set(n,[]),r=t.getData(),o={stackResultDimension:r.getCalculationInfo("stackResultDimension"),stackedOverDimension:r.getCalculationInfo("stackedOverDimension"),stackedDimension:r.getCalculationInfo("stackedDimension"),stackedByDimension:r.getCalculationInfo("stackedByDimension"),isStackedByIndex:r.getCalculationInfo("isStackedByIndex"),data:r,seriesModel:t};if(!o.stackedDimension||!o.isStackedByIndex&&!o.stackedByDimension)return;i.length&&r.setCalculationInfo("stackedOnSeries",i[i.length-1].seriesModel),i.push(o)}})),e.each(Hd)})),km("default",(function(t,e){k(e=e||{},{text:"loading",textColor:"#000",fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255, 255, 255, 0.8)",showSpinner:!0,color:"#5470c6",spinnerRadius:10,lineWidth:5,zlevel:0});var n=new zr,i=new zs({style:{fill:e.maskColor},zlevel:e.zlevel,z:1e4});n.add(i);var r,o=new Fs({style:{text:e.text,fill:e.textColor,fontSize:e.fontSize,fontWeight:e.fontWeight,fontStyle:e.fontStyle,fontFamily:e.fontFamily},zlevel:e.zlevel,z:10001}),a=new zs({style:{fill:"none"},textContent:o,textConfig:{position:"right",distance:10},zlevel:e.zlevel,z:10001});return n.add(a),e.showSpinner&&((r=new Qu({shape:{startAngle:-$g/2,endAngle:-$g/2+.1,r:e.spinnerRadius},style:{stroke:e.color,lineCap:"round",lineWidth:e.lineWidth},zlevel:e.zlevel,z:10001})).animateShape(!0).when(1e3,{endAngle:3*$g/2}).start("circularInOut"),r.animateShape(!0).when(1e3,{startAngle:3*$g/2}).delay(300).start("circularInOut"),n.add(r)),n.resize=function(){var n=o.getBoundingRect().width,s=e.showSpinner?e.spinnerRadius:0,l=(t.getWidth()-2*s-(e.showSpinner&&n?10:0)-n)/2-(e.showSpinner&&n?0:5+n/2)+(e.showSpinner?0:n/2)+(n?0:s),u=t.getHeight()/2;e.showSpinner&&r.setShape({cx:l,cy:u}),a.setShape({x:l-s,y:u-s,width:2*s,height:2*s}),i.setShape({x:0,y:0,width:t.getWidth(),height:t.getHeight()})},n.resize(),n})),Mm({type:ll,event:ll,update:ll},bt),Mm({type:ul,event:ul,update:ul},bt),Mm({type:hl,event:hl,update:hl},bt),Mm({type:cl,event:cl,update:cl},bt),Mm({type:pl,event:pl,update:pl},bt),mm("light",fy),mm("dark",xy);var Om=[],Rm={registerPreprocessor:xm,registerProcessor:_m,registerPostInit:bm,registerPostUpdate:wm,registerUpdateLifecycle:Sm,registerAction:Mm,registerCoordinateSystem:Im,registerLayout:Tm,registerVisual:Cm,registerTransform:Pm,registerLoading:km,registerMap:Lm,registerImpl:function(t,e){_v[t]=e},PRIORITY:Mv,ComponentModel:Rp,ComponentView:Tg,SeriesModel:mg,ChartView:kg,registerComponentModel:function(t){Rp.registerClass(t)},registerComponentView:function(t){Tg.registerClass(t)},registerSeriesModel:function(t){mg.registerClass(t)},registerChartView:function(t){kg.registerClass(t)},registerSubTypeDefaulter:function(t,e){Rp.registerSubTypeDefaulter(t,e)},registerPainter:function(t,e){Wr(t,e)}};function Nm(t){Y(t)?E(t,(function(t){Nm(t)})):P(Om,t)>=0||(Om.push(t),X(t)&&(t={install:t}),t.install(Rm))}function Em(t){return null==t?0:t.length||1}function zm(t){return t}var Vm=function(){function t(t,e,n,i,r,o){this._old=t,this._new=e,this._oldKeyGetter=n||zm,this._newKeyGetter=i||zm,this.context=r,this._diffModeMultiple="multiple"===o}return t.prototype.add=function(t){return this._add=t,this},t.prototype.update=function(t){return this._update=t,this},t.prototype.updateManyToOne=function(t){return this._updateManyToOne=t,this},t.prototype.updateOneToMany=function(t){return this._updateOneToMany=t,this},t.prototype.updateManyToMany=function(t){return this._updateManyToMany=t,this},t.prototype.remove=function(t){return this._remove=t,this},t.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},t.prototype._executeOneToOne=function(){var t=this._old,e=this._new,n={},i=new Array(t.length),r=new Array(e.length);this._initIndexMap(t,null,i,"_oldKeyGetter"),this._initIndexMap(e,n,r,"_newKeyGetter");for(var o=0;o1){var u=s.shift();1===s.length&&(n[a]=s[0]),this._update&&this._update(u,o)}else 1===l?(n[a]=null,this._update&&this._update(s,o)):this._remove&&this._remove(o)}this._performRestAdd(r,n)},t.prototype._executeMultiple=function(){var t=this._old,e=this._new,n={},i={},r=[],o=[];this._initIndexMap(t,n,r,"_oldKeyGetter"),this._initIndexMap(e,i,o,"_newKeyGetter");for(var a=0;a1&&1===c)this._updateManyToOne&&this._updateManyToOne(u,l),i[s]=null;else if(1===h&&c>1)this._updateOneToMany&&this._updateOneToMany(u,l),i[s]=null;else if(1===h&&1===c)this._update&&this._update(u,l),i[s]=null;else if(h>1&&c>1)this._updateManyToMany&&this._updateManyToMany(u,l),i[s]=null;else if(h>1)for(var p=0;p1)for(var a=0;a30}var Km,$m,Jm,Qm,tx,ex,nx,ix=q,rx=z,ox="undefined"==typeof Int32Array?Array:Int32Array,ax=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],sx=["_approximateExtent"],lx=function(){function t(t,e){var n;this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","lttbDownSample"];var i=!1;Um(t)?(n=t.dimensions,this._dimOmitted=t.isDimensionOmitted(),this._schema=t):(i=!0,n=t),n=n||["x","y"];for(var r={},o=[],a={},s=!1,l={},u=0;u=e)){var n=this._store.getProvider();this._updateOrdinalMeta();var i=this._nameList,r=this._idList;if(n.getSource().sourceFormat===Bp&&!n.pure)for(var o=[],a=t;a0},t.prototype.ensureUniqueItemVisual=function(t,e){var n=this._itemVisuals,i=n[t];i||(i=n[t]={});var r=i[e];return null==r&&(Y(r=this.getVisual(e))?r=r.slice():ix(r)&&(r=A({},r)),i[e]=r),r},t.prototype.setItemVisual=function(t,e,n){var i=this._itemVisuals[t]||{};this._itemVisuals[t]=i,ix(e)?A(i,e):i[e]=n},t.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},t.prototype.setLayout=function(t,e){ix(t)?A(this._layout,t):this._layout[t]=e},t.prototype.getLayout=function(t){return this._layout[t]},t.prototype.getItemLayout=function(t){return this._itemLayouts[t]},t.prototype.setItemLayout=function(t,e,n){this._itemLayouts[t]=n?A(this._itemLayouts[t]||{},e):e},t.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},t.prototype.setItemGraphicEl=function(t,e){var n=this.hostModel&&this.hostModel.seriesIndex;tl(n,this.dataType,t,e),this._graphicEls[t]=e},t.prototype.getItemGraphicEl=function(t){return this._graphicEls[t]},t.prototype.eachItemGraphicEl=function(t,e){E(this._graphicEls,(function(n,i){n&&t&&t.call(e,n,i)}))},t.prototype.cloneShallow=function(e){return e||(e=new t(this._schema?this._schema:rx(this.dimensions,this._getDimInfo,this),this.hostModel)),tx(e,this),e._store=this._store,e},t.prototype.wrapMethod=function(t,e){var n=this[t];X(n)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(t),this[t]=function(){var t=n.apply(this,arguments);return e.apply(this,[t].concat(at(arguments)))})},t.internalField=(Km=function(t){var e=t._invertedIndicesMap;E(e,(function(n,i){var r=t._dimInfos[i],o=r.ordinalMeta,a=t._store;if(o){n=e[i]=new ox(o.categories.length);for(var s=0;s1&&(s+="__ec__"+u),i[e]=s}})),t}();function ux(t,e){Kd(t)||(t=Jd(t));var n=(e=e||{}).coordDimensions||[],i=e.dimensionsDefine||t.dimensionsDefine||[],r=yt(),o=[],a=function(t,e,n,i){var r=Math.max(t.dimensionsDetectedCount||1,e.length,n.length,i||0);return E(e,(function(t){var e;q(t)&&(e=t.dimsDef)&&(r=Math.max(r,e.length))})),r}(t,n,i,e.dimensionsCount),s=e.canOmitUnusedDimensions&&qm(a),l=i===t.dimensionsDefine,u=l?jm(t):Zm(i),h=e.encodeDefine;!h&&e.encodeDefaulter&&(h=e.encodeDefaulter(t,a));for(var c=yt(h),p=new Wf(a),d=0;d0&&(i.name=r+(o-1)),o++,e.set(r,o)}}(o),new Xm({source:t,dimensions:o,fullDimensionCount:a,dimensionOmitted:s})}function hx(t,e,n){if(n||e.hasKey(t)){for(var i=0;e.hasKey(t+i);)i++;t+=i}return e.set(t,!0),t}var cx=function(t){this.coordSysDims=[],this.axisMap=yt(),this.categoryAxisMap=yt(),this.coordSysName=t};var px={cartesian2d:function(t,e,n,i){var r=t.getReferringComponents("xAxis",zo).models[0],o=t.getReferringComponents("yAxis",zo).models[0];e.coordSysDims=["x","y"],n.set("x",r),n.set("y",o),dx(r)&&(i.set("x",r),e.firstCategoryDimIndex=0),dx(o)&&(i.set("y",o),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},singleAxis:function(t,e,n,i){var r=t.getReferringComponents("singleAxis",zo).models[0];e.coordSysDims=["single"],n.set("single",r),dx(r)&&(i.set("single",r),e.firstCategoryDimIndex=0)},polar:function(t,e,n,i){var r=t.getReferringComponents("polar",zo).models[0],o=r.findAxisModel("radiusAxis"),a=r.findAxisModel("angleAxis");e.coordSysDims=["radius","angle"],n.set("radius",o),n.set("angle",a),dx(o)&&(i.set("radius",o),e.firstCategoryDimIndex=0),dx(a)&&(i.set("angle",a),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},geo:function(t,e,n,i){e.coordSysDims=["lng","lat"]},parallel:function(t,e,n,i){var r=t.ecModel,o=r.getComponent("parallel",t.get("parallelIndex")),a=e.coordSysDims=o.dimensions.slice();E(o.parallelAxisIndex,(function(t,o){var s=r.getComponent("parallelAxis",t),l=a[o];n.set(l,s),dx(s)&&(i.set(l,s),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=o))}))}};function dx(t){return"category"===t.get("type")}function fx(t,e,n){var i,r,o,a=(n=n||{}).byIndex,s=n.stackedCoordDimension;!function(t){return!Um(t.schema)}(e)?(r=e.schema,i=r.dimensions,o=e.store):i=e;var l,u,h,c,p=!(!t||!t.get("stack"));if(E(i,(function(t,e){U(t)&&(i[e]=t={name:t}),p&&!t.isExtraCoord&&(a||l||!t.ordinalMeta||(l=t),u||"ordinal"===t.type||"time"===t.type||s&&s!==t.coordDim||(u=t))})),!u||a||l||(a=!0),u){h="__\0ecstackresult_"+t.id,c="__\0ecstackedover_"+t.id,l&&(l.createInvertedIndices=!0);var d=u.coordDim,f=u.type,g=0;E(i,(function(t){t.coordDim===d&&g++}));var y={name:h,coordDim:d,coordDimIndex:g,type:f,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length},v={name:c,coordDim:c,coordDimIndex:g+1,type:f,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length+1};r?(o&&(y.storeDimIndex=o.ensureCalculationDimension(c,f),v.storeDimIndex=o.ensureCalculationDimension(h,f)),r.appendCalculationDimension(y),r.appendCalculationDimension(v)):(i.push(y),i.push(v))}return{stackedDimension:u&&u.name,stackedByDimension:l&&l.name,isStackedByIndex:a,stackedOverDimension:c,stackResultDimension:h}}function gx(t,e){return!!e&&e===t.getCalculationInfo("stackedDimension")}function yx(t,e){return gx(t,e)?t.getCalculationInfo("stackResultDimension"):e}function vx(t,e,n){n=n||{};var i,r=e.getSourceManager(),o=!1;t?(o=!0,i=Jd(t)):o=(i=r.getSource()).sourceFormat===Bp;var a=function(t){var e=t.get("coordinateSystem"),n=new cx(e),i=px[e];if(i)return i(t,n,n.axisMap,n.categoryAxisMap),n}(e),s=function(t,e){var n,i=t.get("coordinateSystem"),r=xd.get(i);return e&&e.coordSysDims&&(n=z(e.coordSysDims,(function(t){var n={name:t},i=e.axisMap.get(t);if(i){var r=i.get("type");n.type=Gm(r)}return n}))),n||(n=r&&(r.getDimensionsInfo?r.getDimensionsInfo():r.dimensions.slice())||["x","y"]),n}(e,a),l=n.useEncodeDefaulter,u=X(l)?l:l?H($p,s,e):null,h=ux(i,{coordDimensions:s,generateCoord:n.generateCoord,encodeDefine:e.getEncode(),encodeDefaulter:u,canOmitUnusedDimensions:!o}),c=function(t,e,n){var i,r;return n&&E(t,(function(t,o){var a=t.coordDim,s=n.categoryAxisMap.get(a);s&&(null==i&&(i=o),t.ordinalMeta=s.getOrdinalMeta(),e&&(t.createInvertedIndices=!0)),null!=t.otherDims.itemName&&(r=!0)})),r||null==i||(t[i].otherDims.itemName=0),i}(h.dimensions,n.createInvertedIndices,a),p=o?null:r.getSharedDataStore(h),d=fx(e,{schema:h,store:p}),f=new lx(h,e);f.setCalculationInfo(d);var g=null!=c&&function(t){if(t.sourceFormat===Bp){var e=function(t){var e=0;for(;ee[1]&&(e[1]=t[1])},t.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=t),isNaN(e)||(n[1]=e)},t.prototype.isInExtentRange=function(t){return this._extent[0]<=t&&this._extent[1]>=t},t.prototype.isBlank=function(){return this._isBlank},t.prototype.setBlank=function(t){this._isBlank=t},t}();$o(mx);var xx=0,_x=function(){function t(t){this.categories=t.categories||[],this._needCollect=t.needCollect,this._deduplication=t.deduplication,this.uid=++xx}return t.createByAxisModel=function(e){var n=e.option,i=n.data,r=i&&z(i,bx);return new t({categories:r,needCollect:!r,deduplication:!1!==n.dedplication})},t.prototype.getOrdinal=function(t){return this._getOrCreateMap().get(t)},t.prototype.parseAndCollect=function(t){var e,n=this._needCollect;if(!U(t)&&!n)return t;if(n&&!this._deduplication)return e=this.categories.length,this.categories[e]=t,e;var i=this._getOrCreateMap();return null==(e=i.get(t))&&(n?(e=this.categories.length,this.categories[e]=t,i.set(t,e)):e=NaN),e},t.prototype._getOrCreateMap=function(){return this._map||(this._map=yt(this.categories))},t}();function bx(t){return q(t)&&null!=t.value?t.value:t+""}function Sx(t){return"interval"===t.type||"log"===t.type}function Mx(t,e,n,i){var r={},o=t[1]-t[0],a=r.interval=so(o/e,!0);null!=n&&ai&&(a=r.interval=i);var s=r.intervalPrecision=Tx(a);return function(t,e){!isFinite(t[0])&&(t[0]=e[0]),!isFinite(t[1])&&(t[1]=e[1]),Cx(t,0,e),Cx(t,1,e),t[0]>t[1]&&(t[0]=t[1])}(r.niceTickExtent=[Zr(Math.ceil(t[0]/a)*a,s),Zr(Math.floor(t[1]/a)*a,s)],t),r}function Ix(t){var e=Math.pow(10,ao(t)),n=t/e;return n?2===n?n=3:3===n?n=5:n*=2:n=1,Zr(n*e)}function Tx(t){return qr(t)+2}function Cx(t,e,n){t[e]=Math.max(Math.min(t[e],n[1]),n[0])}function Dx(t,e){return t>=e[0]&&t<=e[1]}function Ax(t,e){return e[1]===e[0]?.5:(t-e[0])/(e[1]-e[0])}function kx(t,e){return t*(e[1]-e[0])+e[0]}var Lx=function(t){function e(e){var n=t.call(this,e)||this;n.type="ordinal";var i=n.getSetting("ordinalMeta");return i||(i=new _x({})),Y(i)&&(i=new _x({categories:z(i,(function(t){return q(t)?t.value:t}))})),n._ordinalMeta=i,n._extent=n.getSetting("extent")||[0,i.categories.length-1],n}return n(e,t),e.prototype.parse=function(t){return null==t?NaN:U(t)?this._ordinalMeta.getOrdinal(t):Math.round(t)},e.prototype.contain=function(t){return Dx(t=this.parse(t),this._extent)&&null!=this._ordinalMeta.categories[t]},e.prototype.normalize=function(t){return Ax(t=this._getTickNumber(this.parse(t)),this._extent)},e.prototype.scale=function(t){return t=Math.round(kx(t,this._extent)),this.getRawOrdinalNumber(t)},e.prototype.getTicks=function(){for(var t=[],e=this._extent,n=e[0];n<=e[1];)t.push({value:n}),n++;return t},e.prototype.getMinorTicks=function(t){},e.prototype.setSortInfo=function(t){if(null!=t){for(var e=t.ordinalNumbers,n=this._ordinalNumbersByTick=[],i=this._ticksByOrdinalNumber=[],r=0,o=this._ordinalMeta.categories.length,a=Math.min(o,e.length);r=0&&t=0&&t=t},e.prototype.getOrdinalMeta=function(){return this._ordinalMeta},e.prototype.calcNiceTicks=function(){},e.prototype.calcNiceExtent=function(){},e.type="ordinal",e}(mx);mx.registerClass(Lx);var Px=Zr,Ox=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="interval",e._interval=0,e._intervalPrecision=2,e}return n(e,t),e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return Dx(t,this._extent)},e.prototype.normalize=function(t){return Ax(t,this._extent)},e.prototype.scale=function(t){return kx(t,this._extent)},e.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=parseFloat(t)),isNaN(e)||(n[1]=parseFloat(e))},e.prototype.unionExtent=function(t){var e=this._extent;t[0]e[1]&&(e[1]=t[1]),this.setExtent(e[0],e[1])},e.prototype.getInterval=function(){return this._interval},e.prototype.setInterval=function(t){this._interval=t,this._niceExtent=this._extent.slice(),this._intervalPrecision=Tx(t)},e.prototype.getTicks=function(t){var e=this._interval,n=this._extent,i=this._niceExtent,r=this._intervalPrecision,o=[];if(!e)return o;n[0]1e4)return[];var s=o.length?o[o.length-1].value:i[1];return n[1]>s&&(t?o.push({value:Px(s+e,r)}):o.push({value:n[1]})),o},e.prototype.getMinorTicks=function(t){for(var e=this.getTicks(!0),n=[],i=this.getExtent(),r=1;ri[0]&&h0&&(o=null===o?s:Math.min(o,s))}n[i]=o}}return n}(t),n=[];return E(t,(function(t){var i,r=t.coordinateSystem.getBaseAxis(),o=r.getExtent();if("category"===r.type)i=r.getBandWidth();else if("value"===r.type||"time"===r.type){var a=r.dim+"_"+r.index,s=e[a],l=Math.abs(o[1]-o[0]),u=r.scale.getExtent(),h=Math.abs(u[1]-u[0]);i=s?l/h*s:l}else{var c=t.getData();i=Math.abs(o[1]-o[0])/c.count()}var p=Ur(t.get("barWidth"),i),d=Ur(t.get("barMaxWidth"),i),f=Ur(t.get("barMinWidth")||(Ux(t)?.5:1),i),g=t.get("barGap"),y=t.get("barCategoryGap");n.push({bandWidth:i,barWidth:p,barMaxWidth:d,barMinWidth:f,barGap:g,barCategoryGap:y,axisKey:Bx(r),stackId:Vx(t)})})),Wx(n)}function Wx(t){var e={};E(t,(function(t,n){var i=t.axisKey,r=t.bandWidth,o=e[i]||{bandWidth:r,remainedWidth:r,autoWidthCount:0,categoryGap:null,gap:"20%",stacks:{}},a=o.stacks;e[i]=o;var s=t.stackId;a[s]||o.autoWidthCount++,a[s]=a[s]||{width:0,maxWidth:0};var l=t.barWidth;l&&!a[s].width&&(a[s].width=l,l=Math.min(o.remainedWidth,l),o.remainedWidth-=l);var u=t.barMaxWidth;u&&(a[s].maxWidth=u);var h=t.barMinWidth;h&&(a[s].minWidth=h);var c=t.barGap;null!=c&&(o.gap=c);var p=t.barCategoryGap;null!=p&&(o.categoryGap=p)}));var n={};return E(e,(function(t,e){n[e]={};var i=t.stacks,r=t.bandWidth,o=t.categoryGap;if(null==o){var a=G(i).length;o=Math.max(35-4*a,15)+"%"}var s=Ur(o,r),l=Ur(t.gap,1),u=t.remainedWidth,h=t.autoWidthCount,c=(u-s)/(h+(h-1)*l);c=Math.max(c,0),E(i,(function(t){var e=t.maxWidth,n=t.minWidth;if(t.width){i=t.width;e&&(i=Math.min(i,e)),n&&(i=Math.max(i,n)),t.width=i,u-=i+l*i,h--}else{var i=c;e&&ei&&(i=n),i!==c&&(t.width=i,u-=i+l*i,h--)}})),c=(u-s)/(h+(h-1)*l),c=Math.max(c,0);var p,d=0;E(i,(function(t,e){t.width||(t.width=c),p=t,d+=t.width*(1+l)})),p&&(d-=p.width*l);var f=-d/2;E(i,(function(t,i){n[e][i]=n[e][i]||{bandWidth:r,offset:f,width:t.width},f+=t.width*(1+l)}))})),n}function Hx(t,e){var n=Fx(t,e),i=Gx(n);E(n,(function(t){var e=t.getData(),n=t.coordinateSystem.getBaseAxis(),r=Vx(t),o=i[Bx(n)][r],a=o.offset,s=o.width;e.setLayout({bandWidth:o.bandWidth,offset:a,size:s})}))}function Yx(t){return{seriesType:t,plan:Cg(),reset:function(t){if(Xx(t)){var e=t.getData(),n=t.coordinateSystem,i=n.getBaseAxis(),r=n.getOtherAxis(i),o=e.getDimensionIndex(e.mapDimension(r.dim)),a=e.getDimensionIndex(e.mapDimension(i.dim)),s=t.get("showBackground",!0),l=e.mapDimension(r.dim),u=e.getCalculationInfo("stackResultDimension"),h=gx(e,l)&&!!e.getCalculationInfo("stackedOnSeries"),c=r.isHorizontal(),p=function(t,e){return e.toGlobalCoord(e.dataToCoord("log"===e.type?1:0))}(0,r),d=Ux(t),f=t.get("barMinHeight")||0,g=u&&e.getDimensionIndex(u),y=e.getLayout("size"),v=e.getLayout("offset");return{progress:function(t,e){for(var i,r=t.count,l=d&&Ex(3*r),u=d&&s&&Ex(3*r),m=d&&Ex(r),x=n.master.getRect(),_=c?x.width:x.height,b=e.getStore(),w=0;null!=(i=t.next());){var S=b.get(h?g:o,i),M=b.get(a,i),I=p,T=void 0;h&&(T=+S-b.get(o,i));var C=void 0,D=void 0,A=void 0,k=void 0;if(c){var L=n.dataToPoint([S,M]);if(h)I=n.dataToPoint([T,M])[0];C=I,D=L[1]+v,A=L[0]-I,k=y,Math.abs(A)0)for(var s=0;s=0;--s)if(l[u]){o=l[u];break}o=o||a.none}if(Y(o)){var h=null==t.level?0:t.level>=0?t.level:o.length+t.level;o=o[h=Math.min(h,o.length-1)]}}return qc(new Date(t.value),o,r,i)}(t,e,n,this.getSetting("locale"),i)},e.prototype.getTicks=function(){var t=this._interval,e=this._extent,n=[];if(!t)return n;n.push({value:e[0],level:0});var i=this.getSetting("useUTC"),r=function(t,e,n,i){var r=1e4,o=Xc,a=0;function s(t,e,n,r,o,a,s){for(var l=new Date(e),u=e,h=l[r]();u1&&0===u&&o.unshift({value:o[0].value-p})}}for(u=0;u=i[0]&&v<=i[1]&&c++)}var m=(i[1]-i[0])/e;if(c>1.5*m&&p>m/1.5)break;if(u.push(g),c>m||t===o[d])break}h=[]}}0;var x=B(z(u,(function(t){return B(t,(function(t){return t.value>=i[0]&&t.value<=i[1]&&!t.notAdd}))})),(function(t){return t.length>0})),_=[],b=x.length-1;for(d=0;dn&&(this._approxInterval=n);var o=jx.length,a=Math.min(function(t,e,n,i){for(;n>>1;t[r][1]16?16:t>7.5?7:t>3.5?4:t>1.5?2:1}function Kx(t){return(t/=2592e6)>6?6:t>3?3:t>2?2:1}function $x(t){return(t/=Vc)>12?12:t>6?6:t>3.5?4:t>2?2:1}function Jx(t,e){return(t/=e?zc:Ec)>30?30:t>20?20:t>15?15:t>10?10:t>5?5:t>2?2:1}function Qx(t){return so(t,!0)}function t_(t,e,n){var i=new Date(t);switch(Zc(e)){case"year":case"month":i[ap(n)](0);case"day":i[sp(n)](1);case"hour":i[lp(n)](0);case"minute":i[up(n)](0);case"second":i[hp(n)](0),i[cp(n)](0)}return i.getTime()}mx.registerClass(Zx);var e_=mx.prototype,n_=Ox.prototype,i_=Zr,r_=Math.floor,o_=Math.ceil,a_=Math.pow,s_=Math.log,l_=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="log",e.base=10,e._originalScale=new Ox,e._interval=0,e}return n(e,t),e.prototype.getTicks=function(t){var e=this._originalScale,n=this._extent,i=e.getExtent();return z(n_.getTicks.call(this,t),(function(t){var e=t.value,r=Zr(a_(this.base,e));return r=e===n[0]&&this._fixMin?h_(r,i[0]):r,{value:r=e===n[1]&&this._fixMax?h_(r,i[1]):r}}),this)},e.prototype.setExtent=function(t,e){var n=s_(this.base);t=s_(Math.max(0,t))/n,e=s_(Math.max(0,e))/n,n_.setExtent.call(this,t,e)},e.prototype.getExtent=function(){var t=this.base,e=e_.getExtent.call(this);e[0]=a_(t,e[0]),e[1]=a_(t,e[1]);var n=this._originalScale.getExtent();return this._fixMin&&(e[0]=h_(e[0],n[0])),this._fixMax&&(e[1]=h_(e[1],n[1])),e},e.prototype.unionExtent=function(t){this._originalScale.unionExtent(t);var e=this.base;t[0]=s_(t[0])/s_(e),t[1]=s_(t[1])/s_(e),e_.unionExtent.call(this,t)},e.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},e.prototype.calcNiceTicks=function(t){t=t||10;var e=this._extent,n=e[1]-e[0];if(!(n===1/0||n<=0)){var i=oo(n);for(t/n*i<=.5&&(i*=10);!isNaN(i)&&Math.abs(i)<1&&Math.abs(i)>0;)i*=10;var r=[Zr(o_(e[0]/i)*i),Zr(r_(e[1]/i)*i)];this._interval=i,this._niceExtent=r}},e.prototype.calcNiceExtent=function(t){n_.calcNiceExtent.call(this,t),this._fixMin=t.fixMin,this._fixMax=t.fixMax},e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return Dx(t=s_(t)/s_(this.base),this._extent)},e.prototype.normalize=function(t){return Ax(t=s_(t)/s_(this.base),this._extent)},e.prototype.scale=function(t){return t=kx(t,this._extent),a_(this.base,t)},e.type="log",e}(mx),u_=l_.prototype;function h_(t,e){return i_(t,qr(e))}u_.getMinorTicks=n_.getMinorTicks,u_.getLabel=n_.getLabel,mx.registerClass(l_);var c_=function(){function t(t,e,n){this._prepareParams(t,e,n)}return t.prototype._prepareParams=function(t,e,n){n[1]0&&s>0&&!l&&(a=0),a<0&&s<0&&!u&&(s=0));var c=this._determinedMin,p=this._determinedMax;return null!=c&&(a=c,l=!0),null!=p&&(s=p,u=!0),{min:a,max:s,minFixed:l,maxFixed:u,isBlank:h}},t.prototype.modifyDataMinMax=function(t,e){this[d_[t]]=e},t.prototype.setDeterminedMinMax=function(t,e){var n=p_[t];this[n]=e},t.prototype.freeze=function(){this.frozen=!0},t}(),p_={min:"_determinedMin",max:"_determinedMax"},d_={min:"_dataMin",max:"_dataMax"};function f_(t,e,n){var i=t.rawExtentInfo;return i||(i=new c_(t,e,n),t.rawExtentInfo=i,i)}function g_(t,e){return null==e?null:nt(e)?NaN:t.parse(e)}function y_(t,e){var n=t.type,i=f_(t,e,t.getExtent()).calculate();t.setBlank(i.isBlank);var r=i.min,o=i.max,a=e.ecModel;if(a&&"time"===n){var s=Fx("bar",a),l=!1;if(E(s,(function(t){l=l||t.getBaseAxis()===e.axis})),l){var u=Gx(s),h=function(t,e,n,i){var r=n.axis.getExtent(),o=r[1]-r[0],a=function(t,e,n){if(t&&e){var i=t[Bx(e)];return null!=i&&null!=n?i[Vx(n)]:i}}(i,n.axis);if(void 0===a)return{min:t,max:e};var s=1/0;E(a,(function(t){s=Math.min(t.offset,s)}));var l=-1/0;E(a,(function(t){l=Math.max(t.offset+t.width,l)})),s=Math.abs(s),l=Math.abs(l);var u=s+l,h=e-t,c=h/(1-(s+l)/o)-h;return e+=c*(l/u),t-=c*(s/u),{min:t,max:e}}(r,o,e,u);r=h.min,o=h.max}}return{extent:[r,o],fixMin:i.minFixed,fixMax:i.maxFixed}}function v_(t,e){var n=e,i=y_(t,n),r=i.extent,o=n.get("splitNumber");t instanceof l_&&(t.base=n.get("logBase"));var a=t.type,s=n.get("interval"),l="interval"===a||"time"===a;t.setExtent(r[0],r[1]),t.calcNiceExtent({splitNumber:o,fixMin:i.fixMin,fixMax:i.fixMax,minInterval:l?n.get("minInterval"):null,maxInterval:l?n.get("maxInterval"):null}),null!=s&&t.setInterval&&t.setInterval(s)}function m_(t,e){if(e=e||t.get("type"))switch(e){case"category":return new Lx({ordinalMeta:t.getOrdinalMeta?t.getOrdinalMeta():t.getCategories(),extent:[1/0,-1/0]});case"time":return new Zx({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC")});default:return new(mx.getClass(e)||Ox)}}function x_(t){var e,n,i=t.getLabelModel().get("formatter"),r="category"===t.type?t.scale.getExtent()[0]:null;return"time"===t.scale.type?(n=i,function(e,i){return t.scale.getFormattedLabel(e,i,n)}):U(i)?function(e){return function(n){var i=t.scale.getLabel(n);return e.replace("{value}",null!=i?i:"")}}(i):X(i)?(e=i,function(n,i){return null!=r&&(i=n.value-r),e(__(t,n),i,null!=n.level?{level:n.level}:null)}):function(e){return t.scale.getLabel(e)}}function __(t,e){return"category"===t.type?t.scale.getLabel(e):e.value}function b_(t,e){var n=e*Math.PI/180,i=t.width,r=t.height,o=i*Math.abs(Math.cos(n))+Math.abs(r*Math.sin(n)),a=i*Math.abs(Math.sin(n))+Math.abs(r*Math.cos(n));return new ze(t.x,t.y,o,a)}function w_(t){var e=t.get("interval");return null==e?"auto":e}function S_(t){return"category"===t.type&&0===w_(t.getLabelModel())}function M_(t,e){var n={};return E(t.mapDimensionsAll(e),(function(e){n[yx(t,e)]=!0})),G(n)}var I_=function(){function t(){}return t.prototype.getNeedCrossZero=function(){return!this.option.scale},t.prototype.getCoordSysModel=function(){},t}();var T_={isDimensionStacked:gx,enableDataStack:fx,getStackedDimension:yx};var C_=Object.freeze({__proto__:null,createList:function(t){return vx(null,t)},getLayoutRect:Cp,dataStack:T_,createScale:function(t,e){var n=e;e instanceof Mc||(n=new Mc(e));var i=m_(n);return i.setExtent(t[0],t[1]),v_(i,n),i},mixinAxisModelCommonMethods:function(t){R(t,I_)},getECData:Qs,createTextStyle:function(t,e){return nc(t,null,null,"normal"!==(e=e||{}).state)},createDimensions:function(t,e){return ux(t,e).dimensions},createSymbol:Wy,enableHoverEmphasis:Hl});function D_(t,e){return Math.abs(t-e)<1e-8}function A_(t,e,n){var i=0,r=t[0];if(!r)return!1;for(var o=1;on&&(t=r,n=a)}if(t)return function(t){for(var e=0,n=0,i=0,r=t.length,o=t[r-1][0],a=t[r-1][1],s=0;s>1^-(1&s),l=l>>1^-(1&l),r=s+=r,o=l+=o,i.push([s/n,l/n])}return i}function F_(t,e){return z(B((t=function(t){if(!t.UTF8Encoding)return t;var e=t,n=e.UTF8Scale;return null==n&&(n=1024),E(e.features,(function(t){var e=t.geometry,i=e.encodeOffsets,r=e.coordinates;if(i)switch(e.type){case"LineString":e.coordinates=B_(r,i,n);break;case"Polygon":case"MultiLineString":V_(r,i,n);break;case"MultiPolygon":E(r,(function(t,e){return V_(t,i[e],n)}))}})),e.UTF8Encoding=!1,e}(t)).features,(function(t){return t.geometry&&t.properties&&t.geometry.coordinates.length>0})),(function(t){var n=t.properties,i=t.geometry,r=[];switch(i.type){case"Polygon":var o=i.coordinates;r.push(new R_(o[0],o.slice(1)));break;case"MultiPolygon":E(i.coordinates,(function(t){t[0]&&r.push(new R_(t[0],t.slice(1)))}));break;case"LineString":r.push(new N_([i.coordinates]));break;case"MultiLineString":r.push(new N_(i.coordinates))}var a=new E_(n[e||"name"],r,n.cp);return a.properties=n,a}))}var G_=Object.freeze({__proto__:null,linearMap:Xr,round:Zr,asc:jr,getPrecision:qr,getPrecisionSafe:Kr,getPixelPrecision:$r,getPercentWithPrecision:function(t,e,n){return t[e]&&Jr(t,n)[e]||0},MAX_SAFE_INTEGER:to,remRadian:eo,isRadianAroundZero:no,parseDate:ro,quantity:oo,quantityExponent:ao,nice:so,quantile:lo,reformIntervals:uo,isNumeric:co,numericToNumber:ho}),W_=Object.freeze({__proto__:null,parse:ro,format:qc}),H_=Object.freeze({__proto__:null,extendShape:Mh,extendPath:Th,makePath:Ah,makeImage:kh,mergePath:Ph,resizePath:Oh,createIcon:Hh,updateProps:fh,initProps:gh,getTransform:Eh,clipPointsByRect:Gh,clipRectByRect:Wh,registerShape:Ch,getShapeClass:Dh,Group:zr,Image:ks,Text:Fs,Circle:_u,Ellipse:wu,Sector:zu,Ring:Bu,Polygon:Wu,Polyline:Yu,Rect:zs,Line:Zu,BezierCurve:$u,Arc:Qu,IncrementalDisplayable:hh,CompoundPath:th,LinearGradient:nh,RadialGradient:ih,BoundingRect:ze}),Y_=Object.freeze({__proto__:null,addCommas:pp,toCamelCase:dp,normalizeCssArray:fp,encodeHTML:re,formatTpl:mp,getTooltipMarker:xp,formatTime:function(t,e,n){"week"!==t&&"month"!==t&&"quarter"!==t&&"half-year"!==t&&"year"!==t||(t="MM-dd\nyyyy");var i=ro(e),r=n?"getUTC":"get",o=i[r+"FullYear"](),a=i[r+"Month"]()+1,s=i[r+"Date"](),l=i[r+"Hours"](),u=i[r+"Minutes"](),h=i[r+"Seconds"](),c=i[r+"Milliseconds"]();return t=t.replace("MM",Uc(a,2)).replace("M",a).replace("yyyy",o).replace("yy",Uc(o%100+"",2)).replace("dd",Uc(s,2)).replace("d",s).replace("hh",Uc(l,2)).replace("h",l).replace("mm",Uc(u,2)).replace("m",u).replace("ss",Uc(h,2)).replace("s",h).replace("SSS",Uc(c,3))},capitalFirst:function(t){return t?t.charAt(0).toUpperCase()+t.substr(1):t},truncateText:sa,getTextRect:function(t,e,n,i,r,o,a,s){return new Fs({style:{text:t,font:e,align:n,verticalAlign:i,padding:r,rich:o,overflow:a?"truncate":null,lineHeight:s}}).getBoundingRect()}}),X_=Object.freeze({__proto__:null,map:z,each:E,indexOf:P,inherits:O,reduce:V,filter:B,bind:W,curry:H,isArray:Y,isString:U,isObject:q,isFunction:X,extend:A,defaults:k,clone:T,merge:C}),U_=Oo();function Z_(t){return"category"===t.type?function(t){var e=t.getLabelModel(),n=q_(t,e);return!e.get("show")||t.scale.isBlank()?{labels:[],labelCategoryInterval:n.labelCategoryInterval}:n}(t):function(t){var e=t.scale.getTicks(),n=x_(t);return{labels:z(e,(function(e,i){return{level:e.level,formattedLabel:n(e,i),rawLabel:t.scale.getLabel(e),tickValue:e.value}}))}}(t)}function j_(t,e){return"category"===t.type?function(t,e){var n,i,r=K_(t,"ticks"),o=w_(e),a=$_(r,o);if(a)return a;e.get("show")&&!t.scale.isBlank()||(n=[]);if(X(o))n=tb(t,o,!0);else if("auto"===o){var s=q_(t,t.getLabelModel());i=s.labelCategoryInterval,n=z(s.labels,(function(t){return t.tickValue}))}else n=Q_(t,i=o,!0);return J_(r,o,{ticks:n,tickCategoryInterval:i})}(t,e):{ticks:z(t.scale.getTicks(),(function(t){return t.value}))}}function q_(t,e){var n,i,r=K_(t,"labels"),o=w_(e),a=$_(r,o);return a||(X(o)?n=tb(t,o):(i="auto"===o?function(t){var e=U_(t).autoInterval;return null!=e?e:U_(t).autoInterval=t.calculateCategoryInterval()}(t):o,n=Q_(t,i)),J_(r,o,{labels:n,labelCategoryInterval:i}))}function K_(t,e){return U_(t)[e]||(U_(t)[e]=[])}function $_(t,e){for(var n=0;n1&&h/l>2&&(u=Math.round(Math.ceil(u/l)*l));var c=S_(t),p=a.get("showMinLabel")||c,d=a.get("showMaxLabel")||c;p&&u!==o[0]&&g(o[0]);for(var f=u;f<=o[1];f+=l)g(f);function g(t){var e={value:t};s.push(n?t:{formattedLabel:i(e),rawLabel:r.getLabel(e),tickValue:t})}return d&&f-l!==o[1]&&g(o[1]),s}function tb(t,e,n){var i=t.scale,r=x_(t),o=[];return E(i.getTicks(),(function(t){var a=i.getLabel(t),s=t.value;e(t.value,a)&&o.push(n?s:{formattedLabel:r(t),rawLabel:a,tickValue:s})})),o}var eb=[0,1],nb=function(){function t(t,e,n){this.onBand=!1,this.inverse=!1,this.dim=t,this.scale=e,this._extent=n||[0,0]}return t.prototype.contain=function(t){var e=this._extent,n=Math.min(e[0],e[1]),i=Math.max(e[0],e[1]);return t>=n&&t<=i},t.prototype.containData=function(t){return this.scale.contain(t)},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.getPixelPrecision=function(t){return $r(t||this.scale.getExtent(),this._extent)},t.prototype.setExtent=function(t,e){var n=this._extent;n[0]=t,n[1]=e},t.prototype.dataToCoord=function(t,e){var n=this._extent,i=this.scale;return t=i.normalize(t),this.onBand&&"ordinal"===i.type&&ib(n=n.slice(),i.count()),Xr(t,eb,n,e)},t.prototype.coordToData=function(t,e){var n=this._extent,i=this.scale;this.onBand&&"ordinal"===i.type&&ib(n=n.slice(),i.count());var r=Xr(t,n,eb,e);return this.scale.scale(r)},t.prototype.pointToData=function(t,e){},t.prototype.getTicksCoords=function(t){var e=(t=t||{}).tickModel||this.getTickModel(),n=z(j_(this,e).ticks,(function(t){return{coord:this.dataToCoord("ordinal"===this.scale.type?this.scale.getRawOrdinalNumber(t):t),tickValue:t}}),this);return function(t,e,n,i){var r=e.length;if(!t.onBand||n||!r)return;var o,a,s=t.getExtent();if(1===r)e[0].coord=s[0],o=e[1]={coord:s[1]};else{var l=e[r-1].tickValue-e[0].tickValue,u=(e[r-1].coord-e[0].coord)/l;E(e,(function(t){t.coord-=u/2})),a=1+t.scale.getExtent()[1]-e[r-1].tickValue,o={coord:e[r-1].coord+u*a},e.push(o)}var h=s[0]>s[1];c(e[0].coord,s[0])&&(i?e[0].coord=s[0]:e.shift());i&&c(s[0],e[0].coord)&&e.unshift({coord:s[0]});c(s[1],o.coord)&&(i?o.coord=s[1]:e.pop());i&&c(o.coord,s[1])&&e.push({coord:s[1]});function c(t,e){return t=Zr(t),e=Zr(e),h?t>e:t0&&t<100||(t=5),z(this.scale.getMinorTicks(t),(function(t){return z(t,(function(t){return{coord:this.dataToCoord(t),tickValue:t}}),this)}),this)},t.prototype.getViewLabels=function(){return Z_(this).labels},t.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},t.prototype.getTickModel=function(){return this.model.getModel("axisTick")},t.prototype.getBandWidth=function(){var t=this._extent,e=this.scale.getExtent(),n=e[1]-e[0]+(this.onBand?1:0);0===n&&(n=1);var i=Math.abs(t[1]-t[0]);return Math.abs(i)/n},t.prototype.calculateCategoryInterval=function(){return function(t){var e=function(t){var e=t.getLabelModel();return{axisRotate:t.getRotate?t.getRotate():t.isHorizontal&&!t.isHorizontal()?90:0,labelRotate:e.get("rotate")||0,font:e.getFont()}}(t),n=x_(t),i=(e.axisRotate-e.labelRotate)/180*Math.PI,r=t.scale,o=r.getExtent(),a=r.count();if(o[1]-o[0]<1)return 0;var s=1;a>40&&(s=Math.max(1,Math.floor(a/40)));for(var l=o[0],u=t.dataToCoord(l+1)-t.dataToCoord(l),h=Math.abs(u*Math.cos(i)),c=Math.abs(u*Math.sin(i)),p=0,d=0;l<=o[1];l+=s){var f,g,y=br(n({value:l}),e.font,"center","top");f=1.3*y.width,g=1.3*y.height,p=Math.max(p,f,7),d=Math.max(d,g,7)}var v=p/h,m=d/c;isNaN(v)&&(v=1/0),isNaN(m)&&(m=1/0);var x=Math.max(0,Math.floor(Math.min(v,m))),_=U_(t.model),b=t.getExtent(),w=_.lastAutoInterval,S=_.lastTickCount;return null!=w&&null!=S&&Math.abs(w-x)<=1&&Math.abs(S-a)<=1&&w>x&&_.axisExtent0===b[0]&&_.axisExtent1===b[1]?x=w:(_.lastTickCount=a,_.lastAutoInterval=x,_.axisExtent0=b[0],_.axisExtent1=b[1]),x}(this)},t}();function ib(t,e){var n=(t[1]-t[0])/e/2;t[0]+=n,t[1]-=n}var rb=2*Math.PI,ob=os.CMD,ab=["top","right","bottom","left"];function sb(t,e,n,i,r){var o=n.width,a=n.height;switch(t){case"top":i.set(n.x+o/2,n.y-e),r.set(0,-1);break;case"bottom":i.set(n.x+o/2,n.y+a+e),r.set(0,1);break;case"left":i.set(n.x-e,n.y+a/2),r.set(-1,0);break;case"right":i.set(n.x+o+e,n.y+a/2),r.set(1,0)}}function lb(t,e,n,i,r,o,a,s,l){a-=t,s-=e;var u=Math.sqrt(a*a+s*s),h=(a/=u)*n+t,c=(s/=u)*n+e;if(Math.abs(i-r)%rb<1e-4)return l[0]=h,l[1]=c,u-n;if(o){var p=i;i=hs(r),r=hs(p)}else i=hs(i),r=hs(r);i>r&&(r+=rb);var d=Math.atan2(s,a);if(d<0&&(d+=rb),d>=i&&d<=r||d+rb>=i&&d+rb<=r)return l[0]=h,l[1]=c,u-n;var f=n*Math.cos(i)+t,g=n*Math.sin(i)+e,y=n*Math.cos(r)+t,v=n*Math.sin(r)+e,m=(f-a)*(f-a)+(g-s)*(g-s),x=(y-a)*(y-a)+(v-s)*(v-s);return m0){e=e/180*Math.PI,fb.fromArray(t[0]),gb.fromArray(t[1]),yb.fromArray(t[2]),De.sub(vb,fb,gb),De.sub(mb,yb,gb);var n=vb.len(),i=mb.len();if(!(n<.001||i<.001)){vb.scale(1/n),mb.scale(1/i);var r=vb.dot(mb);if(Math.cos(e)1&&De.copy(bb,yb),bb.toArray(t[1])}}}}function Sb(t,e,n){if(n<=180&&n>0){n=n/180*Math.PI,fb.fromArray(t[0]),gb.fromArray(t[1]),yb.fromArray(t[2]),De.sub(vb,gb,fb),De.sub(mb,yb,gb);var i=vb.len(),r=mb.len();if(!(i<.001||r<.001))if(vb.scale(1/i),mb.scale(1/r),vb.dot(e)=a)De.copy(bb,yb);else{bb.scaleAndAdd(mb,o/Math.tan(Math.PI/2-s));var l=yb.x!==gb.x?(bb.x-gb.x)/(yb.x-gb.x):(bb.y-gb.y)/(yb.y-gb.y);if(isNaN(l))return;l<0?De.copy(bb,gb):l>1&&De.copy(bb,yb)}bb.toArray(t[1])}}}function Mb(t,e,n,i){var r="normal"===n,o=r?t:t.ensureState(n);o.ignore=e;var a=i.get("smooth");a&&!0===a&&(a=.3),o.shape=o.shape||{},a>0&&(o.shape.smooth=a);var s=i.getModel("lineStyle").getLineStyle();r?t.useStyle(s):o.style=s}function Ib(t,e){var n=e.smooth,i=e.points;if(i)if(t.moveTo(i[0][0],i[0][1]),n>0&&i.length>=3){var r=Vt(i[0],i[1]),o=Vt(i[1],i[2]);if(!r||!o)return t.lineTo(i[1][0],i[1][1]),void t.lineTo(i[2][0],i[2][1]);var a=Math.min(r,o)*n,s=Gt([],i[1],i[0],a/r),l=Gt([],i[1],i[2],a/o),u=Gt([],s,l,.5);t.bezierCurveTo(s[0],s[1],s[0],s[1],u[0],u[1]),t.bezierCurveTo(l[0],l[1],l[0],l[1],i[2][0],i[2][1])}else for(var h=1;h0&&o&&_(-h/a,0,a);var f,g,y=t[0],v=t[a-1];return m(),f<0&&b(-f,.8),g<0&&b(g,.8),m(),x(f,g,1),x(g,f,-1),m(),f<0&&w(-f),g<0&&w(g),u}function m(){f=y.rect[e]-i,g=r-v.rect[e]-v.rect[n]}function x(t,e,n){if(t<0){var i=Math.min(e,-t);if(i>0){_(i*n,0,a);var r=i+t;r<0&&b(-r*n,1)}else b(-t*n,1)}}function _(n,i,r){0!==n&&(u=!0);for(var o=i;o0)for(l=0;l0;l--){_(-(o[l-1]*c),l,a)}}}function w(t){var e=t<0?-1:1;t=Math.abs(t);for(var n=Math.ceil(t/(a-1)),i=0;i0?_(n,0,i+1):_(-n,a-i-1,a),(t-=n)<=0)return}}function kb(t,e,n,i){return Ab(t,"y","height",e,n,i)}function Lb(t){var e=[];t.sort((function(t,e){return e.priority-t.priority}));var n=new ze(0,0,0,0);function i(t){if(!t.ignore){var e=t.ensureState("emphasis");null==e.ignore&&(e.ignore=!1)}t.ignore=!0}for(var r=0;r=0&&n.attr(d.oldLayoutSelect),P(u,"emphasis")>=0&&n.attr(d.oldLayoutEmphasis)),fh(n,s,e,a)}else if(n.attr(s),!uc(n).valueAnimation){var h=rt(n.style.opacity,1);n.style.opacity=0,gh(n,{style:{opacity:h}},e,a)}if(d.oldLayout=s,n.states.select){var c=d.oldLayoutSelect={};Vb(c,s,Bb),Vb(c,n.states.select,Bb)}if(n.states.emphasis){var p=d.oldLayoutEmphasis={};Vb(p,s,Bb),Vb(p,n.states.emphasis,Bb)}cc(n,a,l,e,e)}if(i&&!i.ignore&&!i.invisible){r=(d=zb(i)).oldLayout;var d,f={points:i.shape.points};r?(i.attr({shape:r}),fh(i,{shape:f},e)):(i.setShape(f),i.style.strokePercent=0,gh(i,{style:{strokePercent:1}},e)),d.oldLayout=f}},t}(),Gb=Oo();var Wb=Math.sin,Hb=Math.cos,Yb=Math.PI,Xb=2*Math.PI,Ub=180/Yb,Zb=function(){function t(){}return t.prototype.reset=function(t){this._start=!0,this._d=[],this._str="",this._p=Math.pow(10,t||4)},t.prototype.moveTo=function(t,e){this._add("M",t,e)},t.prototype.lineTo=function(t,e){this._add("L",t,e)},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){this._add("C",t,e,n,i,r,o)},t.prototype.quadraticCurveTo=function(t,e,n,i){this._add("Q",t,e,n,i)},t.prototype.arc=function(t,e,n,i,r,o){this.ellipse(t,e,n,n,0,i,r,o)},t.prototype.ellipse=function(t,e,n,i,r,o,a,s){var l=a-o,u=!s,h=Math.abs(l),c=hi(h-Xb)||(u?l>=Xb:-l>=Xb),p=l>0?l%Xb:l%Xb+Xb,d=!1;d=!!c||!hi(h)&&p>=Yb==!!u;var f=t+n*Hb(o),g=e+i*Wb(o);this._start&&this._add("M",f,g);var y=Math.round(r*Ub);if(c){var v=1/this._p,m=(u?1:-1)*(Xb-v);this._add("A",n,i,y,1,+u,t+n*Hb(o+m),e+i*Wb(o+m)),v>.01&&this._add("A",n,i,y,0,+u,f,g)}else{var x=t+n*Hb(a),_=e+i*Wb(a);this._add("A",n,i,y,+d,+u,x,_)}},t.prototype.rect=function(t,e,n,i){this._add("M",t,e),this._add("l",n,0),this._add("l",0,i),this._add("l",-n,0),this._add("Z")},t.prototype.closePath=function(){this._d.length>0&&this._add("Z")},t.prototype._add=function(t,e,n,i,r,o,a,s,l){for(var u=[],h=this._p,c=1;c"}(r,o)+("style"!==r?re(a):a||"")+(i?""+n+z(i,(function(e){return t(e)})).join(n)+n:"")+("")}(t)}function rw(t){return{zrId:t,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssClassIdx:0,cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function ow(t,e,n,i){return nw("svg","root",{width:t,height:e,xmlns:Qb,"xmlns:xlink":tw,version:"1.1",baseProfile:"full",viewBox:!!i&&"0 0 "+t+" "+e},n)}var aw={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},sw="transform-origin";function lw(t,e,n){var i=A({},t.shape);A(i,e),t.buildPath(n,i);var r=new Zb;return r.reset(_i(t)),n.rebuildPath(r,1),r.generateStr(),r.getStr()}function uw(t,e){var n=e.originX,i=e.originY;(n||i)&&(t[sw]=n+"px "+i+"px")}var hw={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function cw(t,e){var n=e.zrId+"-ani-"+e.cssAnimIdx++;return e.cssAnims[n]=t,n}function pw(t){return U(t)?aw[t]?"cubic-bezier("+aw[t]+")":Pn(t)?t:"":""}function dw(t,e,n,i){var r=t.animators,o=r.length,a=[];if(t instanceof th){var s=function(t,e,n){var i,r,o=t.shape.paths,a={};if(E(o,(function(t){var e=rw(n.zrId);e.animation=!0,dw(t,{},e,!0);var o=e.cssAnims,s=e.cssNodes,l=G(o),u=l.length;if(u){var h=o[r=l[u-1]];for(var c in h){var p=h[c];a[c]=a[c]||{d:""},a[c].d+=p.d||""}for(var d in s){var f=s[d].animation;f.indexOf(r)>=0&&(i=f)}}})),i){e.d=!1;var s=cw(a,n);return i.replace(r,s)}}(t,e,n);if(s)a.push(s);else if(!o)return}else if(!o)return;for(var l={},u=0;u0})).length)return cw(h,n)+" "+r[0]+" both"}for(var y in l){(s=g(l[y]))&&a.push(s)}if(a.length){var v=n.zrId+"-cls-"+n.cssClassIdx++;n.cssNodes["."+v]={animation:a.join(",")},e.class=v}}var fw=Math.round;function gw(t){return t&&U(t.src)}function yw(t){return t&&X(t.toDataURL)}function vw(t,e,n,i){Jb((function(r,o){var a="fill"===r||"stroke"===r;a&&mi(o)?Cw(e,t,r,i):a&&gi(o)?Dw(n,t,r,i):t[r]=o}),e,n,!1),function(t,e,n){var i=t.style;if(function(t){return t&&(t.shadowBlur||t.shadowOffsetX||t.shadowOffsetY)}(i)){var r=function(t){var e=t.style,n=t.getGlobalScale();return[e.shadowColor,(e.shadowBlur||0).toFixed(2),(e.shadowOffsetX||0).toFixed(2),(e.shadowOffsetY||0).toFixed(2),n[0],n[1]].join(",")}(t),o=n.shadowCache,a=o[r];if(!a){var s=t.getGlobalScale(),l=s[0],u=s[1];if(!l||!u)return;var h=i.shadowOffsetX||0,c=i.shadowOffsetY||0,p=i.shadowBlur,d=li(i.shadowColor),f=d.opacity,g=d.color,y=p/2/l+" "+p/2/u;a=n.zrId+"-s"+n.shadowIdx++,n.defs[a]=nw("filter",a,{id:a,x:"-100%",y:"-100%",width:"300%",height:"300%"},[nw("feDropShadow","",{dx:h/l,dy:c/u,stdDeviation:y,"flood-color":g,"flood-opacity":f})]),o[r]=a}e.filter=xi(a)}}(n,t,i)}function mw(t){return hi(t[0]-1)&&hi(t[1])&&hi(t[2])&&hi(t[3]-1)}function xw(t,e,n){if(e&&(!function(t){return hi(t[4])&&hi(t[5])}(e)||!mw(e))){var i=n?10:1e4;t.transform=mw(e)?"translate("+fw(e[4]*i)/i+" "+fw(e[5]*i)/i+")":function(t){return"matrix("+ci(t[0])+","+ci(t[1])+","+ci(t[2])+","+ci(t[3])+","+pi(t[4])+","+pi(t[5])+")"}(e)}}function _w(t,e,n){for(var i=t.points,r=[],o=0;ol?Hw(t,null==n[c+1]?null:n[c+1].elm,n,s,c):Yw(t,e,a,l))}(n,i,r):Bw(r)?(Bw(t.text)&&Ew(n,""),Hw(n,null,r,0,r.length-1)):Bw(i)?Yw(n,i,0,i.length-1):Bw(t.text)&&Ew(n,""):t.text!==e.text&&(Bw(i)&&Yw(n,i,0,i.length-1),Ew(n,e.text)))}var Zw=0,jw=function(){function t(t,e,n){if(this.type="svg",this.refreshHover=qw("refreshHover"),this.configLayer=qw("configLayer"),this.storage=e,this._opts=n=A({},n),this.root=t,this._id="zr"+Zw++,this._oldVNode=ow(n.width,n.height),t&&!n.ssr){var i=this._viewport=document.createElement("div");i.style.cssText="position:relative;overflow:hidden";var r=this._svgDom=this._oldVNode.elm=ew("svg");Xw(null,this._oldVNode),i.appendChild(r),t.appendChild(i)}this.resize(n.width,n.height)}return t.prototype.getType=function(){return this.type},t.prototype.getViewportRoot=function(){return this._viewport},t.prototype.getViewportRootOffset=function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},t.prototype.getSvgDom=function(){return this._svgDom},t.prototype.refresh=function(){if(this.root){var t=this.renderToVNode({willUpdate:!0});t.attrs.style="position:absolute;left:0;top:0;user-select:none",function(t,e){if(Gw(t,e))Uw(t,e);else{var n=t.elm,i=Rw(n);Ww(e),null!==i&&(Lw(i,e.elm,Nw(n)),Yw(i,[t],0,0))}}(this._oldVNode,t),this._oldVNode=t}},t.prototype.renderOneToVNode=function(t){return Tw(t,rw(this._id))},t.prototype.renderToVNode=function(t){t=t||{};var e=this.storage.getDisplayList(!0),n=this._width,i=this._height,r=rw(this._id);r.animation=t.animation,r.willUpdate=t.willUpdate,r.compress=t.compress;var o=[],a=this._bgVNode=function(t,e,n,i){var r;if(n&&"none"!==n)if(r=nw("rect","bg",{width:t,height:e,x:"0",y:"0",id:"0"}),mi(n))Cw({fill:n},r.attrs,"fill",i);else if(gi(n))Dw({style:{fill:n},dirty:bt,getBoundingRect:function(){return{width:t,height:e}}},r.attrs,"fill",i);else{var o=li(n),a=o.color,s=o.opacity;r.attrs.fill=a,s<1&&(r.attrs["fill-opacity"]=s)}return r}(n,i,this._backgroundColor,r);a&&o.push(a);var s=t.compress?null:this._mainVNode=nw("g","main",{},[]);this._paintList(e,r,s?s.children:o),s&&o.push(s);var l=z(G(r.defs),(function(t){return r.defs[t]}));if(l.length&&o.push(nw("defs","defs",{},l)),t.animation){var u=function(t,e,n){var i=(n=n||{}).newline?"\n":"",r=" {"+i,o=i+"}",a=z(G(t),(function(e){return e+r+z(G(t[e]),(function(n){return n+":"+t[e][n]+";"})).join(i)+o})).join(i),s=z(G(e),(function(t){return"@keyframes "+t+r+z(G(e[t]),(function(n){return n+r+z(G(e[t][n]),(function(i){var r=e[t][n][i];return"d"===i&&(r='path("'+r+'")'),i+":"+r+";"})).join(i)+o})).join(i)+o})).join(i);return a||s?[""].join(i):""}(r.cssNodes,r.cssAnims,{newline:!0});if(u){var h=nw("style","stl",{},[],u);o.push(h)}}return ow(n,i,o,t.useViewBox)},t.prototype.renderToString=function(t){return t=t||{},iw(this.renderToVNode({animation:rt(t.cssAnimation,!0),willUpdate:!1,compress:!0,useViewBox:rt(t.useViewBox,!0)}),{newline:!0})},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t},t.prototype.getSvgRoot=function(){return this._mainVNode&&this._mainVNode.elm},t.prototype._paintList=function(t,e,n){for(var i,r,o=t.length,a=[],s=0,l=0,u=0;u=0&&(!c||!r||c[f]!==r[f]);f--);for(var g=d-1;g>f;g--)i=a[--s-1];for(var y=f+1;y=a)}}for(var h=this.__startIndex;h15)break}n.prevElClipPaths&&u.restore()};if(p)if(0===p.length)s=l.__endIndex;else for(var _=d.dpr,b=0;b0&&t>i[0]){for(s=0;st);s++);a=n[i[s]]}if(i.splice(s+1,0,t),n[t]=e,!e.virtual)if(a){var l=a.dom;l.nextSibling?o.insertBefore(e.dom,l.nextSibling):o.appendChild(e.dom)}else o.firstChild?o.insertBefore(e.dom,o.firstChild):o.appendChild(e.dom);e.__painter=this}},t.prototype.eachLayer=function(t,e){for(var n=this._zlevelList,i=0;i0?tS:0),this._needsManuallyCompositing),u.__builtin__||I("ZLevel "+l+" has been used by unkown layer "+u.id),u!==o&&(u.__used=!0,u.__startIndex!==r&&(u.__dirty=!0),u.__startIndex=r,u.incremental?u.__drawIndex=-1:u.__drawIndex=r,e(r),o=u),1&s.__dirty&&!s.__inHover&&(u.__dirty=!0,u.incremental&&u.__drawIndex<0&&(u.__drawIndex=r))}e(r),this.eachBuiltinLayer((function(t,e){!t.__used&&t.getElementCount()>0&&(t.__dirty=!0,t.__startIndex=t.__endIndex=t.__drawIndex=0),t.__dirty&&t.__drawIndex<0&&(t.__drawIndex=t.__startIndex)}))},t.prototype.clear=function(){return this.eachBuiltinLayer(this._clearLayer),this},t.prototype._clearLayer=function(t){t.clear()},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t,E(this._layers,(function(t){t.setUnpainted()}))},t.prototype.configLayer=function(t,e){if(e){var n=this._layerConfig;n[t]?C(n[t],e,!0):n[t]=e;for(var i=0;i-1&&(s.style.stroke=s.style.fill,s.style.fill="#fff",s.style.lineWidth=2),e},e.type="series.line",e.dependencies=["grid","polar"],e.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1},e}(mg);function iS(t,e){var n=t.mapDimensionsAll("defaultedLabel"),i=n.length;if(1===i){var r=gf(t,e,n[0]);return null!=r?r+"":null}if(i){for(var o=[],a=0;a=0&&i.push(e[o])}return i.join(" ")}var oS=function(t){function e(e,n,i,r){var o=t.call(this)||this;return o.updateData(e,n,i,r),o}return n(e,t),e.prototype._createSymbol=function(t,e,n,i,r){this.removeAll();var o=Wy(t,-1,-1,2,2,null,r);o.attr({z2:100,culling:!0,scaleX:i[0]/2,scaleY:i[1]/2}),o.drift=aS,this._symbolType=t,this.add(o)},e.prototype.stopSymbolAnimation=function(t){this.childAt(0).stopAnimation(null,t)},e.prototype.getSymbolType=function(){return this._symbolType},e.prototype.getSymbolPath=function(){return this.childAt(0)},e.prototype.highlight=function(){kl(this.childAt(0))},e.prototype.downplay=function(){Ll(this.childAt(0))},e.prototype.setZ=function(t,e){var n=this.childAt(0);n.zlevel=t,n.z=e},e.prototype.setDraggable=function(t,e){var n=this.childAt(0);n.draggable=t,n.cursor=!e&&t?"move":n.cursor},e.prototype.updateData=function(t,n,i,r){this.silent=!1;var o=t.getItemVisual(n,"symbol")||"circle",a=t.hostModel,s=e.getSymbolSize(t,n),l=o!==this._symbolType,u=r&&r.disableAnimation;if(l){var h=t.getItemVisual(n,"symbolKeepAspect");this._createSymbol(o,t,n,s,h)}else{(p=this.childAt(0)).silent=!1;var c={scaleX:s[0]/2,scaleY:s[1]/2};u?p.attr(c):fh(p,c,a,n),_h(p)}if(this._updateCommon(t,n,s,i,r),l){var p=this.childAt(0);if(!u){c={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:p.style.opacity}};p.scaleX=p.scaleY=0,p.style.opacity=0,gh(p,c,a,n)}}u&&this.childAt(0).stopAnimation("leave")},e.prototype._updateCommon=function(t,e,n,i,r){var o,a,s,l,u,h,c,p,d,f=this.childAt(0),g=t.hostModel;if(i&&(o=i.emphasisItemStyle,a=i.blurItemStyle,s=i.selectItemStyle,l=i.focus,u=i.blurScope,c=i.labelStatesModels,p=i.hoverScale,d=i.cursorStyle,h=i.emphasisDisabled),!i||t.hasItemOption){var y=i&&i.itemModel?i.itemModel:t.getItemModel(e),v=y.getModel("emphasis");o=v.getModel("itemStyle").getItemStyle(),s=y.getModel(["select","itemStyle"]).getItemStyle(),a=y.getModel(["blur","itemStyle"]).getItemStyle(),l=v.get("focus"),u=v.get("blurScope"),h=v.get("disabled"),c=ec(y),p=v.getShallow("scale"),d=y.getShallow("cursor")}var m=t.getItemVisual(e,"symbolRotate");f.attr("rotation",(m||0)*Math.PI/180||0);var x=Yy(t.getItemVisual(e,"symbolOffset"),n);x&&(f.x=x[0],f.y=x[1]),d&&f.attr("cursor",d);var _=t.getItemVisual(e,"style"),b=_.fill;if(f instanceof ks){var w=f.style;f.useStyle(A({image:w.image,x:w.x,y:w.y,width:w.width,height:w.height},_))}else f.__isEmptyBrush?f.useStyle(A({},_)):f.useStyle(_),f.style.decal=null,f.setColor(b,r&&r.symbolInnerColor),f.style.strokeNoScale=!0;var S=t.getItemVisual(e,"liftZ"),M=this._z2;null!=S?null==M&&(this._z2=f.z2,f.z2+=S):null!=M&&(f.z2=M,this._z2=null);var I=r&&r.useNameLabel;tc(f,c,{labelFetcher:g,labelDataIndex:e,defaultText:function(e){return I?t.getName(e):iS(t,e)},inheritColor:b,defaultOpacity:_.opacity}),this._sizeX=n[0]/2,this._sizeY=n[1]/2;var T=f.ensureState("emphasis");T.style=o,f.ensureState("select").style=s,f.ensureState("blur").style=a;var C=null==p||!0===p?Math.max(1.1,3/this._sizeY):isFinite(p)&&p>0?+p:1;T.scaleX=this._sizeX*C,T.scaleY=this._sizeY*C,this.setSymbolScale(1),Yl(this,l,u,h)},e.prototype.setSymbolScale=function(t){this.scaleX=this.scaleY=t},e.prototype.fadeOut=function(t,e,n){var i=this.childAt(0),r=Qs(this).dataIndex,o=n&&n.animation;if(this.silent=i.silent=!0,n&&n.fadeLabel){var a=i.getTextContent();a&&vh(a,{style:{opacity:0}},e,{dataIndex:r,removeOpt:o,cb:function(){i.removeTextContent()}})}else i.removeTextContent();vh(i,{style:{opacity:0},scaleX:0,scaleY:0},e,{dataIndex:r,cb:t,removeOpt:o})},e.getSymbolSize=function(t,e){return Hy(t.getItemVisual(e,"symbolSize"))},e}(zr);function aS(t,e){this.parent.drift(t,e)}function sS(t,e,n,i){return e&&!isNaN(e[0])&&!isNaN(e[1])&&!(i.isIgnore&&i.isIgnore(n))&&!(i.clipShape&&!i.clipShape.contain(e[0],e[1]))&&"none"!==t.getItemVisual(n,"symbol")}function lS(t){return null==t||q(t)||(t={isIgnore:t}),t||{}}function uS(t){var e=t.hostModel,n=e.getModel("emphasis");return{emphasisItemStyle:n.getModel("itemStyle").getItemStyle(),blurItemStyle:e.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:e.getModel(["select","itemStyle"]).getItemStyle(),focus:n.get("focus"),blurScope:n.get("blurScope"),emphasisDisabled:n.get("disabled"),hoverScale:n.get("scale"),labelStatesModels:ec(e),cursorStyle:e.get("cursor")}}var hS=function(){function t(t){this.group=new zr,this._SymbolCtor=t||oS}return t.prototype.updateData=function(t,e){this._progressiveEls=null,e=lS(e);var n=this.group,i=t.hostModel,r=this._data,o=this._SymbolCtor,a=e.disableAnimation,s=uS(t),l={disableAnimation:a},u=e.getSymbolPoint||function(e){return t.getItemLayout(e)};r||n.removeAll(),t.diff(r).add((function(i){var r=u(i);if(sS(t,r,i,e)){var a=new o(t,i,s,l);a.setPosition(r),t.setItemGraphicEl(i,a),n.add(a)}})).update((function(h,c){var p=r.getItemGraphicEl(c),d=u(h);if(sS(t,d,h,e)){var f=t.getItemVisual(h,"symbol")||"circle",g=p&&p.getSymbolType&&p.getSymbolType();if(!p||g&&g!==f)n.remove(p),(p=new o(t,h,s,l)).setPosition(d);else{p.updateData(t,h,s,l);var y={x:d[0],y:d[1]};a?p.attr(y):fh(p,y,i)}n.add(p),t.setItemGraphicEl(h,p)}else n.remove(p)})).remove((function(t){var e=r.getItemGraphicEl(t);e&&e.fadeOut((function(){n.remove(e)}),i)})).execute(),this._getSymbolPoint=u,this._data=t},t.prototype.updateLayout=function(){var t=this,e=this._data;e&&e.eachItemGraphicEl((function(e,n){var i=t._getSymbolPoint(n);e.setPosition(i),e.markRedraw()}))},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=uS(t),this._data=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e,n){function i(t){t.isGroup||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[],n=lS(n);for(var r=t.start;r0?n=i[0]:i[1]<0&&(n=i[1]);return n}(r,n),a=i.dim,s=r.dim,l=e.mapDimension(s),u=e.mapDimension(a),h="x"===s||"radius"===s?1:0,c=z(t.dimensions,(function(t){return e.mapDimension(t)})),p=!1,d=e.getCalculationInfo("stackResultDimension");return gx(e,c[0])&&(p=!0,c[0]=d),gx(e,c[1])&&(p=!0,c[1]=d),{dataDimsForPoint:c,valueStart:o,valueAxisDim:s,baseAxisDim:a,stacked:!!p,valueDim:l,baseDim:u,baseDataOffset:h,stackedOverDimension:e.getCalculationInfo("stackedOverDimension")}}function pS(t,e,n,i){var r=NaN;t.stacked&&(r=n.get(n.getCalculationInfo("stackedOverDimension"),i)),isNaN(r)&&(r=t.valueStart);var o=t.baseDataOffset,a=[];return a[o]=n.get(t.baseDim,i),a[1-o]=r,e.dataToPoint(a)}var dS=Math.min,fS=Math.max;function gS(t,e){return isNaN(t)||isNaN(e)}function yS(t,e,n,i,r,o,a,s,l){for(var u,h,c,p,d,f,g=n,y=0;y=r||g<0)break;if(gS(v,m)){if(l){g+=o;continue}break}if(g===n)t[o>0?"moveTo":"lineTo"](v,m),c=v,p=m;else{var x=v-u,_=m-h;if(x*x+_*_<.5){g+=o;continue}if(a>0){for(var b=g+o,w=e[2*b],S=e[2*b+1];w===v&&S===m&&y=i||gS(w,S))d=v,f=m;else{T=w-u,C=S-h;var k=v-u,L=w-v,P=m-h,O=S-m,R=void 0,N=void 0;if("x"===s){var E=T>0?1:-1;d=v-E*(R=Math.abs(k))*a,f=m,D=v+E*(N=Math.abs(L))*a,A=m}else if("y"===s){var z=C>0?1:-1;d=v,f=m-z*(R=Math.abs(P))*a,D=v,A=m+z*(N=Math.abs(O))*a}else R=Math.sqrt(k*k+P*P),d=v-T*a*(1-(I=(N=Math.sqrt(L*L+O*O))/(N+R))),f=m-C*a*(1-I),A=m+C*a*I,D=dS(D=v+T*a*I,fS(w,v)),A=dS(A,fS(S,m)),D=fS(D,dS(w,v)),f=m-(C=(A=fS(A,dS(S,m)))-m)*R/N,d=dS(d=v-(T=D-v)*R/N,fS(u,v)),f=dS(f,fS(h,m)),D=v+(T=v-(d=fS(d,dS(u,v))))*N/R,A=m+(C=m-(f=fS(f,dS(h,m))))*N/R}t.bezierCurveTo(c,p,d,f,v,m),c=D,p=A}else t.lineTo(v,m)}u=v,h=m,g+=o}return y}var vS=function(){this.smooth=0,this.smoothConstraint=!0},mS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polyline",n}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new vS},e.prototype.buildPath=function(t,e){var n=e.points,i=0,r=n.length/2;if(e.connectNulls){for(;r>0&&gS(n[2*r-2],n[2*r-1]);r--);for(;i=0){var y=a?(h-i)*g+i:(u-n)*g+n;return a?[t,y]:[y,t]}n=u,i=h;break;case o.C:u=r[l++],h=r[l++],c=r[l++],p=r[l++],d=r[l++],f=r[l++];var v=a?_n(n,u,c,d,t,s):_n(i,h,p,f,t,s);if(v>0)for(var m=0;m=0){y=a?mn(i,h,p,f,x):mn(n,u,c,d,x);return a?[t,y]:[y,t]}}n=d,i=f}}},e}(Is),xS=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e}(vS),_S=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polygon",n}return n(e,t),e.prototype.getDefaultShape=function(){return new xS},e.prototype.buildPath=function(t,e){var n=e.points,i=e.stackedOnPoints,r=0,o=n.length/2,a=e.smoothMonotone;if(e.connectNulls){for(;o>0&&gS(n[2*o-2],n[2*o-1]);o--);for(;r=0;a--){var s=t.getDimensionInfo(i[a].dimension);if("x"===(r=s&&s.coordDim)||"y"===r){o=i[a];break}}if(o){var l=e.getAxis(r),u=z(o.stops,(function(t){return{coord:l.toGlobalCoord(l.dataToCoord(t.value)),color:t.color}})),h=u.length,c=o.outerColors.slice();h&&u[0].coord>u[h-1].coord&&(u.reverse(),c.reverse());var p=function(t,e){var n,i,r=[],o=t.length;function a(t,e,n){var i=t.coord;return{coord:n,color:ti((n-i)/(e.coord-i),[t.color,e.color])}}for(var s=0;se){i?r.push(a(i,l,e)):n&&r.push(a(n,l,0),a(n,l,e));break}n&&(r.push(a(n,l,0)),n=null),r.push(l),i=l}}return r}(u,"x"===r?n.getWidth():n.getHeight()),d=p.length;if(!d&&h)return u[0].coord<0?c[1]?c[1]:u[h-1].color:c[0]?c[0]:u[0].color;var f=p[0].coord-10,g=p[d-1].coord+10,y=g-f;if(y<.001)return"transparent";E(p,(function(t){t.offset=(t.coord-f)/y})),p.push({offset:d?p[d-1].offset:.5,color:c[1]||"transparent"}),p.unshift({offset:d?p[0].offset:.5,color:c[0]||"transparent"});var v=new nh(0,0,0,0,p,!0);return v[r]=f,v[r+"2"]=g,v}}}function LS(t,e,n){var i=t.get("showAllSymbol"),r="auto"===i;if(!i||r){var o=n.getAxesByScale("ordinal")[0];if(o&&(!r||!function(t,e){var n=t.getExtent(),i=Math.abs(n[1]-n[0])/t.scale.count();isNaN(i)&&(i=0);for(var r=e.count(),o=Math.max(1,Math.round(r/5)),a=0;ai)return!1;return!0}(o,e))){var a=e.mapDimension(o.dim),s={};return E(o.getViewLabels(),(function(t){var e=o.scale.getRawOrdinalNumber(t.tickValue);s[e]=1})),function(t){return!s.hasOwnProperty(e.get(a,t))}}}}function PS(t,e){return[t[2*e],t[2*e+1]]}function OS(t){if(t.get(["endLabel","show"]))return!0;for(var e=0;e0&&"bolder"===t.get(["emphasis","lineStyle","width"]))&&(d.getState("emphasis").style.lineWidth=+d.style.lineWidth+1);Qs(d).seriesIndex=t.seriesIndex,Yl(d,L,P,O);var R=DS(t.get("smooth")),N=t.get("smoothMonotone");if(d.setShape({smooth:R,smoothMonotone:N,connectNulls:w}),f){var E=a.getCalculationInfo("stackedOnSeries"),z=0;f.useStyle(k(l.getAreaStyle(),{fill:C,opacity:.7,lineJoin:"bevel",decal:a.getVisual("style").decal})),E&&(z=DS(E.get("smooth"))),f.setShape({smooth:R,stackedOnSmooth:z,smoothMonotone:N,connectNulls:w}),jl(f,t,"areaStyle"),Qs(f).seriesIndex=t.seriesIndex,Yl(f,L,P,O)}var V=function(t){i._changePolyState(t)};a.eachItemGraphicEl((function(t){t&&(t.onHoverStateChange=V)})),this._polyline.onHoverStateChange=V,this._data=a,this._coordSys=r,this._stackedOnPoints=_,this._points=u,this._step=T,this._valueOrigin=m,t.get("triggerLineEvent")&&(this.packEventData(t,d),f&&this.packEventData(t,f))},e.prototype.packEventData=function(t,e){Qs(e).eventData={componentType:"series",componentSubType:"line",componentIndex:t.componentIndex,seriesIndex:t.seriesIndex,seriesName:t.name,seriesType:"line"}},e.prototype.highlight=function(t,e,n,i){var r=t.getData(),o=Po(r,i);if(this._changePolyState("emphasis"),!(o instanceof Array)&&null!=o&&o>=0){var a=r.getLayout("points"),s=r.getItemGraphicEl(o);if(!s){var l=a[2*o],u=a[2*o+1];if(isNaN(l)||isNaN(u))return;if(this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(l,u))return;var h=t.get("zlevel")||0,c=t.get("z")||0;(s=new oS(r,o)).x=l,s.y=u,s.setZ(h,c);var p=s.getSymbolPath().getTextContent();p&&(p.zlevel=h,p.z=c,p.z2=this._polyline.z2+1),s.__temp=!0,r.setItemGraphicEl(o,s),s.stopSymbolAnimation(!0),this.group.add(s)}s.highlight()}else kg.prototype.highlight.call(this,t,e,n,i)},e.prototype.downplay=function(t,e,n,i){var r=t.getData(),o=Po(r,i);if(this._changePolyState("normal"),null!=o&&o>=0){var a=r.getItemGraphicEl(o);a&&(a.__temp?(r.setItemGraphicEl(o,null),this.group.remove(a)):a.downplay())}else kg.prototype.downplay.call(this,t,e,n,i)},e.prototype._changePolyState=function(t){var e=this._polygon;Il(this._polyline,t),e&&Il(e,t)},e.prototype._newPolyline=function(t){var e=this._polyline;return e&&this._lineGroup.remove(e),e=new mS({shape:{points:t},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(e),this._polyline=e,e},e.prototype._newPolygon=function(t,e){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new _S({shape:{points:t,stackedOnPoints:e},segmentIgnoreThreshold:2}),this._lineGroup.add(n),this._polygon=n,n},e.prototype._initSymbolLabelAnimation=function(t,e,n){var i,r,o=e.getBaseAxis(),a=o.inverse;"cartesian2d"===e.type?(i=o.isHorizontal(),r=!1):"polar"===e.type&&(i="angle"===o.dim,r=!0);var s=t.hostModel,l=s.get("animationDuration");X(l)&&(l=l(null));var u=s.get("animationDelay")||0,h=X(u)?u(null):u;t.eachItemGraphicEl((function(t,o){var s=t;if(s){var c=[t.x,t.y],p=void 0,d=void 0,f=void 0;if(n)if(r){var g=n,y=e.pointToCoord(c);i?(p=g.startAngle,d=g.endAngle,f=-y[1]/180*Math.PI):(p=g.r0,d=g.r,f=y[0])}else{var v=n;i?(p=v.x,d=v.x+v.width,f=t.x):(p=v.y+v.height,d=v.y,f=t.y)}var m=d===p?0:(f-p)/(d-p);a&&(m=1-m);var x=X(u)?u(o):l*m+h,_=s.getSymbolPath(),b=_.getTextContent();s.attr({scaleX:0,scaleY:0}),s.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:x}),b&&b.animateFrom({style:{opacity:0}},{duration:300,delay:x}),_.disableLabelAnimation=!0}}))},e.prototype._initOrUpdateEndLabel=function(t,e,n){var i=t.getModel("endLabel");if(OS(t)){var r=t.getData(),o=this._polyline,a=r.getLayout("points");if(!a)return o.removeTextContent(),void(this._endLabel=null);var s=this._endLabel;s||((s=this._endLabel=new Fs({z2:200})).ignoreClip=!0,o.setTextContent(this._endLabel),o.disableLabelAnimation=!0);var l=function(t){for(var e,n,i=t.length/2;i>0&&(e=t[2*i-2],n=t[2*i-1],isNaN(e)||isNaN(n));i--);return i-1}(a);l>=0&&(tc(o,ec(t,"endLabel"),{inheritColor:n,labelFetcher:t,labelDataIndex:l,defaultText:function(t,e,n){return null!=n?rS(r,n):iS(r,t)},enableTextSetter:!0},function(t,e){var n=e.getBaseAxis(),i=n.isHorizontal(),r=n.inverse,o=i?r?"right":"left":"center",a=i?"middle":r?"top":"bottom";return{normal:{align:t.get("align")||o,verticalAlign:t.get("verticalAlign")||a}}}(i,e)),o.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},e.prototype._endLabelOnDuring=function(t,e,n,i,r,o,a){var s=this._endLabel,l=this._polyline;if(s){t<1&&null==i.originalX&&(i.originalX=s.x,i.originalY=s.y);var u=n.getLayout("points"),h=n.hostModel,c=h.get("connectNulls"),p=o.get("precision"),d=o.get("distance")||0,f=a.getBaseAxis(),g=f.isHorizontal(),y=f.inverse,v=e.shape,m=y?g?v.x:v.y+v.height:g?v.x+v.width:v.y,x=(g?d:0)*(y?-1:1),_=(g?0:-d)*(y?-1:1),b=g?"x":"y",w=function(t,e,n){for(var i,r,o=t.length/2,a="x"===n?0:1,s=0,l=-1,u=0;u=e||i>=e&&r<=e){l=u;break}s=u,i=r}else i=r;return{range:[s,l],t:(e-i)/(r-i)}}(u,m,b),S=w.range,M=S[1]-S[0],I=void 0;if(M>=1){if(M>1&&!c){var T=PS(u,S[0]);s.attr({x:T[0]+x,y:T[1]+_}),r&&(I=h.getRawValue(S[0]))}else{(T=l.getPointOn(m,b))&&s.attr({x:T[0]+x,y:T[1]+_});var C=h.getRawValue(S[0]),D=h.getRawValue(S[1]);r&&(I=Wo(n,p,C,D,w.t))}i.lastFrameIndex=S[0]}else{var A=1===t||i.lastFrameIndex>0?S[0]:0;T=PS(u,A);r&&(I=h.getRawValue(A)),s.attr({x:T[0]+x,y:T[1]+_})}if(r){var k=uc(s);"function"==typeof k.setLabelText&&k.setLabelText(I)}}},e.prototype._doUpdateAnimation=function(t,e,n,i,r,o,a){var s=this._polyline,l=this._polygon,u=t.hostModel,h=function(t,e,n,i,r,o,a,s){for(var l=function(t,e){var n=[];return e.diff(t).add((function(t){n.push({cmd:"+",idx:t})})).update((function(t,e){n.push({cmd:"=",idx:e,idx1:t})})).remove((function(t){n.push({cmd:"-",idx:t})})).execute(),n}(t,e),u=[],h=[],c=[],p=[],d=[],f=[],g=[],y=cS(r,e,a),v=t.getLayout("points")||[],m=e.getLayout("points")||[],x=0;x3e3||l&&CS(p,f)>3e3)return s.stopAnimation(),s.setShape({points:d}),void(l&&(l.stopAnimation(),l.setShape({points:d,stackedOnPoints:f})));s.shape.__points=h.current,s.shape.points=c;var g={shape:{points:d}};h.current!==c&&(g.shape.__points=h.next),s.stopAnimation(),fh(s,g,u),l&&(l.setShape({points:c,stackedOnPoints:p}),l.stopAnimation(),fh(l,{shape:{stackedOnPoints:f}},u),s.shape.points!==l.shape.points&&(l.shape.points=s.shape.points));for(var y=[],v=h.status,m=0;me&&(e=t[n]);return isFinite(e)?e:NaN},min:function(t){for(var e=1/0,n=0;n10&&"cartesian2d"===o.type&&r){var s=o.getBaseAxis(),l=o.getOtherAxis(s),u=s.getExtent(),h=n.getDevicePixelRatio(),c=Math.abs(u[1]-u[0])*(h||1),p=Math.round(a/c);if(isFinite(p)&&p>1){"lttb"===r&&t.setData(i.lttbDownSample(i.mapDimension(l.dim),1/p));var d=void 0;U(r)?d=zS[r]:X(r)&&(d=r),d&&t.setData(i.downSample(i.mapDimension(l.dim),1/p,d,VS))}}}}}var FS=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(t,e){return vx(null,this,{useEncodeDefaulter:!0})},e.prototype.getMarkerPosition=function(t,e,n){var i=this.coordinateSystem;if(i&&i.clampData){var r=i.clampData(t),o=i.dataToPoint(r);if(n)E(i.getAxes(),(function(t,n){if("category"===t.type&&null!=e){var i=t.getTicksCoords(),a=r[n],s="x1"===e[n]||"y1"===e[n];if(s&&(a+=1),i.length<2)return;if(2===i.length)return void(o[n]=t.toGlobalCoord(t.getExtent()[s?1:0]));for(var l=void 0,u=void 0,h=1,c=0;ca){u=(p+l)/2;break}1===c&&(h=d-i[0].tickValue)}null==u&&(l?l&&(u=i[i.length-1].coord):u=i[0].coord),o[n]=t.toGlobalCoord(u)}}));else{var a=this.getData(),s=a.getLayout("offset"),l=a.getLayout("size"),u=i.getBaseAxis().isHorizontal()?0:1;o[u]+=s+l/2}return o}return[NaN,NaN]},e.type="series.__base_bar__",e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod"},e}(mg);mg.registerClass(FS);var GS=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(){return vx(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},e.prototype.getProgressive=function(){return!!this.get("large")&&this.get("progressive")},e.prototype.getProgressiveThreshold=function(){var t=this.get("progressiveThreshold"),e=this.get("largeThreshold");return e>t&&(t=e),t},e.prototype.brushSelector=function(t,e,n){return n.rect(e.getItemLayout(t))},e.type="series.bar",e.dependencies=["grid","polar"],e.defaultOption=Cc(FS.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:"#212121"}},realtimeSort:!1}),e}(FS),WS=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0},HS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="sausage",n}return n(e,t),e.prototype.getDefaultShape=function(){return new WS},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=Math.max(e.r0||0,0),o=Math.max(e.r,0),a=.5*(o-r),s=r+a,l=e.startAngle,u=e.endAngle,h=e.clockwise,c=2*Math.PI,p=h?u-lo)return!0;o=u}return!1},e.prototype._isOrderDifferentInView=function(t,e){for(var n=e.scale,i=n.getExtent(),r=Math.max(0,i[0]),o=Math.min(i[1],n.getOrdinalMeta().categories.length-1);r<=o;++r)if(t.ordinalNumbers[r]!==n.getRawOrdinalNumber(r))return!0},e.prototype._updateSortWithinSameData=function(t,e,n,i){if(this._isOrderChangedWithinSameData(t,e,n)){var r=this._dataSort(t,n,e);this._isOrderDifferentInView(r,n)&&(this._removeOnRenderedListener(i),i.dispatchAction({type:"changeAxisOrder",componentType:n.dim+"Axis",axisId:n.index,sortInfo:r}))}},e.prototype._dispatchInitSort=function(t,e,n){var i=e.baseAxis,r=this._dataSort(t,i,(function(n){return t.get(t.mapDimension(e.otherAxis.dim),n)}));n.dispatchAction({type:"changeAxisOrder",componentType:i.dim+"Axis",isInitSort:!0,axisId:i.index,sortInfo:r})},e.prototype.remove=function(t,e){this._clear(this._model),this._removeOnRenderedListener(e)},e.prototype.dispose=function(t,e){this._removeOnRenderedListener(e)},e.prototype._removeOnRenderedListener=function(t){this._onRendered&&(t.getZr().off("rendered",this._onRendered),this._onRendered=null)},e.prototype._clear=function(t){var e=this.group,n=this._data;t&&t.isAnimationEnabled()&&n&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],n.eachItemGraphicEl((function(e){xh(e,t,Qs(e).dataIndex)}))):e.removeAll(),this._data=null,this._isFirstFrame=!0},e.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},e.type="bar",e}(kg),KS={cartesian2d:function(t,e){var n=e.width<0?-1:1,i=e.height<0?-1:1;n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height);var r=t.x+t.width,o=t.y+t.height,a=ZS(e.x,t.x),s=jS(e.x+e.width,r),l=ZS(e.y,t.y),u=jS(e.y+e.height,o),h=sr?s:a,e.y=c&&l>o?u:l,e.width=h?0:s-a,e.height=c?0:u-l,n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height),h||c},polar:function(t,e){var n=e.r0<=e.r?1:-1;if(n<0){var i=e.r;e.r=e.r0,e.r0=i}var r=jS(e.r,t.r),o=ZS(e.r0,t.r0);e.r=r,e.r0=o;var a=r-o<0;if(n<0){i=e.r;e.r=e.r0,e.r0=i}return a}},$S={cartesian2d:function(t,e,n,i,r,o,a,s,l){var u=new zs({shape:A({},i),z2:1});(u.__dataIndex=n,u.name="item",o)&&(u.shape[r?"height":"width"]=0);return u},polar:function(t,e,n,i,r,o,a,s,l){var u=!r&&l?HS:zu,h=new u({shape:i,z2:1});h.name="item";var c,p,d=rM(r);if(h.calculateTextPosition=(c=d,p=({isRoundCap:u===HS}||{}).isRoundCap,function(t,e,n){var i=e.position;if(!i||i instanceof Array)return Tr(t,e,n);var r=c(i),o=null!=e.distance?e.distance:5,a=this.shape,s=a.cx,l=a.cy,u=a.r,h=a.r0,d=(u+h)/2,f=a.startAngle,g=a.endAngle,y=(f+g)/2,v=p?Math.abs(u-h)/2:0,m=Math.cos,x=Math.sin,_=s+u*m(f),b=l+u*x(f),w="left",S="top";switch(r){case"startArc":_=s+(h-o)*m(y),b=l+(h-o)*x(y),w="center",S="top";break;case"insideStartArc":_=s+(h+o)*m(y),b=l+(h+o)*x(y),w="center",S="bottom";break;case"startAngle":_=s+d*m(f)+YS(f,o+v,!1),b=l+d*x(f)+XS(f,o+v,!1),w="right",S="middle";break;case"insideStartAngle":_=s+d*m(f)+YS(f,-o+v,!1),b=l+d*x(f)+XS(f,-o+v,!1),w="left",S="middle";break;case"middle":_=s+d*m(y),b=l+d*x(y),w="center",S="middle";break;case"endArc":_=s+(u+o)*m(y),b=l+(u+o)*x(y),w="center",S="bottom";break;case"insideEndArc":_=s+(u-o)*m(y),b=l+(u-o)*x(y),w="center",S="top";break;case"endAngle":_=s+d*m(g)+YS(g,o+v,!0),b=l+d*x(g)+XS(g,o+v,!0),w="left",S="middle";break;case"insideEndAngle":_=s+d*m(g)+YS(g,-o+v,!0),b=l+d*x(g)+XS(g,-o+v,!0),w="right",S="middle";break;default:return Tr(t,e,n)}return(t=t||{}).x=_,t.y=b,t.align=w,t.verticalAlign=S,t}),o){var f=r?"r":"endAngle",g={};h.shape[f]=r?i.r0:i.startAngle,g[f]=i[f],(s?fh:gh)(h,{shape:g},o)}return h}};function JS(t,e,n,i,r,o,a,s){var l,u;o?(u={x:i.x,width:i.width},l={y:i.y,height:i.height}):(u={y:i.y,height:i.height},l={x:i.x,width:i.width}),s||(a?fh:gh)(n,{shape:l},e,r,null),(a?fh:gh)(n,{shape:u},e?t.baseAxis.model:null,r)}function QS(t,e){for(var n=0;n0?1:-1,a=i.height>0?1:-1;return{x:i.x+o*r/2,y:i.y+a*r/2,width:i.width-o*r,height:i.height-a*r}},polar:function(t,e,n){var i=t.getItemLayout(e);return{cx:i.cx,cy:i.cy,r0:i.r0,r:i.r,startAngle:i.startAngle,endAngle:i.endAngle,clockwise:i.clockwise}}};function rM(t){return function(t){var e=t?"Arc":"Angle";return function(t){switch(t){case"start":case"insideStart":case"end":case"insideEnd":return t+e;default:return t}}}(t)}function oM(t,e,n,i,r,o,a,s){var l=e.getItemVisual(n,"style");if(s){if(!o.get("roundCap")){var u=t.shape;A(u,US(i.getModel("itemStyle"),u,!0)),t.setShape(u)}}else{var h=i.get(["itemStyle","borderRadius"])||0;t.setShape("r",h)}t.useStyle(l);var c=i.getShallow("cursor");c&&t.attr("cursor",c);var p=s?a?r.r>=r.r0?"endArc":"startArc":r.endAngle>=r.startAngle?"endAngle":"startAngle":a?r.height>=0?"bottom":"top":r.width>=0?"right":"left",d=ec(i);tc(t,d,{labelFetcher:o,labelDataIndex:n,defaultText:iS(o.getData(),n),inheritColor:l.fill,defaultOpacity:l.opacity,defaultOutsidePosition:p});var f=t.getTextContent();if(s&&f){var g=i.get(["label","position"]);t.textConfig.inside="middle"===g||null,function(t,e,n,i){if(j(i))t.setTextConfig({rotation:i});else if(Y(e))t.setTextConfig({rotation:0});else{var r,o=t.shape,a=o.clockwise?o.startAngle:o.endAngle,s=o.clockwise?o.endAngle:o.startAngle,l=(a+s)/2,u=n(e);switch(u){case"startArc":case"insideStartArc":case"middle":case"insideEndArc":case"endArc":r=l;break;case"startAngle":case"insideStartAngle":r=a;break;case"endAngle":case"insideEndAngle":r=s;break;default:return void t.setTextConfig({rotation:0})}var h=1.5*Math.PI-r;"middle"===u&&h>Math.PI/2&&h<1.5*Math.PI&&(h-=Math.PI),t.setTextConfig({rotation:h})}}(t,"outside"===g?p:g,rM(a),i.get(["label","rotate"]))}hc(f,d,o.getRawValue(n),(function(t){return rS(e,t)}));var y=i.getModel(["emphasis"]);Yl(t,y.get("focus"),y.get("blurScope"),y.get("disabled")),jl(t,i),function(t){return null!=t.startAngle&&null!=t.endAngle&&t.startAngle===t.endAngle}(r)&&(t.style.fill="none",t.style.stroke="none",E(t.states,(function(t){t.style&&(t.style.fill=t.style.stroke="none")})))}var aM=function(){},sM=function(t){function e(e){var n=t.call(this,e)||this;return n.type="largeBar",n}return n(e,t),e.prototype.getDefaultShape=function(){return new aM},e.prototype.buildPath=function(t,e){for(var n=e.points,i=this.baseDimIdx,r=1-this.baseDimIdx,o=[],a=[],s=this.barWidth,l=0;l=s[0]&&e<=s[0]+l[0]&&n>=s[1]&&n<=s[1]+l[1])return a[h]}return-1}(this,t.offsetX,t.offsetY);Qs(this).dataIndex=e>=0?e:null}),30,!1);function hM(t,e,n){if(MS(n,"cartesian2d")){var i=e,r=n.getArea();return{x:t?i.x:r.x,y:t?r.y:i.y,width:t?i.width:r.width,height:t?r.height:i.height}}var o=e;return{cx:(r=n.getArea()).cx,cy:r.cy,r0:t?r.r0:o.r0,r:t?r.r:o.r,startAngle:t?o.startAngle:0,endAngle:t?o.endAngle:2*Math.PI}}var cM=2*Math.PI,pM=Math.PI/180;function dM(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}function fM(t,e){var n=dM(t,e),i=t.get("center"),r=t.get("radius");Y(r)||(r=[0,r]);var o,a,s=Ur(n.width,e.getWidth()),l=Ur(n.height,e.getHeight()),u=Math.min(s,l),h=Ur(r[0],u/2),c=Ur(r[1],u/2),p=t.coordinateSystem;if(p){var d=p.dataToPoint(i);o=d[0]||0,a=d[1]||0}else Y(i)||(i=[i,i]),o=Ur(i[0],s)+n.x,a=Ur(i[1],l)+n.y;return{cx:o,cy:a,r0:h,r:c}}function gM(t,e,n){e.eachSeriesByType(t,(function(t){var e=t.getData(),i=e.mapDimension("value"),r=dM(t,n),o=fM(t,n),a=o.cx,s=o.cy,l=o.r,u=o.r0,h=-t.get("startAngle")*pM,c=t.get("minAngle")*pM,p=0;e.each(i,(function(t){!isNaN(t)&&p++}));var d=e.getSum(i),f=Math.PI/(d||p)*2,g=t.get("clockwise"),y=t.get("roseType"),v=t.get("stillShowZeroSum"),m=e.getDataExtent(i);m[0]=0;var x=cM,_=0,b=h,w=g?1:-1;if(e.setLayout({viewRect:r,r:l}),e.each(i,(function(t,n){var i;if(isNaN(t))e.setItemLayout(n,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:g,cx:a,cy:s,r0:u,r:y?NaN:l});else{(i="area"!==y?0===d&&v?f:t*f:cM/p)n?a:o,h=Math.abs(l.label.y-n);if(h>=u.maxY){var c=l.label.x-e-l.len2*r,p=i+l.len,f=Math.abs(c)t.unconstrainedWidth?null:d:null;i.setStyle("width",f)}var g=i.getBoundingRect();o.width=g.width;var y=(i.style.margin||0)+2.1;o.height=g.height+y,o.y-=(o.height-c)/2}}}function _M(t){return"center"===t.position}function bM(t){var e,n,i=t.getData(),r=[],o=!1,a=(t.get("minShowLabelAngle")||0)*vM,s=i.getLayout("viewRect"),l=i.getLayout("r"),u=s.width,h=s.x,c=s.y,p=s.height;function d(t){t.ignore=!0}i.each((function(t){var s=i.getItemGraphicEl(t),c=s.shape,p=s.getTextContent(),f=s.getTextGuideLine(),g=i.getItemModel(t),y=g.getModel("label"),v=y.get("position")||g.get(["emphasis","label","position"]),m=y.get("distanceToLabelLine"),x=y.get("alignTo"),_=Ur(y.get("edgeDistance"),u),b=y.get("bleedMargin"),w=g.getModel("labelLine"),S=w.get("length");S=Ur(S,u);var M=w.get("length2");if(M=Ur(M,u),Math.abs(c.endAngle-c.startAngle)0?"right":"left":k>0?"left":"right"}var B=Math.PI,F=0,G=y.get("rotate");if(j(G))F=G*(B/180);else if("center"===v)F=0;else if("radial"===G||!0===G){F=k<0?-A+B:-A}else if("tangential"===G&&"outside"!==v&&"outer"!==v){var W=Math.atan2(k,L);W<0&&(W=2*B+W),L>0&&(W=B+W),F=W-B}if(o=!!F,p.x=I,p.y=T,p.rotation=F,p.setStyle({verticalAlign:"middle"}),P){p.setStyle({align:D});var H=p.states.select;H&&(H.x+=p.x,H.y+=p.y)}else{var Y=p.getBoundingRect().clone();Y.applyTransform(p.getComputedTransform());var X=(p.style.margin||0)+2.1;Y.y-=X/2,Y.height+=X,r.push({label:p,labelLine:f,position:v,len:S,len2:M,minTurnAngle:w.get("minTurnAngle"),maxSurfaceAngle:w.get("maxSurfaceAngle"),surfaceNormal:new De(k,L),linePoints:C,textAlign:D,labelDistance:m,labelAlignTo:x,edgeDistance:_,bleedMargin:b,rect:Y,unconstrainedWidth:Y.width,labelStyleWidth:p.style.width})}s.setTextConfig({inside:P})}})),!o&&t.get("avoidLabelOverlap")&&function(t,e,n,i,r,o,a,s){for(var l=[],u=[],h=Number.MAX_VALUE,c=-Number.MAX_VALUE,p=0;p0){for(var l=o.getItemLayout(0),u=1;isNaN(l&&l.startAngle)&&u=n.r0}},e.type="pie",e}(kg);function MM(t,e,n){e=Y(e)&&{coordDimensions:e}||A({encodeDefine:t.getEncode()},e);var i=t.getSource(),r=ux(i,e).dimensions,o=new lx(r,t);return o.initData(i,n),o}var IM=function(){function t(t,e){this._getDataWithEncodedVisual=t,this._getRawData=e}return t.prototype.getAllNames=function(){var t=this._getRawData();return t.mapArray(t.getName)},t.prototype.containName=function(t){return this._getRawData().indexOfName(t)>=0},t.prototype.indexOfName=function(t){return this._getDataWithEncodedVisual().indexOfName(t)},t.prototype.getItemVisual=function(t,e){return this._getDataWithEncodedVisual().getItemVisual(t,e)},t}(),TM=Oo(),CM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new IM(W(this.getData,this),W(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.mergeOption=function(){t.prototype.mergeOption.apply(this,arguments)},e.prototype.getInitialData=function(){return MM(this,{coordDimensions:["value"],encodeDefaulter:H(Jp,this)})},e.prototype.getDataParams=function(e){var n=this.getData(),i=TM(n),r=i.seats;if(!r){var o=[];n.each(n.mapDimension("value"),(function(t){o.push(t)})),r=i.seats=Jr(o,n.hostModel.get("percentPrecision"))}var a=t.prototype.getDataParams.call(this,e);return a.percent=r[e]||0,a.$vars.push("percent"),a},e.prototype._defaultLabelLine=function(t){wo(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.type="series.pie",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",bleedMargin:10,distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:15,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},e}(mg);var DM=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){return vx(null,this,{useEncodeDefaulter:!0})},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?5e3:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?1e4:this.get("progressiveThreshold"):t},e.prototype.brushSelector=function(t,e,n){return n.point(e.getItemLayout(t))},e.prototype.getZLevelKey=function(){return this.getData().count()>this.getProgressiveThreshold()?this.id:""},e.type="series.scatter",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8},emphasis:{scale:!0},clip:!0,select:{itemStyle:{borderColor:"#212121"}},universalTransition:{divideShape:"clone"}},e}(mg),AM=function(){},kM=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.getDefaultShape=function(){return new AM},e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.buildPath=function(t,e){var n,i=e.points,r=e.size,o=this.symbolProxy,a=o.shape,s=t.getContext?t.getContext():t,l=s&&r[0]<4,u=this.softClipShape;if(l)this._ctx=s;else{for(this._ctx=null,n=this._off;n=0;s--){var l=2*s,u=i[l]-o/2,h=i[l+1]-a/2;if(t>=u&&e>=h&&t<=u+o&&e<=h+a)return s}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape,n=e.points,i=e.size,r=i[0],o=i[1],a=1/0,s=1/0,l=-1/0,u=-1/0,h=0;h=0&&(l.dataIndex=n+(t.startIndex||0))}))},t.prototype.remove=function(){this._clear()},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),PM=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).updateData(i,{clipShape:this._getClipShape(t)}),this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).incrementalPrepareUpdate(i),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._symbolDraw.incrementalUpdate(t,e.getData(),{clipShape:this._getClipShape(e)}),this._finished=t.end===e.getData().count()},e.prototype.updateTransform=function(t,e,n){var i=t.getData();if(this.group.dirty(),!this._finished||i.count()>1e4)return{update:!0};var r=ES("").reset(t,e,n);r.progress&&r.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout(i)},e.prototype.eachRendered=function(t){this._symbolDraw&&this._symbolDraw.eachRendered(t)},e.prototype._getClipShape=function(t){var e=t.coordinateSystem,n=e&&e.getArea&&e.getArea();return t.get("clip",!0)?n:null},e.prototype._updateSymbolDraw=function(t,e){var n=this._symbolDraw,i=e.pipelineContext.large;return n&&i===this._isLargeDraw||(n&&n.remove(),n=this._symbolDraw=i?new LM:new hS,this._isLargeDraw=i,this.group.removeAll()),this.group.add(n.group),n},e.prototype.remove=function(t,e){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},e.prototype.dispose=function(){},e.type="scatter",e}(kg),OM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.type="grid",e.dependencies=["xAxis","yAxis"],e.layoutMode="box",e.defaultOption={show:!1,z:0,left:"10%",top:60,right:"10%",bottom:70,containLabel:!1,backgroundColor:"rgba(0,0,0,0)",borderWidth:1,borderColor:"#ccc"},e}(Rp),RM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",zo).models[0]},e.type="cartesian2dAxis",e}(Rp);R(RM,I_);var NM={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:!0,onZeroAxisIndex:null,lineStyle:{color:"#6E7079",width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15]},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12},splitLine:{show:!0,lineStyle:{color:["#E0E6F1"],width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.2)","rgba(210,219,238,0.2)"]}}},EM=C({boundaryGap:!0,deduplication:null,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto"},axisLabel:{interval:"auto"}},NM),zM=C({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:"#F4F7FD",width:1}}},NM),VM={category:EM,value:zM,time:C({splitNumber:6,axisLabel:{showMinLabel:!1,showMaxLabel:!1,rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},zM),log:k({logBase:10},zM)},BM={value:1,category:1,time:1,log:1};function FM(t,e,i,r){E(BM,(function(o,a){var s=C(C({},VM[a],!0),r,!0),l=function(t){function i(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e+"Axis."+a,n}return n(i,t),i.prototype.mergeDefaultAndTheme=function(t,e){var n=Ap(this),i=n?Lp(t):{};C(t,e.getTheme().get(a+"Axis")),C(t,this.getDefaultOption()),t.type=GM(t),n&&kp(t,i,n)},i.prototype.optionUpdated=function(){"category"===this.option.type&&(this.__ordinalMeta=_x.createByAxisModel(this))},i.prototype.getCategories=function(t){var e=this.option;if("category"===e.type)return t?e.data:this.__ordinalMeta.categories},i.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},i.type=e+"Axis."+a,i.defaultOption=s,i}(i);t.registerComponentModel(l)})),t.registerSubTypeDefaulter(e+"Axis",GM)}function GM(t){return t.type||(t.data?"category":"value")}var WM=function(){function t(t){this.type="cartesian",this._dimList=[],this._axes={},this.name=t||""}return t.prototype.getAxis=function(t){return this._axes[t]},t.prototype.getAxes=function(){return z(this._dimList,(function(t){return this._axes[t]}),this)},t.prototype.getAxesByScale=function(t){return t=t.toLowerCase(),B(this.getAxes(),(function(e){return e.scale.type===t}))},t.prototype.addAxis=function(t){var e=t.dim;this._axes[e]=t,this._dimList.push(e)},t}(),HM=["x","y"];function YM(t){return"interval"===t.type||"time"===t.type}var XM=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="cartesian2d",e.dimensions=HM,e}return n(e,t),e.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var t=this.getAxis("x").scale,e=this.getAxis("y").scale;if(YM(t)&&YM(e)){var n=t.getExtent(),i=e.getExtent(),r=this.dataToPoint([n[0],i[0]]),o=this.dataToPoint([n[1],i[1]]),a=n[1]-n[0],s=i[1]-i[0];if(a&&s){var l=(o[0]-r[0])/a,u=(o[1]-r[1])/s,h=r[0]-n[0]*l,c=r[1]-i[0]*u,p=this._transform=[l,0,0,u,h,c];this._invTransform=Ie([],p)}}},e.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},e.prototype.containPoint=function(t){var e=this.getAxis("x"),n=this.getAxis("y");return e.contain(e.toLocalCoord(t[0]))&&n.contain(n.toLocalCoord(t[1]))},e.prototype.containData=function(t){return this.getAxis("x").containData(t[0])&&this.getAxis("y").containData(t[1])},e.prototype.containZone=function(t,e){var n=this.dataToPoint(t),i=this.dataToPoint(e),r=this.getArea(),o=new ze(n[0],n[1],i[0]-n[0],i[1]-n[1]);return r.intersect(o)},e.prototype.dataToPoint=function(t,e,n){n=n||[];var i=t[0],r=t[1];if(this._transform&&null!=i&&isFinite(i)&&null!=r&&isFinite(r))return Wt(n,t,this._transform);var o=this.getAxis("x"),a=this.getAxis("y");return n[0]=o.toGlobalCoord(o.dataToCoord(i,e)),n[1]=a.toGlobalCoord(a.dataToCoord(r,e)),n},e.prototype.clampData=function(t,e){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,r=n.getExtent(),o=i.getExtent(),a=n.parse(t[0]),s=i.parse(t[1]);return(e=e||[])[0]=Math.min(Math.max(Math.min(r[0],r[1]),a),Math.max(r[0],r[1])),e[1]=Math.min(Math.max(Math.min(o[0],o[1]),s),Math.max(o[0],o[1])),e},e.prototype.pointToData=function(t,e){var n=[];if(this._invTransform)return Wt(n,t,this._invTransform);var i=this.getAxis("x"),r=this.getAxis("y");return n[0]=i.coordToData(i.toLocalCoord(t[0]),e),n[1]=r.coordToData(r.toLocalCoord(t[1]),e),n},e.prototype.getOtherAxis=function(t){return this.getAxis("x"===t.dim?"y":"x")},e.prototype.getArea=function(){var t=this.getAxis("x").getGlobalExtent(),e=this.getAxis("y").getGlobalExtent(),n=Math.min(t[0],t[1]),i=Math.min(e[0],e[1]),r=Math.max(t[0],t[1])-n,o=Math.max(e[0],e[1])-i;return new ze(n,i,r,o)},e}(WM),UM=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.index=0,a.type=r||"value",a.position=o||"bottom",a}return n(e,t),e.prototype.isHorizontal=function(){var t=this.position;return"top"===t||"bottom"===t},e.prototype.getGlobalExtent=function(t){var e=this.getExtent();return e[0]=this.toGlobalCoord(e[0]),e[1]=this.toGlobalCoord(e[1]),t&&e[0]>e[1]&&e.reverse(),e},e.prototype.pointToData=function(t,e){return this.coordToData(this.toLocalCoord(t["x"===this.dim?0:1]),e)},e.prototype.setCategorySortInfo=function(t){if("category"!==this.type)return!1;this.model.option.categorySortInfo=t,this.scale.setSortInfo(t)},e}(nb);function ZM(t,e,n){n=n||{};var i=t.coordinateSystem,r=e.axis,o={},a=r.getAxesOnZeroOf()[0],s=r.position,l=a?"onZero":s,u=r.dim,h=i.getRect(),c=[h.x,h.x+h.width,h.y,h.y+h.height],p={left:0,right:1,top:0,bottom:1,onZero:2},d=e.get("offset")||0,f="x"===u?[c[2]-d,c[3]+d]:[c[0]-d,c[1]+d];if(a){var g=a.toGlobalCoord(a.dataToCoord(0));f[p.onZero]=Math.max(Math.min(g,f[1]),f[0])}o.position=["y"===u?f[p[l]]:c[0],"x"===u?f[p[l]]:c[3]],o.rotation=Math.PI/2*("x"===u?0:1);o.labelDirection=o.tickDirection=o.nameDirection={top:-1,bottom:1,left:-1,right:1}[s],o.labelOffset=a?f[p[s]]-f[p.onZero]:0,e.get(["axisTick","inside"])&&(o.tickDirection=-o.tickDirection),it(n.labelInside,e.get(["axisLabel","inside"]))&&(o.labelDirection=-o.labelDirection);var y=e.get(["axisLabel","rotate"]);return o.labelRotate="top"===l?-y:y,o.z2=1,o}function jM(t){return"cartesian2d"===t.get("coordinateSystem")}function qM(t){var e={xAxisModel:null,yAxisModel:null};return E(e,(function(n,i){var r=i.replace(/Model$/,""),o=t.getReferringComponents(r,zo).models[0];e[i]=o})),e}var KM=Math.log;function $M(t,e,n){var i=Ox.prototype,r=i.getTicks.call(n),o=i.getTicks.call(n,!0),a=r.length-1,s=i.getInterval.call(n),l=y_(t,e),u=l.extent,h=l.fixMin,c=l.fixMax;if("log"===t.type){var p=KM(t.base);u=[KM(u[0])/p,KM(u[1])/p]}t.setExtent(u[0],u[1]),t.calcNiceExtent({splitNumber:a,fixMin:h,fixMax:c});var d=i.getExtent.call(t);h&&(u[0]=d[0]),c&&(u[1]=d[1]);var f=i.getInterval.call(t),g=u[0],y=u[1];if(h&&c)f=(y-g)/a;else if(h)for(y=u[0]+f*a;yu[0]&&isFinite(g)&&isFinite(u[0]);)f=Ix(f),g=u[1]-f*a;else{t.getTicks().length-1>a&&(f=Ix(f));var v=f*a;(g=Zr((y=Math.ceil(u[1]/f)*f)-v))<0&&u[0]>=0?(g=0,y=Zr(v)):y>0&&u[1]<=0&&(y=0,g=-Zr(v))}var m=(r[0].value-o[0].value)/s,x=(r[a].value-o[a].value)/s;i.setExtent.call(t,g+f*m,y+f*x),i.setInterval.call(t,f),(m||x)&&i.setNiceExtent.call(t,g+f,y-f)}var JM=function(){function t(t,e,n){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=HM,this._initCartesian(t,e,n),this.model=t}return t.prototype.getRect=function(){return this._rect},t.prototype.update=function(t,e){var n=this._axesMap;function i(t){var e,n=G(t),i=n.length;if(i){for(var r=[],o=i-1;o>=0;o--){var a=t[+n[o]],s=a.model,l=a.scale;Sx(l)&&s.get("alignTicks")&&null==s.get("interval")?r.push(a):(v_(l,s),Sx(l)&&(e=a))}r.length&&(e||v_((e=r.pop()).scale,e.model),E(r,(function(t){$M(t.scale,t.model,e.scale)})))}}this._updateScale(t,this.model),i(n.x),i(n.y);var r={};E(n.x,(function(t){tI(n,"y",t,r)})),E(n.y,(function(t){tI(n,"x",t,r)})),this.resize(this.model,e)},t.prototype.resize=function(t,e,n){var i=t.getBoxLayoutParams(),r=!n&&t.get("containLabel"),o=Cp(i,{width:e.getWidth(),height:e.getHeight()});this._rect=o;var a=this._axesList;function s(){E(a,(function(t){var e=t.isHorizontal(),n=e?[0,o.width]:[0,o.height],i=t.inverse?1:0;t.setExtent(n[i],n[1-i]),function(t,e){var n=t.getExtent(),i=n[0]+n[1];t.toGlobalCoord="x"===t.dim?function(t){return t+e}:function(t){return i-t+e},t.toLocalCoord="x"===t.dim?function(t){return t-e}:function(t){return i-t+e}}(t,e?o.x:o.y)}))}s(),r&&(E(a,(function(t){if(!t.model.get(["axisLabel","inside"])){var e=function(t){var e=t.model,n=t.scale;if(e.get(["axisLabel","show"])&&!n.isBlank()){var i,r,o=n.getExtent();r=n instanceof Lx?n.count():(i=n.getTicks()).length;var a,s=t.getLabelModel(),l=x_(t),u=1;r>40&&(u=Math.ceil(r/40));for(var h=0;h0&&i>0||n<0&&i<0)}(t)}var nI=Math.PI,iI=function(){function t(t,e){this.group=new zr,this.opt=e,this.axisModel=t,k(e,{labelOffset:0,nameDirection:1,tickDirection:1,labelDirection:1,silent:!0,handleAutoShown:function(){return!0}});var n=new zr({x:e.position[0],y:e.position[1],rotation:e.rotation});n.updateTransform(),this._transformGroup=n}return t.prototype.hasBuilder=function(t){return!!rI[t]},t.prototype.add=function(t){rI[t](this.opt,this.axisModel,this.group,this._transformGroup)},t.prototype.getGroup=function(){return this.group},t.innerTextLayout=function(t,e,n){var i,r,o=eo(e-t);return no(o)?(r=n>0?"top":"bottom",i="center"):no(o-nI)?(r=n>0?"bottom":"top",i="center"):(r="middle",i=o>0&&o0?"right":"left":n>0?"left":"right"),{rotation:o,textAlign:i,textVerticalAlign:r}},t.makeAxisEventDataBase=function(t){var e={componentType:t.mainType,componentIndex:t.componentIndex};return e[t.mainType+"Index"]=t.componentIndex,e},t.isLabelSilent=function(t){var e=t.get("tooltip");return t.get("silent")||!(t.get("triggerEvent")||e&&e.show)},t}(),rI={axisLine:function(t,e,n,i){var r=e.get(["axisLine","show"]);if("auto"===r&&t.handleAutoShown&&(r=t.handleAutoShown("axisLine")),r){var o=e.axis.getExtent(),a=i.transform,s=[o[0],0],l=[o[1],0],u=s[0]>l[0];a&&(Wt(s,s,a),Wt(l,l,a));var h=A({lineCap:"round"},e.getModel(["axisLine","lineStyle"]).getLineStyle()),c=new Zu({shape:{x1:s[0],y1:s[1],x2:l[0],y2:l[1]},style:h,strokeContainThreshold:t.strokeContainThreshold||5,silent:!0,z2:1});Rh(c.shape,c.style.lineWidth),c.anid="line",n.add(c);var p=e.get(["axisLine","symbol"]);if(null!=p){var d=e.get(["axisLine","symbolSize"]);U(p)&&(p=[p,p]),(U(d)||j(d))&&(d=[d,d]);var f=Yy(e.get(["axisLine","symbolOffset"])||0,d),g=d[0],y=d[1];E([{rotate:t.rotation+Math.PI/2,offset:f[0],r:0},{rotate:t.rotation-Math.PI/2,offset:f[1],r:Math.sqrt((s[0]-l[0])*(s[0]-l[0])+(s[1]-l[1])*(s[1]-l[1]))}],(function(e,i){if("none"!==p[i]&&null!=p[i]){var r=Wy(p[i],-g/2,-y/2,g,y,h.stroke,!0),o=e.r+e.offset,a=u?l:s;r.attr({rotation:e.rotate,x:a[0]+o*Math.cos(t.rotation),y:a[1]-o*Math.sin(t.rotation),silent:!0,z2:11}),n.add(r)}}))}}},axisTickLabel:function(t,e,n,i){var r=function(t,e,n,i){var r=n.axis,o=n.getModel("axisTick"),a=o.get("show");"auto"===a&&i.handleAutoShown&&(a=i.handleAutoShown("axisTick"));if(!a||r.scale.isBlank())return;for(var s=o.getModel("lineStyle"),l=i.tickDirection*o.get("length"),u=lI(r.getTicksCoords(),e.transform,l,k(s.getLineStyle(),{stroke:n.get(["axisLine","lineStyle","color"])}),"ticks"),h=0;hc[1]?-1:1,d=["start"===s?c[0]-p*h:"end"===s?c[1]+p*h:(c[0]+c[1])/2,sI(s)?t.labelOffset+l*h:0],f=e.get("nameRotate");null!=f&&(f=f*nI/180),sI(s)?o=iI.innerTextLayout(t.rotation,null!=f?f:t.rotation,l):(o=function(t,e,n,i){var r,o,a=eo(n-t),s=i[0]>i[1],l="start"===e&&!s||"start"!==e&&s;no(a-nI/2)?(o=l?"bottom":"top",r="center"):no(a-1.5*nI)?(o=l?"top":"bottom",r="center"):(o="middle",r=a<1.5*nI&&a>nI/2?l?"left":"right":l?"right":"left");return{rotation:a,textAlign:r,textVerticalAlign:o}}(t.rotation,s,f||0,c),null!=(a=t.axisNameAvailableWidth)&&(a=Math.abs(a/Math.sin(o.rotation)),!isFinite(a)&&(a=null)));var g=u.getFont(),y=e.get("nameTruncate",!0)||{},v=y.ellipsis,m=it(t.nameTruncateMaxWidth,y.maxWidth,a),x=new Fs({x:d[0],y:d[1],rotation:o.rotation,silent:iI.isLabelSilent(e),style:nc(u,{text:r,font:g,overflow:"truncate",width:m,ellipsis:v,fill:u.getTextColor()||e.get(["axisLine","lineStyle","color"]),align:u.get("align")||o.textAlign,verticalAlign:u.get("verticalAlign")||o.textVerticalAlign}),z2:1});if(Zh({el:x,componentModel:e,itemName:r}),x.__fullText=r,x.anid="name",e.get("triggerEvent")){var _=iI.makeAxisEventDataBase(e);_.targetType="axisName",_.name=r,Qs(x).eventData=_}i.add(x),x.updateTransform(),n.add(x),x.decomposeTransform()}}};function oI(t){t&&(t.ignore=!0)}function aI(t,e){var n=t&&t.getBoundingRect().clone(),i=e&&e.getBoundingRect().clone();if(n&&i){var r=xe([]);return Se(r,r,-t.rotation),n.applyTransform(be([],r,t.getLocalTransform())),i.applyTransform(be([],r,e.getLocalTransform())),n.intersect(i)}}function sI(t){return"middle"===t||"center"===t}function lI(t,e,n,i,r){for(var o=[],a=[],s=[],l=0;l=0||t===e}function cI(t){var e=pI(t);if(e){var n=e.axisPointerModel,i=e.axis.scale,r=n.option,o=n.get("status"),a=n.get("value");null!=a&&(a=i.parse(a));var s=dI(n);null==o&&(r.status=s?"show":"hide");var l=i.getExtent().slice();l[0]>l[1]&&l.reverse(),(null==a||a>l[1])&&(a=l[1]),a0&&!c.min?c.min=0:null!=c.min&&c.min<0&&!c.max&&(c.max=0);var p=a;null!=c.color&&(p=k({color:c.color},a));var d=C(T(c),{boundaryGap:t,splitNumber:e,scale:n,axisLine:i,axisTick:r,axisLabel:o,name:c.text,showName:s,nameLocation:"end",nameGap:u,nameTextStyle:p,triggerEvent:h},!1);if(U(l)){var f=d.name;d.name=l.replace("{value}",null!=f?f:"")}else X(l)&&(d.name=l(d.name,d));var g=new Mc(d,null,this.ecModel);return R(g,I_.prototype),g.mainType="radar",g.componentIndex=this.componentIndex,g}),this);this._indicatorModels=c},e.prototype.getIndicatorModels=function(){return this._indicatorModels},e.type="radar",e.defaultOption={z:0,center:["50%","50%"],radius:"75%",startAngle:90,axisName:{show:!0},boundaryGap:[0,0],splitNumber:5,axisNameGap:15,scale:!1,shape:"polygon",axisLine:C({lineStyle:{color:"#bbb"}},NI.axisLine),axisLabel:EI(NI.axisLabel,!1),axisTick:EI(NI.axisTick,!1),splitLine:EI(NI.splitLine,!0),splitArea:EI(NI.splitArea,!0),indicator:[]},e}(Rp),VI=["axisLine","axisTickLabel","axisName"],BI=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll(),this._buildAxes(t),this._buildSplitLineAndArea(t)},e.prototype._buildAxes=function(t){var e=t.coordinateSystem;E(z(e.getIndicatorAxes(),(function(t){var n=t.model.get("showName")?t.name:"";return new iI(t.model,{axisName:n,position:[e.cx,e.cy],rotation:t.angle,labelDirection:-1,tickDirection:-1,nameDirection:1})})),(function(t){E(VI,t.add,t),this.group.add(t.getGroup())}),this)},e.prototype._buildSplitLineAndArea=function(t){var e=t.coordinateSystem,n=e.getIndicatorAxes();if(n.length){var i=t.get("shape"),r=t.getModel("splitLine"),o=t.getModel("splitArea"),a=r.getModel("lineStyle"),s=o.getModel("areaStyle"),l=r.get("show"),u=o.get("show"),h=a.get("color"),c=s.get("color"),p=Y(h)?h:[h],d=Y(c)?c:[c],f=[],g=[];if("circle"===i)for(var y=n[0].getTicksCoords(),v=e.cx,m=e.cy,x=0;x3?1.4:r>1?1.2:1.1;ZI(this,"zoom","zoomOnMouseWheel",t,{scale:i>0?s:1/s,originX:o,originY:a,isAvailableBehavior:null})}if(n){var l=Math.abs(i);ZI(this,"scrollMove","moveOnMouseWheel",t,{scrollDelta:(i>0?1:-1)*(l>3?.4:l>1?.15:.05),originX:o,originY:a,isAvailableBehavior:null})}}},e.prototype._pinchHandler=function(t){YI(this._zr,"globalPan")||ZI(this,"zoom",null,t,{scale:t.pinchScale>1?1.1:1/1.1,originX:t.pinchX,originY:t.pinchY,isAvailableBehavior:null})},e}(jt);function ZI(t,e,n,i,r){t.pointerChecker&&t.pointerChecker(i,r.originX,r.originY)&&(de(i.event),jI(t,e,n,i,r))}function jI(t,e,n,i,r){r.isAvailableBehavior=W(qI,null,n,i),t.trigger(e,r)}function qI(t,e,n){var i=n[t];return!t||i&&(!U(i)||e.event[i+"Key"])}function KI(t,e,n){var i=t.target;i.x+=e,i.y+=n,i.dirty()}function $I(t,e,n,i){var r=t.target,o=t.zoomLimit,a=t.zoom=t.zoom||1;if(a*=e,o){var s=o.min||0,l=o.max||1/0;a=Math.max(Math.min(l,a),s)}var u=a/t.zoom;t.zoom=a,r.x-=(n-r.x)*(u-1),r.y-=(i-r.y)*(u-1),r.scaleX*=u,r.scaleY*=u,r.dirty()}var JI,QI={axisPointer:1,tooltip:1,brush:1};function tT(t,e,n){var i=e.getComponentByElement(t.topTarget),r=i&&i.coordinateSystem;return i&&i!==n&&!QI.hasOwnProperty(i.mainType)&&r&&r.model!==n}function eT(t){U(t)&&(t=(new DOMParser).parseFromString(t,"text/xml"));var e=t;for(9===e.nodeType&&(e=e.firstChild);"svg"!==e.nodeName.toLowerCase()||1!==e.nodeType;)e=e.nextSibling;return e}var nT={fill:"fill",stroke:"stroke","stroke-width":"lineWidth",opacity:"opacity","fill-opacity":"fillOpacity","stroke-opacity":"strokeOpacity","stroke-dasharray":"lineDash","stroke-dashoffset":"lineDashOffset","stroke-linecap":"lineCap","stroke-linejoin":"lineJoin","stroke-miterlimit":"miterLimit","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","text-anchor":"textAlign",visibility:"visibility",display:"display"},iT=G(nT),rT={"alignment-baseline":"textBaseline","stop-color":"stopColor"},oT=G(rT),aT=function(){function t(){this._defs={},this._root=null}return t.prototype.parse=function(t,e){e=e||{};var n=eT(t);this._defsUsePending=[];var i=new zr;this._root=i;var r=[],o=n.getAttribute("viewBox")||"",a=parseFloat(n.getAttribute("width")||e.width),s=parseFloat(n.getAttribute("height")||e.height);isNaN(a)&&(a=null),isNaN(s)&&(s=null),pT(n,i,null,!0,!1);for(var l,u,h=n.firstChild;h;)this._parseNode(h,i,r,null,!1,!1),h=h.nextSibling;if(function(t,e){for(var n=0;n=4&&(l={x:parseFloat(c[0]||0),y:parseFloat(c[1]||0),width:parseFloat(c[2]),height:parseFloat(c[3])})}if(l&&null!=a&&null!=s&&(u=bT(l,{x:0,y:0,width:a,height:s}),!e.ignoreViewBox)){var p=i;(i=new zr).add(p),p.scaleX=p.scaleY=u.scale,p.x=u.x,p.y=u.y}return e.ignoreRootClip||null==a||null==s||i.setClipPath(new zs({shape:{x:0,y:0,width:a,height:s}})),{root:i,width:a,height:s,viewBoxRect:l,viewBoxTransform:u,named:r}},t.prototype._parseNode=function(t,e,n,i,r,o){var a,s=t.nodeName.toLowerCase(),l=i;if("defs"===s&&(r=!0),"text"===s&&(o=!0),"defs"===s||"switch"===s)a=e;else{if(!r){var u=JI[s];if(u&&_t(JI,s)){a=u.call(this,t,e);var h=t.getAttribute("name");if(h){var c={name:h,namedFrom:null,svgNodeTagLower:s,el:a};n.push(c),"g"===s&&(l=c)}else i&&n.push({name:i.name,namedFrom:i,svgNodeTagLower:s,el:a});e.add(a)}}var p=sT[s];if(p&&_t(sT,s)){var d=p.call(this,t),f=t.getAttribute("id");f&&(this._defs[f]=d)}}if(a&&a.isGroup)for(var g=t.firstChild;g;)1===g.nodeType?this._parseNode(g,a,n,l,r,o):3===g.nodeType&&o&&this._parseText(g,a),g=g.nextSibling},t.prototype._parseText=function(t,e){var n=new Cs({style:{text:t.textContent},silent:!0,x:this._textX||0,y:this._textY||0});hT(e,n),pT(t,n,this._defsUsePending,!1,!1),function(t,e){var n=e.__selfStyle;if(n){var i=n.textBaseline,r=i;i&&"auto"!==i?"baseline"===i?r="alphabetic":"before-edge"===i||"text-before-edge"===i?r="top":"after-edge"===i||"text-after-edge"===i?r="bottom":"central"!==i&&"mathematical"!==i||(r="middle"):r="alphabetic",t.style.textBaseline=r}var o=e.__inheritedStyle;if(o){var a=o.textAlign,s=a;a&&("middle"===a&&(s="center"),t.style.textAlign=s)}}(n,e);var i=n.style,r=i.fontSize;r&&r<9&&(i.fontSize=9,n.scaleX*=r/9,n.scaleY*=r/9);var o=(i.fontSize||i.fontFamily)&&[i.fontStyle,i.fontWeight,(i.fontSize||12)+"px",i.fontFamily||"sans-serif"].join(" ");i.font=o;var a=n.getBoundingRect();return this._textX+=a.width,e.add(n),n},t.internalField=void(JI={g:function(t,e){var n=new zr;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n},rect:function(t,e){var n=new zs;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setShape({x:parseFloat(t.getAttribute("x")||"0"),y:parseFloat(t.getAttribute("y")||"0"),width:parseFloat(t.getAttribute("width")||"0"),height:parseFloat(t.getAttribute("height")||"0")}),n.silent=!0,n},circle:function(t,e){var n=new _u;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),r:parseFloat(t.getAttribute("r")||"0")}),n.silent=!0,n},line:function(t,e){var n=new Zu;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setShape({x1:parseFloat(t.getAttribute("x1")||"0"),y1:parseFloat(t.getAttribute("y1")||"0"),x2:parseFloat(t.getAttribute("x2")||"0"),y2:parseFloat(t.getAttribute("y2")||"0")}),n.silent=!0,n},ellipse:function(t,e){var n=new wu;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),rx:parseFloat(t.getAttribute("rx")||"0"),ry:parseFloat(t.getAttribute("ry")||"0")}),n.silent=!0,n},polygon:function(t,e){var n,i=t.getAttribute("points");i&&(n=cT(i));var r=new Wu({shape:{points:n||[]},silent:!0});return hT(e,r),pT(t,r,this._defsUsePending,!1,!1),r},polyline:function(t,e){var n,i=t.getAttribute("points");i&&(n=cT(i));var r=new Yu({shape:{points:n||[]},silent:!0});return hT(e,r),pT(t,r,this._defsUsePending,!1,!1),r},image:function(t,e){var n=new ks;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setStyle({image:t.getAttribute("xlink:href")||t.getAttribute("href"),x:+t.getAttribute("x"),y:+t.getAttribute("y"),width:+t.getAttribute("width"),height:+t.getAttribute("height")}),n.silent=!0,n},text:function(t,e){var n=t.getAttribute("x")||"0",i=t.getAttribute("y")||"0",r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0";this._textX=parseFloat(n)+parseFloat(r),this._textY=parseFloat(i)+parseFloat(o);var a=new zr;return hT(e,a),pT(t,a,this._defsUsePending,!1,!0),a},tspan:function(t,e){var n=t.getAttribute("x"),i=t.getAttribute("y");null!=n&&(this._textX=parseFloat(n)),null!=i&&(this._textY=parseFloat(i));var r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0",a=new zr;return hT(e,a),pT(t,a,this._defsUsePending,!1,!0),this._textX+=parseFloat(r),this._textY+=parseFloat(o),a},path:function(t,e){var n=vu(t.getAttribute("d")||"");return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.silent=!0,n}}),t}(),sT={lineargradient:function(t){var e=parseInt(t.getAttribute("x1")||"0",10),n=parseInt(t.getAttribute("y1")||"0",10),i=parseInt(t.getAttribute("x2")||"10",10),r=parseInt(t.getAttribute("y2")||"0",10),o=new nh(e,n,i,r);return lT(t,o),uT(t,o),o},radialgradient:function(t){var e=parseInt(t.getAttribute("cx")||"0",10),n=parseInt(t.getAttribute("cy")||"0",10),i=parseInt(t.getAttribute("r")||"0",10),r=new ih(e,n,i);return lT(t,r),uT(t,r),r}};function lT(t,e){"userSpaceOnUse"===t.getAttribute("gradientUnits")&&(e.global=!0)}function uT(t,e){for(var n=t.firstChild;n;){if(1===n.nodeType&&"stop"===n.nodeName.toLocaleLowerCase()){var i=n.getAttribute("offset"),r=void 0;r=i&&i.indexOf("%")>0?parseInt(i,10)/100:i?parseFloat(i):0;var o={};_T(n,o,o);var a=o.stopColor||n.getAttribute("stop-color")||"#000000";e.colorStops.push({offset:r,color:a})}n=n.nextSibling}}function hT(t,e){t&&t.__inheritedStyle&&(e.__inheritedStyle||(e.__inheritedStyle={}),k(e.__inheritedStyle,t.__inheritedStyle))}function cT(t){for(var e=yT(t),n=[],i=0;i0;o-=2){var a=i[o],s=i[o-1],l=yT(a);switch(r=r||[1,0,0,1,0,0],s){case"translate":we(r,r,[parseFloat(l[0]),parseFloat(l[1]||"0")]);break;case"scale":Me(r,r,[parseFloat(l[0]),parseFloat(l[1]||l[0])]);break;case"rotate":Se(r,r,-parseFloat(l[0])*mT);break;case"skewX":be(r,[1,0,Math.tan(parseFloat(l[0])*mT),1,0,0],r);break;case"skewY":be(r,[1,Math.tan(parseFloat(l[0])*mT),0,1,0,0],r);break;case"matrix":r[0]=parseFloat(l[0]),r[1]=parseFloat(l[1]),r[2]=parseFloat(l[2]),r[3]=parseFloat(l[3]),r[4]=parseFloat(l[4]),r[5]=parseFloat(l[5])}}e.setLocalTransform(r)}}(t,e),_T(t,a,s),i||function(t,e,n){for(var i=0;i0,f={api:n,geo:s,mapOrGeoModel:t,data:a,isVisualEncodedByVisualMap:d,isGeo:o,transformInfoRaw:c};"geoJSON"===s.resourceType?this._buildGeoJSON(f):"geoSVG"===s.resourceType&&this._buildSVG(f),this._updateController(t,e,n),this._updateMapSelectHandler(t,l,n,i)},t.prototype._buildGeoJSON=function(t){var e=this._regionsGroupByName=yt(),n=yt(),i=this._regionsGroup,r=t.transformInfoRaw,o=t.mapOrGeoModel,a=t.data,s=t.geo.projection,l=s&&s.stream;function u(t,e){return e&&(t=e(t)),t&&[t[0]*r.scaleX+r.x,t[1]*r.scaleY+r.y]}function h(t){for(var e=[],n=!l&&s&&s.project,i=0;i=0)&&(p=r);var d=a?{normal:{align:"center",verticalAlign:"middle"}}:null;tc(e,ec(i),{labelFetcher:p,labelDataIndex:c,defaultText:n},d);var f=e.getTextContent();if(f&&(WT(f).ignore=f.ignore,e.textConfig&&a)){var g=e.getBoundingRect().clone();e.textConfig.layoutRect=g,e.textConfig.position=[(a[0]-g.x)/g.width*100+"%",(a[1]-g.y)/g.height*100+"%"]}e.disableLabelAnimation=!0}else e.removeTextContent(),e.removeTextConfig(),e.disableLabelAnimation=null}function jT(t,e,n,i,r,o){t.data?t.data.setItemGraphicEl(o,e):Qs(e).eventData={componentType:"geo",componentIndex:r.componentIndex,geoIndex:r.componentIndex,name:n,region:i&&i.option||{}}}function qT(t,e,n,i,r){t.data||Zh({el:e,componentModel:r,itemName:n,itemTooltipOption:i.get("tooltip")})}function KT(t,e,n,i,r){e.highDownSilentOnTouch=!!r.get("selectedMode");var o=i.getModel("emphasis"),a=o.get("focus");return Yl(e,a,o.get("blurScope"),o.get("disabled")),t.isGeo&&function(t,e,n){var i=Qs(t);i.componentMainType=e.mainType,i.componentIndex=e.componentIndex,i.componentHighDownName=n}(e,r,n),a}function $T(t,e,n){var i,r=[];function o(){i=[]}function a(){i.length&&(r.push(i),i=[])}var s=e({polygonStart:o,polygonEnd:a,lineStart:o,lineEnd:a,point:function(t,e){isFinite(t)&&isFinite(e)&&i.push([t,e])},sphere:function(){}});return!n&&s.polygonStart(),E(t,(function(t){s.lineStart();for(var e=0;e-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2),n},e.type="series.map",e.dependencies=["geo"],e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:null,showLegendSymbol:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,selectedMode:!0,label:{show:!1,color:"#000"},itemStyle:{borderWidth:.5,borderColor:"#444",areaColor:"#eee"},emphasis:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{areaColor:"rgba(255,215,0,0.8)"}},select:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{color:"rgba(255,215,0,0.8)"}},nameProperty:"name"},e}(mg);function tC(t){var e={};t.eachSeriesByType("map",(function(t){var n=t.getHostGeoModel(),i=n?"o"+n.id:"i"+t.getMapType();(e[i]=e[i]||[]).push(t)})),E(e,(function(t,e){for(var n,i,r,o=(n=z(t,(function(t){return t.getData()})),i=t[0].get("mapValueCalculation"),r={},E(n,(function(t){t.each(t.mapDimension("value"),(function(e,n){var i="ec-"+t.getName(n);r[i]=r[i]||[],isNaN(e)||r[i].push(e)}))})),n[0].map(n[0].mapDimension("value"),(function(t,e){for(var o="ec-"+n[0].getName(e),a=0,s=1/0,l=-1/0,u=r[o].length,h=0;h1?(d.width=p,d.height=p/x):(d.height=p,d.width=p*x),d.y=c[1]-d.height/2,d.x=c[0]-d.width/2;else{var b=t.getBoxLayoutParams();b.aspect=x,d=Cp(b,{width:v,height:m})}this.setViewRect(d.x,d.y,d.width,d.height),this.setCenter(t.get("center"),e),this.setZoom(t.get("zoom"))}R(sC,iC);var hC=function(){function t(){this.dimensions=aC}return t.prototype.create=function(t,e){var n=[];function i(t){return{nameProperty:t.get("nameProperty"),aspectScale:t.get("aspectScale"),projection:t.get("projection")}}t.eachComponent("geo",(function(t,r){var o=t.get("map"),a=new sC(o+r,o,A({nameMap:t.get("nameMap")},i(t)));a.zoomLimit=t.get("scaleLimit"),n.push(a),t.coordinateSystem=a,a.model=t,a.resize=uC,a.resize(t,e)})),t.eachSeries((function(t){if("geo"===t.get("coordinateSystem")){var e=t.get("geoIndex")||0;t.coordinateSystem=n[e]}}));var r={};return t.eachSeriesByType("map",(function(t){if(!t.getHostGeoModel()){var e=t.getMapType();r[e]=r[e]||[],r[e].push(t)}})),E(r,(function(t,r){var o=z(t,(function(t){return t.get("nameMap")})),a=new sC(r,r,A({nameMap:D(o)},i(t[0])));a.zoomLimit=it.apply(null,z(t,(function(t){return t.get("scaleLimit")}))),n.push(a),a.resize=uC,a.resize(t[0],e),E(t,(function(t){t.coordinateSystem=a,function(t,e){E(e.get("geoCoord"),(function(e,n){t.addGeoCoord(n,e)}))}(a,t)}))})),n},t.prototype.getFilledRegions=function(t,e,n,i){for(var r=(t||[]).slice(),o=yt(),a=0;a=0;){var o=e[n];o.hierNode.prelim+=i,o.hierNode.modifier+=i,r+=o.hierNode.change,i+=o.hierNode.shift+r}}(t);var o=(n[0].hierNode.prelim+n[n.length-1].hierNode.prelim)/2;r?(t.hierNode.prelim=r.hierNode.prelim+e(t,r),t.hierNode.modifier=t.hierNode.prelim-o):t.hierNode.prelim=o}else r&&(t.hierNode.prelim=r.hierNode.prelim+e(t,r));t.parentNode.hierNode.defaultAncestor=function(t,e,n,i){if(e){for(var r=t,o=t,a=o.parentNode.children[0],s=e,l=r.hierNode.modifier,u=o.hierNode.modifier,h=a.hierNode.modifier,c=s.hierNode.modifier;s=wC(s),o=SC(o),s&&o;){r=wC(r),a=SC(a),r.hierNode.ancestor=t;var p=s.hierNode.prelim+c-o.hierNode.prelim-u+i(s,o);p>0&&(IC(MC(s,t,n),t,p),u+=p,l+=p),c+=s.hierNode.modifier,u+=o.hierNode.modifier,l+=r.hierNode.modifier,h+=a.hierNode.modifier}s&&!wC(r)&&(r.hierNode.thread=s,r.hierNode.modifier+=c-l),o&&!SC(a)&&(a.hierNode.thread=o,a.hierNode.modifier+=u-h,n=t)}return n}(t,r,t.parentNode.hierNode.defaultAncestor||i[0],e)}function xC(t){var e=t.hierNode.prelim+t.parentNode.hierNode.modifier;t.setLayout({x:e},!0),t.hierNode.modifier+=t.parentNode.hierNode.modifier}function _C(t){return arguments.length?t:TC}function bC(t,e){return t-=Math.PI/2,{x:e*Math.cos(t),y:e*Math.sin(t)}}function wC(t){var e=t.children;return e.length&&t.isExpand?e[e.length-1]:t.hierNode.thread}function SC(t){var e=t.children;return e.length&&t.isExpand?e[0]:t.hierNode.thread}function MC(t,e,n){return t.hierNode.ancestor.parentNode===e.parentNode?t.hierNode.ancestor:n}function IC(t,e,n){var i=n/(e.hierNode.i-t.hierNode.i);e.hierNode.change-=i,e.hierNode.shift+=n,e.hierNode.modifier+=n,e.hierNode.prelim+=n,t.hierNode.change+=i}function TC(t,e){return t.parentNode===e.parentNode?1:2}var CC=function(){this.parentPoint=[],this.childPoints=[]},DC=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new CC},e.prototype.buildPath=function(t,e){var n=e.childPoints,i=n.length,r=e.parentPoint,o=n[0],a=n[i-1];if(1===i)return t.moveTo(r[0],r[1]),void t.lineTo(o[0],o[1]);var s=e.orient,l="TB"===s||"BT"===s?0:1,u=1-l,h=Ur(e.forkPosition,1),c=[];c[l]=r[l],c[u]=r[u]+(a[u]-r[u])*h,t.moveTo(r[0],r[1]),t.lineTo(c[0],c[1]),t.moveTo(o[0],o[1]),c[l]=o[l],t.lineTo(c[0],c[1]),c[l]=a[l],t.lineTo(c[0],c[1]),t.lineTo(a[0],a[1]);for(var p=1;pm.x)||(_-=Math.PI);var S=b?"left":"right",M=s.getModel("label"),I=M.get("rotate"),T=I*(Math.PI/180),C=y.getTextContent();C&&(y.setTextConfig({position:M.get("position")||S,rotation:null==I?-_:T,origin:"center"}),C.setStyle("verticalAlign","middle"))}var D=s.get(["emphasis","focus"]),A="relative"===D?vt(a.getAncestorsIndices(),a.getDescendantIndices()):"ancestor"===D?a.getAncestorsIndices():"descendant"===D?a.getDescendantIndices():null;A&&(Qs(n).focus=A),function(t,e,n,i,r,o,a,s){var l=e.getModel(),u=t.get("edgeShape"),h=t.get("layout"),c=t.getOrient(),p=t.get(["lineStyle","curveness"]),d=t.get("edgeForkPosition"),f=l.getModel("lineStyle").getLineStyle(),g=i.__edge;if("curve"===u)e.parentNode&&e.parentNode!==n&&(g||(g=i.__edge=new $u({shape:NC(h,c,p,r,r)})),fh(g,{shape:NC(h,c,p,o,a)},t));else if("polyline"===u)if("orthogonal"===h){if(e!==n&&e.children&&0!==e.children.length&&!0===e.isExpand){for(var y=e.children,v=[],m=0;me&&(e=i.height)}this.height=e+1},t.prototype.getNodeById=function(t){if(this.getId()===t)return this;for(var e=0,n=this.children,i=n.length;e=0&&this.hostTree.data.setItemLayout(this.dataIndex,t,e)},t.prototype.getLayout=function(){return this.hostTree.data.getItemLayout(this.dataIndex)},t.prototype.getModel=function(t){if(!(this.dataIndex<0))return this.hostTree.data.getItemModel(this.dataIndex).getModel(t)},t.prototype.getLevelModel=function(){return(this.hostTree.levelModels||[])[this.depth]},t.prototype.setVisual=function(t,e){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,t,e)},t.prototype.getVisual=function(t){return this.hostTree.data.getItemVisual(this.dataIndex,t)},t.prototype.getRawIndex=function(){return this.hostTree.data.getRawIndex(this.dataIndex)},t.prototype.getId=function(){return this.hostTree.data.getId(this.dataIndex)},t.prototype.getChildIndex=function(){if(this.parentNode){for(var t=this.parentNode.children,e=0;e=0){var i=n.getData().tree.root,r=t.targetNode;if(U(r)&&(r=i.getNodeById(r)),r&&i.contains(r))return{node:r};var o=t.targetNodeId;if(null!=o&&(r=i.getNodeById(o)))return{node:r}}}function jC(t){for(var e=[];t;)(t=t.parentNode)&&e.push(t);return e.reverse()}function qC(t,e){return P(jC(t),e)>=0}function KC(t,e){for(var n=[];t;){var i=t.dataIndex;n.push({name:t.name,dataIndex:i,value:e.getRawValue(i)}),t=t.parentNode}return n.reverse(),n}var $C=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.hasSymbolVisual=!0,e.ignoreStyleOnData=!0,e}return n(e,t),e.prototype.getInitialData=function(t){var e={name:t.name,children:t.data},n=t.leaves||{},i=new Mc(n,this,this.ecModel),r=UC.createTree(e,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=r.getNodeByDataIndex(e);return n&&n.children.length&&n.isExpand||(t.parentModel=i),t}))}));var o=0;r.eachNode("preorder",(function(t){t.depth>o&&(o=t.depth)}));var a=t.expandAndCollapse&&t.initialTreeDepth>=0?t.initialTreeDepth:o;return r.root.eachNode("preorder",(function(t){var e=t.hostTree.data.getRawDataItem(t.dataIndex);t.isExpand=e&&null!=e.collapsed?!e.collapsed:t.depth<=a})),r.data},e.prototype.getOrient=function(){var t=this.get("orient");return"horizontal"===t?t="LR":"vertical"===t&&(t="TB"),t},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.formatTooltip=function(t,e,n){for(var i=this.getData().tree,r=i.root.children[0],o=i.getNodeByDataIndex(t),a=o.getValue(),s=o.name;o&&o!==r;)s=o.parentNode.name+"."+s,o=o.parentNode;return ng("nameValue",{name:s,value:a,noValue:isNaN(a)||null==a})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=KC(i,this),n.collapsed=!i.isExpand,n},e.type="series.tree",e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"view",left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",edgeShape:"curve",edgeForkPosition:"50%",roam:!1,nodeScaleRatio:.4,center:null,zoom:1,orient:"LR",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{color:"#ccc",width:1.5,curveness:.5},itemStyle:{color:"lightsteelblue",borderWidth:1.5},label:{show:!0},animationEasing:"linear",animationDuration:700,animationDurationUpdate:500},e}(mg);function JC(t,e){for(var n,i=[t];n=i.pop();)if(e(n),n.isExpand){var r=n.children;if(r.length)for(var o=r.length-1;o>=0;o--)i.push(r[o])}}function QC(t,e){t.eachSeriesByType("tree",(function(t){!function(t,e){var n=function(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e);t.layoutInfo=n;var i=t.get("layout"),r=0,o=0,a=null;"radial"===i?(r=2*Math.PI,o=Math.min(n.height,n.width)/2,a=_C((function(t,e){return(t.parentNode===e.parentNode?1:2)/t.depth}))):(r=n.width,o=n.height,a=_C());var s=t.getData().tree.root,l=s.children[0];if(l){!function(t){var e=t;e.hierNode={defaultAncestor:null,ancestor:e,prelim:0,modifier:0,change:0,shift:0,i:0,thread:null};for(var n,i,r=[e];n=r.pop();)if(i=n.children,n.isExpand&&i.length)for(var o=i.length-1;o>=0;o--){var a=i[o];a.hierNode={defaultAncestor:null,ancestor:a,prelim:0,modifier:0,change:0,shift:0,i:o,thread:null},r.push(a)}}(s),function(t,e,n){for(var i,r=[t],o=[];i=r.pop();)if(o.push(i),i.isExpand){var a=i.children;if(a.length)for(var s=0;sh.getLayout().x&&(h=t),t.depth>c.depth&&(c=t)}));var p=u===h?1:a(u,h)/2,d=p-u.getLayout().x,f=0,g=0,y=0,v=0;if("radial"===i)f=r/(h.getLayout().x+p+d),g=o/(c.depth-1||1),JC(l,(function(t){y=(t.getLayout().x+d)*f,v=(t.depth-1)*g;var e=bC(y,v);t.setLayout({x:e.x,y:e.y,rawX:y,rawY:v},!0)}));else{var m=t.getOrient();"RL"===m||"LR"===m?(g=o/(h.getLayout().x+p+d),f=r/(c.depth-1||1),JC(l,(function(t){v=(t.getLayout().x+d)*g,y="LR"===m?(t.depth-1)*f:r-(t.depth-1)*f,t.setLayout({x:y,y:v},!0)}))):"TB"!==m&&"BT"!==m||(f=r/(h.getLayout().x+p+d),g=o/(c.depth-1||1),JC(l,(function(t){y=(t.getLayout().x+d)*f,v="TB"===m?(t.depth-1)*g:o-(t.depth-1)*g,t.setLayout({x:y,y:v},!0)})))}}}(t,e)}))}function tD(t){t.eachSeriesByType("tree",(function(t){var e=t.getData();e.tree.eachNode((function(t){var n=t.getModel().getModel("itemStyle").getItemStyle();A(e.ensureUniqueItemVisual(t.dataIndex,"style"),n)}))}))}var eD=["treemapZoomToNode","treemapRender","treemapMove"];function nD(t){var e=t.getData().tree,n={};e.eachNode((function(e){for(var i=e;i&&i.depth>1;)i=i.parentNode;var r=ud(t.ecModel,i.name||i.dataIndex+"",n);e.setVisual("decal",r)}))}var iD=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.preventUsingHoverLayer=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};rD(n);var i=t.levels||[],r=this.designatedVisualItemStyle={},o=new Mc({itemStyle:r},this,e);i=t.levels=function(t,e){var n,i,r=bo(e.get("color")),o=bo(e.get(["aria","decal","decals"]));if(!r)return;t=t||[],E(t,(function(t){var e=new Mc(t),r=e.get("color"),o=e.get("decal");(e.get(["itemStyle","color"])||r&&"none"!==r)&&(n=!0),(e.get(["itemStyle","decal"])||o&&"none"!==o)&&(i=!0)}));var a=t[0]||(t[0]={});n||(a.color=r.slice());!i&&o&&(a.decal=o.slice());return t}(i,e);var a=z(i||[],(function(t){return new Mc(t,o,e)}),this),s=UC.createTree(n,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=s.getNodeByDataIndex(e),i=n?a[n.depth]:null;return t.parentModel=i||o,t}))}));return s.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.formatTooltip=function(t,e,n){var i=this.getData(),r=this.getRawValue(t);return ng("nameValue",{name:i.getName(t),value:r})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=KC(i,this),n.treePathInfo=n.treeAncestors,n},e.prototype.setLayoutInfo=function(t){this.layoutInfo=this.layoutInfo||{},A(this.layoutInfo,t)},e.prototype.mapIdToIndex=function(t){var e=this._idIndexMap;e||(e=this._idIndexMap=yt(),this._idIndexMapCount=0);var n=e.get(t);return null==n&&e.set(t,n=this._idIndexMapCount++),n},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){nD(this)},e.type="series.treemap",e.layoutMode="box",e.defaultOption={progressive:0,left:"center",top:"middle",width:"80%",height:"80%",sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.1024,roam:!0,nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",top:"bottom",emptyItemWidth:25,itemStyle:{color:"rgba(0,0,0,0.7)",textStyle:{color:"#fff"}},emphasis:{itemStyle:{color:"rgba(0,0,0,0.9)"}}},label:{show:!0,distance:0,padding:5,position:"inside",color:"#fff",overflow:"truncate"},upperLabel:{show:!1,position:[0,"50%"],height:20,overflow:"truncate",verticalAlign:"middle"},itemStyle:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:"#fff",borderColorSaturation:null},emphasis:{upperLabel:{show:!0,position:[0,"50%"],overflow:"truncate",verticalAlign:"middle"}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]},e}(mg);function rD(t){var e=0;E(t.children,(function(t){rD(t);var n=t.value;Y(n)&&(n=n[0]),e+=n}));var n=t.value;Y(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),Y(t.value)?t.value[0]=n:t.value=n}var oD=function(){function t(t){this.group=new zr,t.add(this.group)}return t.prototype.render=function(t,e,n,i){var r=t.getModel("breadcrumb"),o=this.group;if(o.removeAll(),r.get("show")&&n){var a=r.getModel("itemStyle"),s=r.getModel("emphasis"),l=a.getModel("textStyle"),u=s.getModel(["itemStyle","textStyle"]),h={pos:{left:r.get("left"),right:r.get("right"),top:r.get("top"),bottom:r.get("bottom")},box:{width:e.getWidth(),height:e.getHeight()},emptyItemWidth:r.get("emptyItemWidth"),totalWidth:0,renderList:[]};this._prepare(n,h,l),this._renderContent(t,h,a,s,l,u,i),Dp(o,h.pos,h.box)}},t.prototype._prepare=function(t,e,n){for(var i=t;i;i=i.parentNode){var r=Ao(i.getModel().get("name"),""),o=n.getTextRect(r),a=Math.max(o.width+16,e.emptyItemWidth);e.totalWidth+=a+8,e.renderList.push({node:i,text:r,width:a})}},t.prototype._renderContent=function(t,e,n,i,r,o,a){for(var s,l,u,h,c,p,d,f,g,y=0,v=e.emptyItemWidth,m=t.get(["breadcrumb","height"]),x=(s=e.pos,l=e.box,h=l.width,c=l.height,p=Ur(s.left,h),d=Ur(s.top,c),f=Ur(s.right,h),g=Ur(s.bottom,c),(isNaN(p)||isNaN(parseFloat(s.left)))&&(p=0),(isNaN(f)||isNaN(parseFloat(s.right)))&&(f=h),(isNaN(d)||isNaN(parseFloat(s.top)))&&(d=0),(isNaN(g)||isNaN(parseFloat(s.bottom)))&&(g=c),u=fp(u||0),{width:Math.max(f-p-u[1]-u[3],0),height:Math.max(g-d-u[0]-u[2],0)}),_=e.totalWidth,b=e.renderList,w=i.getModel("itemStyle").getItemStyle(),S=b.length-1;S>=0;S--){var M=b[S],I=M.node,T=M.width,C=M.text;_>x.width&&(_-=T-v,T=v,C=null);var D=new Wu({shape:{points:aD(y,0,T,m,S===b.length-1,0===S)},style:k(n.getItemStyle(),{lineJoin:"bevel"}),textContent:new Fs({style:nc(r,{text:C})}),textConfig:{position:"inside"},z2:1e5,onclick:H(a,I)});D.disableLabelAnimation=!0,D.getTextContent().ensureState("emphasis").style=nc(o,{text:C}),D.ensureState("emphasis").style=w,Yl(D,i.get("focus"),i.get("blurScope"),i.get("disabled")),this.group.add(D),sD(D,t,I),y+=T+8}},t.prototype.remove=function(){this.group.removeAll()},t}();function aD(t,e,n,i,r,o){var a=[[r?t:t-5,e],[t+n,e],[t+n,e+i],[r?t:t-5,e+i]];return!o&&a.splice(2,0,[t+n+5,e+i/2]),!r&&a.push([t,e+i/2]),a}function sD(t,e,n){Qs(t).eventData={componentType:"series",componentSubType:"treemap",componentIndex:e.componentIndex,seriesIndex:e.seriesIndex,seriesName:e.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:n&&n.dataIndex,name:n&&n.name},treePathInfo:n&&KC(n,e)}}var lD=function(){function t(){this._storage=[],this._elExistsMap={}}return t.prototype.add=function(t,e,n,i,r){return!this._elExistsMap[t.id]&&(this._elExistsMap[t.id]=!0,this._storage.push({el:t,target:e,duration:n,delay:i,easing:r}),!0)},t.prototype.finished=function(t){return this._finishedCallback=t,this},t.prototype.start=function(){for(var t=this,e=this._storage.length,n=function(){--e<=0&&(t._storage.length=0,t._elExistsMap={},t._finishedCallback&&t._finishedCallback())},i=0,r=this._storage.length;i3||Math.abs(t.dy)>3)){var e=this.seriesModel.getData().tree.root;if(!e)return;var n=e.getLayout();if(!n)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:n.x+t.dx,y:n.y+t.dy,width:n.width,height:n.height}})}},e.prototype._onZoom=function(t){var e=t.originX,n=t.originY;if("animating"!==this._state){var i=this.seriesModel.getData().tree.root;if(!i)return;var r=i.getLayout();if(!r)return;var o=new ze(r.x,r.y,r.width,r.height),a=this.seriesModel.layoutInfo,s=[1,0,0,1,0,0];we(s,s,[-(e-=a.x),-(n-=a.y)]),Me(s,s,[t.scale,t.scale]),we(s,s,[e,n]),o.applyTransform(s),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:o.x,y:o.y,width:o.width,height:o.height}})}},e.prototype._initEvents=function(t){var e=this;t.on("click",(function(t){if("ready"===e._state){var n=e.seriesModel.get("nodeClick",!0);if(n){var i=e.findTarget(t.offsetX,t.offsetY);if(i){var r=i.node;if(r.getLayout().isLeafRoot)e._rootToNode(i);else if("zoomToNode"===n)e._zoomToNode(i);else if("link"===n){var o=r.hostTree.data.getItemModel(r.dataIndex),a=o.get("link",!0),s=o.get("target",!0)||"blank";a&&bp(a,s)}}}}}),this)},e.prototype._renderBreadcrumb=function(t,e,n){var i=this;n||(n=null!=t.get("leafDepth",!0)?{node:t.getViewRoot()}:this.findTarget(e.getWidth()/2,e.getHeight()/2))||(n={node:t.getData().tree.root}),(this._breadcrumb||(this._breadcrumb=new oD(this.group))).render(t,e,n.node,(function(e){"animating"!==i._state&&(qC(t.getViewRoot(),e)?i._rootToNode({node:e}):i._zoomToNode({node:e}))}))},e.prototype.remove=function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage={nodeGroup:[],background:[],content:[]},this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},e.prototype.dispose=function(){this._clearController()},e.prototype._zoomToNode=function(t){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype._rootToNode=function(t){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype.findTarget=function(t,e){var n;return this.seriesModel.getViewRoot().eachNode({attr:"viewChildren",order:"preorder"},(function(i){var r=this._storage.background[i.getRawIndex()];if(r){var o=r.transformCoordToLocal(t,e),a=r.shape;if(!(a.x<=o[0]&&o[0]<=a.x+a.width&&a.y<=o[1]&&o[1]<=a.y+a.height))return!1;n={node:i,offsetX:o[0],offsetY:o[1]}}}),this),n},e.type="treemap",e}(kg);var vD=E,mD=q,xD=-1,_D=function(){function t(e){var n=e.mappingMethod,i=e.type,r=this.option=T(e);this.type=i,this.mappingMethod=n,this._normalizeData=kD[n];var o=t.visualHandlers[i];this.applyVisual=o.applyVisual,this.getColorMapper=o.getColorMapper,this._normalizedToVisual=o._normalizedToVisual[n],"piecewise"===n?(bD(r),function(t){var e=t.pieceList;t.hasSpecialVisual=!1,E(e,(function(e,n){e.originIndex=n,null!=e.visual&&(t.hasSpecialVisual=!0)}))}(r)):"category"===n?r.categories?function(t){var e=t.categories,n=t.categoryMap={},i=t.visual;if(vD(e,(function(t,e){n[t]=e})),!Y(i)){var r=[];q(i)?vD(i,(function(t,e){var i=n[e];r[null!=i?i:xD]=t})):r[-1]=i,i=AD(t,r)}for(var o=e.length-1;o>=0;o--)null==i[o]&&(delete n[e[o]],e.pop())}(r):bD(r,!0):(lt("linear"!==n||r.dataExtent),bD(r))}return t.prototype.mapValueToVisual=function(t){var e=this._normalizeData(t);return this._normalizedToVisual(e,t)},t.prototype.getNormalizer=function(){return W(this._normalizeData,this)},t.listVisualTypes=function(){return G(t.visualHandlers)},t.isValidType=function(e){return t.visualHandlers.hasOwnProperty(e)},t.eachVisual=function(t,e,n){q(t)?E(t,e,n):e.call(n,t)},t.mapVisual=function(e,n,i){var r,o=Y(e)?[]:q(e)?{}:(r=!0,null);return t.eachVisual(e,(function(t,e){var a=n.call(i,t,e);r?o=a:o[e]=a})),o},t.retrieveVisuals=function(e){var n,i={};return e&&vD(t.visualHandlers,(function(t,r){e.hasOwnProperty(r)&&(i[r]=e[r],n=!0)})),n?i:null},t.prepareVisualTypes=function(t){if(Y(t))t=t.slice();else{if(!mD(t))return[];var e=[];vD(t,(function(t,n){e.push(n)})),t=e}return t.sort((function(t,e){return"color"===e&&"color"!==t&&0===t.indexOf("color")?1:-1})),t},t.dependsOn=function(t,e){return"color"===e?!(!t||0!==t.indexOf(e)):t===e},t.findPieceIndex=function(t,e,n){for(var i,r=1/0,o=0,a=e.length;ou[1]&&(u[1]=l);var h=e.get("colorMappingBy"),c={type:a.name,dataExtent:u,visual:a.range};"color"!==c.type||"index"!==h&&"id"!==h?c.mappingMethod="linear":(c.mappingMethod="category",c.loop=!0);var p=new _D(c);return PD(p).drColorMappingBy=h,p}(0,r,o,0,u,d);E(d,(function(t,e){if(t.depth>=n.length||t===n[t.depth]){var o=function(t,e,n,i,r,o){var a=A({},e);if(r){var s=r.type,l="color"===s&&PD(r).drColorMappingBy,u="index"===l?i:"id"===l?o.mapIdToIndex(n.getId()):n.getValue(t.get("visualDimension"));a[s]=r.mapValueToVisual(u)}return a}(r,u,t,e,f,i);RD(t,o,n,i)}}))}else s=ND(u),h.fill=s}}function ND(t){var e=ED(t,"color");if(e){var n=ED(t,"colorAlpha"),i=ED(t,"colorSaturation");return i&&(e=ni(e,null,null,i)),n&&(e=ii(e,n)),e}}function ED(t,e){var n=t[e];if(null!=n&&"none"!==n)return n}function zD(t,e){var n=t.get(e);return Y(n)&&n.length?{name:e,range:n}:null}var VD=Math.max,BD=Math.min,FD=it,GD=E,WD=["itemStyle","borderWidth"],HD=["itemStyle","gapWidth"],YD=["upperLabel","show"],XD=["upperLabel","height"],UD={seriesType:"treemap",reset:function(t,e,n,i){var r=n.getWidth(),o=n.getHeight(),a=t.option,s=Cp(t.getBoxLayoutParams(),{width:n.getWidth(),height:n.getHeight()}),l=a.size||[],u=Ur(FD(s.width,l[0]),r),h=Ur(FD(s.height,l[1]),o),c=i&&i.type,p=ZC(i,["treemapZoomToNode","treemapRootToNode"],t),d="treemapRender"===c||"treemapMove"===c?i.rootRect:null,f=t.getViewRoot(),g=jC(f);if("treemapMove"!==c){var y="treemapZoomToNode"===c?function(t,e,n,i,r){var o,a=(e||{}).node,s=[i,r];if(!a||a===n)return s;var l=i*r,u=l*t.option.zoomToNodeRatio;for(;o=a.parentNode;){for(var h=0,c=o.children,p=0,d=c.length;pto&&(u=to),a=o}ua[1]&&(a[1]=e)}))):a=[NaN,NaN];return{sum:i,dataExtent:a}}(e,a,s);if(0===u.sum)return t.viewChildren=[];if(u.sum=function(t,e,n,i,r){if(!i)return n;for(var o=t.get("visibleMin"),a=r.length,s=a,l=a-1;l>=0;l--){var u=r["asc"===i?a-l-1:l].getValue();u/n*ei&&(i=a));var l=t.area*t.area,u=e*e*n;return l?VD(u*i/l,l/(u*r)):1/0}function qD(t,e,n,i,r){var o=e===n.width?0:1,a=1-o,s=["x","y"],l=["width","height"],u=n[s[o]],h=e?t.area/e:0;(r||h>n[l[a]])&&(h=n[l[a]]);for(var c=0,p=t.length;ci&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;a0&&(m[0]=-m[0],m[1]=-m[1]);var _=v[0]<0?-1:1;if("start"!==i.__position&&"end"!==i.__position){var b=-Math.atan2(v[1],v[0]);u[0].8?"left":h[0]<-.8?"right":"center",p=h[1]>.8?"top":h[1]<-.8?"bottom":"middle";break;case"start":i.x=-h[0]*f+l[0],i.y=-h[1]*g+l[1],c=h[0]>.8?"right":h[0]<-.8?"left":"center",p=h[1]>.8?"bottom":h[1]<-.8?"top":"middle";break;case"insideStartTop":case"insideStart":case"insideStartBottom":i.x=f*_+l[0],i.y=l[1]+w,c=v[0]<0?"right":"left",i.originX=-f*_,i.originY=-w;break;case"insideMiddleTop":case"insideMiddle":case"insideMiddleBottom":case"middle":i.x=x[0],i.y=x[1]+w,c="center",i.originY=-w;break;case"insideEndTop":case"insideEnd":case"insideEndBottom":i.x=-f*_+u[0],i.y=u[1]+w,c=v[0]>=0?"right":"left",i.originX=f*_,i.originY=-w}i.scaleX=i.scaleY=r,i.setStyle({verticalAlign:i.__verticalAlign||p,align:i.__align||c})}}}function S(t,e){var n=t.__specifiedRotation;if(null==n){var i=a.tangentAt(e);t.attr("rotation",(1===e?-1:1)*Math.PI/2-Math.atan2(i[1],i[0]))}else t.attr("rotation",n)}},e}(zr),RA=function(){function t(t){this.group=new zr,this._LineCtor=t||OA}return t.prototype.updateData=function(t){var e=this;this._progressiveEls=null;var n=this,i=n.group,r=n._lineData;n._lineData=t,r||i.removeAll();var o=NA(t);t.diff(r).add((function(n){e._doAdd(t,n,o)})).update((function(n,i){e._doUpdate(r,t,i,n,o)})).remove((function(t){i.remove(r.getItemGraphicEl(t))})).execute()},t.prototype.updateLayout=function(){var t=this._lineData;t&&t.eachItemGraphicEl((function(e,n){e.updateLayout(t,n)}),this)},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=NA(t),this._lineData=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e){function n(t){t.isGroup||function(t){return t.animators&&t.animators.length>0}(t)||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[];for(var i=t.start;i=0?i+=u:i-=u:f>=0?i-=u:i+=u}return i}function XA(t,e){var n=[],i=Dn,r=[[],[],[]],o=[[],[]],a=[];e/=2,t.eachEdge((function(t,s){var l=t.getLayout(),u=t.getVisual("fromSymbol"),h=t.getVisual("toSymbol");l.__original||(l.__original=[Tt(l[0]),Tt(l[1])],l[2]&&l.__original.push(Tt(l[2])));var c=l.__original;if(null!=l[2]){if(It(r[0],c[0]),It(r[1],c[2]),It(r[2],c[1]),u&&"none"!==u){var p=dA(t.node1),d=YA(r,c[0],p*e);i(r[0][0],r[1][0],r[2][0],d,n),r[0][0]=n[3],r[1][0]=n[4],i(r[0][1],r[1][1],r[2][1],d,n),r[0][1]=n[3],r[1][1]=n[4]}if(h&&"none"!==h){p=dA(t.node2),d=YA(r,c[1],p*e);i(r[0][0],r[1][0],r[2][0],d,n),r[1][0]=n[1],r[2][0]=n[2],i(r[0][1],r[1][1],r[2][1],d,n),r[1][1]=n[1],r[2][1]=n[2]}It(l[0],r[0]),It(l[1],r[2]),It(l[2],r[1])}else{if(It(o[0],c[0]),It(o[1],c[1]),kt(a,o[1],o[0]),Et(a,a),u&&"none"!==u){p=dA(t.node1);At(o[0],o[0],a,p*e)}if(h&&"none"!==h){p=dA(t.node2);At(o[1],o[1],a,-p*e)}It(l[0],o[0]),It(l[1],o[1])}}))}function UA(t){return"view"===t.type}var ZA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){var n=new hS,i=new RA,r=this.group;this._controller=new UI(e.getZr()),this._controllerHost={target:r},r.add(n.group),r.add(i.group),this._symbolDraw=n,this._lineDraw=i,this._firstRender=!0},e.prototype.render=function(t,e,n){var i=this,r=t.coordinateSystem;this._model=t;var o=this._symbolDraw,a=this._lineDraw,s=this.group;if(UA(r)){var l={x:r.x,y:r.y,scaleX:r.scaleX,scaleY:r.scaleY};this._firstRender?s.attr(l):fh(s,l,t)}XA(t.getGraph(),pA(t));var u=t.getData();o.updateData(u);var h=t.getEdgeData();a.updateData(h),this._updateNodeAndLinkScale(),this._updateController(t,e,n),clearTimeout(this._layoutTimeout);var c=t.forceLayout,p=t.get(["force","layoutAnimation"]);c&&this._startForceLayoutIteration(c,p);var d=t.get("layout");u.graph.eachNode((function(e){var n=e.dataIndex,r=e.getGraphicEl(),o=e.getModel();if(r){r.off("drag").off("dragend");var a=o.get("draggable");a&&r.on("drag",(function(o){switch(d){case"force":c.warmUp(),!i._layouting&&i._startForceLayoutIteration(c,p),c.setFixed(n),u.setItemLayout(n,[r.x,r.y]);break;case"circular":u.setItemLayout(n,[r.x,r.y]),e.setLayout({fixed:!0},!0),yA(t,"symbolSize",e,[o.offsetX,o.offsetY]),i.updateLayout(t);break;default:u.setItemLayout(n,[r.x,r.y]),hA(t.getGraph(),t),i.updateLayout(t)}})).on("dragend",(function(){c&&c.setUnfixed(n)})),r.setDraggable(a,!!o.get("cursor")),"adjacency"===o.get(["emphasis","focus"])&&(Qs(r).focus=e.getAdjacentDataIndices())}})),u.graph.eachEdge((function(t){var e=t.getGraphicEl(),n=t.getModel().get(["emphasis","focus"]);e&&"adjacency"===n&&(Qs(e).focus={edge:[t.dataIndex],node:[t.node1.dataIndex,t.node2.dataIndex]})}));var f="circular"===t.get("layout")&&t.get(["circular","rotateLabel"]),g=u.getLayout("cx"),y=u.getLayout("cy");u.graph.eachNode((function(t){mA(t,f,g,y)})),this._firstRender=!1},e.prototype.dispose=function(){this._controller&&this._controller.dispose(),this._controllerHost=null},e.prototype._startForceLayoutIteration=function(t,e){var n=this;!function i(){t.step((function(t){n.updateLayout(n._model),(n._layouting=!t)&&(e?n._layoutTimeout=setTimeout(i,16):i())}))}()},e.prototype._updateController=function(t,e,n){var i=this,r=this._controller,o=this._controllerHost,a=this.group;r.setPointerChecker((function(e,i,r){var o=a.getBoundingRect();return o.applyTransform(a.transform),o.contain(i,r)&&!tT(e,n,t)})),UA(t.coordinateSystem)?(r.enable(t.get("roam")),o.zoomLimit=t.get("scaleLimit"),o.zoom=t.coordinateSystem.getZoom(),r.off("pan").off("zoom").on("pan",(function(e){KI(o,e.dx,e.dy),n.dispatchAction({seriesId:t.id,type:"graphRoam",dx:e.dx,dy:e.dy})})).on("zoom",(function(e){$I(o,e.scale,e.originX,e.originY),n.dispatchAction({seriesId:t.id,type:"graphRoam",zoom:e.scale,originX:e.originX,originY:e.originY}),i._updateNodeAndLinkScale(),XA(t.getGraph(),pA(t)),i._lineDraw.updateLayout(),n.updateLabelLayout()}))):r.disable()},e.prototype._updateNodeAndLinkScale=function(){var t=this._model,e=t.getData(),n=pA(t);e.eachItemGraphicEl((function(t,e){t&&t.setSymbolScale(n)}))},e.prototype.updateLayout=function(t){XA(t.getGraph(),pA(t)),this._symbolDraw.updateLayout(),this._lineDraw.updateLayout()},e.prototype.remove=function(t,e){this._symbolDraw&&this._symbolDraw.remove(),this._lineDraw&&this._lineDraw.remove()},e.type="graph",e}(kg);function jA(t){return"_EC_"+t}var qA=function(){function t(t){this.type="graph",this.nodes=[],this.edges=[],this._nodesMap={},this._edgesMap={},this._directed=t||!1}return t.prototype.isDirected=function(){return this._directed},t.prototype.addNode=function(t,e){t=null==t?""+e:""+t;var n=this._nodesMap;if(!n[jA(t)]){var i=new KA(t,e);return i.hostGraph=this,this.nodes.push(i),n[jA(t)]=i,i}},t.prototype.getNodeByIndex=function(t){var e=this.data.getRawIndex(t);return this.nodes[e]},t.prototype.getNodeById=function(t){return this._nodesMap[jA(t)]},t.prototype.addEdge=function(t,e,n){var i=this._nodesMap,r=this._edgesMap;if(j(t)&&(t=this.nodes[t]),j(e)&&(e=this.nodes[e]),t instanceof KA||(t=i[jA(t)]),e instanceof KA||(e=i[jA(e)]),t&&e){var o=t.id+"-"+e.id,a=new $A(t,e,n);return a.hostGraph=this,this._directed&&(t.outEdges.push(a),e.inEdges.push(a)),t.edges.push(a),t!==e&&e.edges.push(a),this.edges.push(a),r[o]=a,a}},t.prototype.getEdgeByIndex=function(t){var e=this.edgeData.getRawIndex(t);return this.edges[e]},t.prototype.getEdge=function(t,e){t instanceof KA&&(t=t.id),e instanceof KA&&(e=e.id);var n=this._edgesMap;return this._directed?n[t+"-"+e]:n[t+"-"+e]||n[e+"-"+t]},t.prototype.eachNode=function(t,e){for(var n=this.nodes,i=n.length,r=0;r=0&&t.call(e,n[r],r)},t.prototype.eachEdge=function(t,e){for(var n=this.edges,i=n.length,r=0;r=0&&n[r].node1.dataIndex>=0&&n[r].node2.dataIndex>=0&&t.call(e,n[r],r)},t.prototype.breadthFirstTraverse=function(t,e,n,i){if(e instanceof KA||(e=this._nodesMap[jA(e)]),e){for(var r="out"===n?"outEdges":"in"===n?"inEdges":"edges",o=0;o=0&&n.node2.dataIndex>=0}));for(r=0,o=i.length;r=0&&this[t][e].setItemVisual(this.dataIndex,n,i)},getVisual:function(n){return this[t][e].getItemVisual(this.dataIndex,n)},setLayout:function(n,i){this.dataIndex>=0&&this[t][e].setItemLayout(this.dataIndex,n,i)},getLayout:function(){return this[t][e].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[t][e].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[t][e].getRawIndex(this.dataIndex)}}}function QA(t,e,n,i,r){for(var o=new qA(i),a=0;a "+p)),u++)}var d,f=n.get("coordinateSystem");if("cartesian2d"===f||"polar"===f)d=vx(t,n);else{var g=xd.get(f),y=g&&g.dimensions||[];P(y,"value")<0&&y.concat(["value"]);var v=ux(t,{coordDimensions:y,encodeDefine:n.getEncode()}).dimensions;(d=new lx(v,n)).initData(t)}var m=new lx(["value"],n);return m.initData(l,s),r&&r(d,m),zC({mainData:d,struct:o,structAttr:"graph",datas:{node:d,edge:m},datasAttr:{node:"data",edge:"edgeData"}}),o.update(),o}R(KA,JA("hostGraph","data")),R($A,JA("hostGraph","edgeData"));var tk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments);var n=this;function i(){return n._categoriesData}this.legendVisualProvider=new IM(i,i),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},e.prototype.mergeDefaultAndTheme=function(e){t.prototype.mergeDefaultAndTheme.apply(this,arguments),wo(e,"edgeLabel",["show"])},e.prototype.getInitialData=function(t,e){var n,i=t.edges||t.links||[],r=t.data||t.nodes||[],o=this;if(r&&i){iA(n=this)&&(n.__curvenessList=[],n.__edgeMap={},rA(n));var a=QA(r,i,this,!0,(function(t,e){t.wrapMethod("getItemModel",(function(t){var e=o._categoriesModels[t.getShallow("category")];return e&&(e.parentModel=t.parentModel,t.parentModel=e),t}));var n=Mc.prototype.getModel;function i(t,e){var i=n.call(this,t,e);return i.resolveParentPath=r,i}function r(t){if(t&&("label"===t[0]||"label"===t[1])){var e=t.slice();return"label"===t[0]?e[0]="edgeLabel":"label"===t[1]&&(e[1]="edgeLabel"),e}return t}e.wrapMethod("getItemModel",(function(t){return t.resolveParentPath=r,t.getModel=i,t}))}));return E(a.edges,(function(t){!function(t,e,n,i){if(iA(n)){var r=oA(t,e,n),o=n.__edgeMap,a=o[aA(r)];o[r]&&!a?o[r].isForward=!0:a&&o[r]&&(a.isForward=!0,o[r].isForward=!1),o[r]=o[r]||[],o[r].push(i)}}(t.node1,t.node2,this,t.dataIndex)}),this),a.data}},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.getCategoriesData=function(){return this._categoriesData},e.prototype.formatTooltip=function(t,e,n){if("edge"===n){var i=this.getData(),r=this.getDataParams(t,n),o=i.graph.getEdgeByIndex(t),a=i.getName(o.node1.dataIndex),s=i.getName(o.node2.dataIndex),l=[];return null!=a&&l.push(a),null!=s&&l.push(s),ng("nameValue",{name:l.join(" > "),value:r.value,noValue:null==r.value})}return fg({series:this,dataIndex:t,multipleSeries:e})},e.prototype._updateCategoriesData=function(){var t=z(this.option.categories||[],(function(t){return null!=t.value?t:A({value:0},t)})),e=new lx(["value"],this);e.initData(t),this._categoriesData=e,this._categoriesModels=e.mapArray((function(t){return e.getItemModel(t)}))},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.isAnimationEnabled=function(){return t.prototype.isAnimationEnabled.call(this)&&!("force"===this.get("layout")&&this.get(["force","layoutAnimation"]))},e.type="series.graph",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={z:2,coordinateSystem:"view",legendHoverLink:!0,layout:null,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,friction:.6,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{position:"middle",distance:5},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{show:!1,formatter:"{b}"},itemStyle:{},lineStyle:{color:"#aaa",width:1,opacity:.5},emphasis:{scale:!0,label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(mg),ek={type:"graphRoam",event:"graphRoam",update:"none"};var nk=function(){this.angle=0,this.width=10,this.r=10,this.x=0,this.y=0},ik=function(t){function e(e){var n=t.call(this,e)||this;return n.type="pointer",n}return n(e,t),e.prototype.getDefaultShape=function(){return new nk},e.prototype.buildPath=function(t,e){var n=Math.cos,i=Math.sin,r=e.r,o=e.width,a=e.angle,s=e.x-n(a)*o*(o>=r/3?1:2),l=e.y-i(a)*o*(o>=r/3?1:2);a=e.angle-Math.PI/2,t.moveTo(s,l),t.lineTo(e.x+n(a)*o,e.y+i(a)*o),t.lineTo(e.x+n(e.angle)*r,e.y+i(e.angle)*r),t.lineTo(e.x-n(a)*o,e.y-i(a)*o),t.lineTo(s,l)},e}(Is);function rk(t,e){var n=null==t?"":t+"";return e&&(U(e)?n=e.replace("{value}",n):X(e)&&(n=e(t))),n}var ok=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll();var i=t.get(["axisLine","lineStyle","color"]),r=function(t,e){var n=t.get("center"),i=e.getWidth(),r=e.getHeight(),o=Math.min(i,r);return{cx:Ur(n[0],e.getWidth()),cy:Ur(n[1],e.getHeight()),r:Ur(t.get("radius"),o/2)}}(t,n);this._renderMain(t,e,n,i,r),this._data=t.getData()},e.prototype.dispose=function(){},e.prototype._renderMain=function(t,e,n,i,r){var o=this.group,a=t.get("clockwise"),s=-t.get("startAngle")/180*Math.PI,l=-t.get("endAngle")/180*Math.PI,u=t.getModel("axisLine"),h=u.get("roundCap")?HS:zu,c=u.get("show"),p=u.getModel("lineStyle"),d=p.get("width"),f=[s,l];rs(f,!a);for(var g=(l=f[1])-(s=f[0]),y=s,v=[],m=0;c&&m=t&&(0===e?0:i[e-1][0])Math.PI/2&&(V+=Math.PI):"tangential"===z?V=-M-Math.PI/2:j(z)&&(V=z*Math.PI/180),0===V?c.add(new Fs({style:nc(x,{text:O,x:N,y:E,verticalAlign:h<-.8?"top":h>.8?"bottom":"middle",align:u<-.4?"left":u>.4?"right":"center"},{inheritColor:R}),silent:!0})):c.add(new Fs({style:nc(x,{text:O,x:N,y:E,verticalAlign:"middle",align:"center"},{inheritColor:R}),silent:!0,originX:N,originY:E,rotation:V}))}if(m.get("show")&&k!==_){P=(P=m.get("distance"))?P+l:l;for(var B=0;B<=b;B++){u=Math.cos(M),h=Math.sin(M);var F=new Zu({shape:{x1:u*(f-P)+p,y1:h*(f-P)+d,x2:u*(f-S-P)+p,y2:h*(f-S-P)+d},silent:!0,style:D});"auto"===D.stroke&&F.setStyle({stroke:i((k+B/b)/_)}),c.add(F),M+=T}M-=T}else M+=I}},e.prototype._renderPointer=function(t,e,n,i,r,o,a,s,l){var u=this.group,h=this._data,c=this._progressEls,p=[],d=t.get(["pointer","show"]),f=t.getModel("progress"),g=f.get("show"),y=t.getData(),v=y.mapDimension("value"),m=+t.get("min"),x=+t.get("max"),_=[m,x],b=[o,a];function w(e,n){var i,o=y.getItemModel(e).getModel("pointer"),a=Ur(o.get("width"),r.r),s=Ur(o.get("length"),r.r),l=t.get(["pointer","icon"]),u=o.get("offsetCenter"),h=Ur(u[0],r.r),c=Ur(u[1],r.r),p=o.get("keepAspect");return(i=l?Wy(l,h-a/2,c-s,a,s,null,p):new ik({shape:{angle:-Math.PI/2,width:a,r:s,x:h,y:c}})).rotation=-(n+Math.PI/2),i.x=r.cx,i.y=r.cy,i}function S(t,e){var n=f.get("roundCap")?HS:zu,i=f.get("overlap"),a=i?f.get("width"):l/y.count(),u=i?r.r-a:r.r-(t+1)*a,h=i?r.r:r.r-t*a,c=new n({shape:{startAngle:o,endAngle:e,cx:r.cx,cy:r.cy,clockwise:s,r0:u,r:h}});return i&&(c.z2=x-y.get(v,t)%x),c}(g||d)&&(y.diff(h).add((function(e){var n=y.get(v,e);if(d){var i=w(e,o);gh(i,{rotation:-((isNaN(+n)?b[0]:Xr(n,_,b,!0))+Math.PI/2)},t),u.add(i),y.setItemGraphicEl(e,i)}if(g){var r=S(e,o),a=f.get("clip");gh(r,{shape:{endAngle:Xr(n,_,b,a)}},t),u.add(r),tl(t.seriesIndex,y.dataType,e,r),p[e]=r}})).update((function(e,n){var i=y.get(v,e);if(d){var r=h.getItemGraphicEl(n),a=r?r.rotation:o,s=w(e,a);s.rotation=a,fh(s,{rotation:-((isNaN(+i)?b[0]:Xr(i,_,b,!0))+Math.PI/2)},t),u.add(s),y.setItemGraphicEl(e,s)}if(g){var l=c[n],m=S(e,l?l.shape.endAngle:o),x=f.get("clip");fh(m,{shape:{endAngle:Xr(i,_,b,x)}},t),u.add(m),tl(t.seriesIndex,y.dataType,e,m),p[e]=m}})).execute(),y.each((function(t){var e=y.getItemModel(t),n=e.getModel("emphasis"),r=n.get("focus"),o=n.get("blurScope"),a=n.get("disabled");if(d){var s=y.getItemGraphicEl(t),l=y.getItemVisual(t,"style"),u=l.fill;if(s instanceof ks){var h=s.style;s.useStyle(A({image:h.image,x:h.x,y:h.y,width:h.width,height:h.height},l))}else s.useStyle(l),"pointer"!==s.type&&s.setColor(u);s.setStyle(e.getModel(["pointer","itemStyle"]).getItemStyle()),"auto"===s.style.fill&&s.setStyle("fill",i(Xr(y.get(v,t),_,[0,1],!0))),s.z2EmphasisLift=0,jl(s,e),Yl(s,r,o,a)}if(g){var c=p[t];c.useStyle(y.getItemVisual(t,"style")),c.setStyle(e.getModel(["progress","itemStyle"]).getItemStyle()),c.z2EmphasisLift=0,jl(c,e),Yl(c,r,o,a)}})),this._progressEls=p)},e.prototype._renderAnchor=function(t,e){var n=t.getModel("anchor");if(n.get("show")){var i=n.get("size"),r=n.get("icon"),o=n.get("offsetCenter"),a=n.get("keepAspect"),s=Wy(r,e.cx-i/2+Ur(o[0],e.r),e.cy-i/2+Ur(o[1],e.r),i,i,null,a);s.z2=n.get("showAbove")?1:0,s.setStyle(n.getModel("itemStyle").getItemStyle()),this.group.add(s)}},e.prototype._renderTitleAndDetail=function(t,e,n,i,r){var o=this,a=t.getData(),s=a.mapDimension("value"),l=+t.get("min"),u=+t.get("max"),h=new zr,c=[],p=[],d=t.isAnimationEnabled(),f=t.get(["pointer","showAbove"]);a.diff(this._data).add((function(t){c[t]=new Fs({silent:!0}),p[t]=new Fs({silent:!0})})).update((function(t,e){c[t]=o._titleEls[e],p[t]=o._detailEls[e]})).execute(),a.each((function(e){var n=a.getItemModel(e),o=a.get(s,e),g=new zr,y=i(Xr(o,[l,u],[0,1],!0)),v=n.getModel("title");if(v.get("show")){var m=v.get("offsetCenter"),x=r.cx+Ur(m[0],r.r),_=r.cy+Ur(m[1],r.r);(D=c[e]).attr({z2:f?0:2,style:nc(v,{x:x,y:_,text:a.getName(e),align:"center",verticalAlign:"middle"},{inheritColor:y})}),g.add(D)}var b=n.getModel("detail");if(b.get("show")){var w=b.get("offsetCenter"),S=r.cx+Ur(w[0],r.r),M=r.cy+Ur(w[1],r.r),I=Ur(b.get("width"),r.r),T=Ur(b.get("height"),r.r),C=t.get(["progress","show"])?a.getItemVisual(e,"style").fill:y,D=p[e],A=b.get("formatter");D.attr({z2:f?0:2,style:nc(b,{x:S,y:M,text:rk(o,A),width:isNaN(I)?null:I,height:isNaN(T)?null:T,align:"center",verticalAlign:"middle"},{inheritColor:C})}),hc(D,{normal:b},o,(function(t){return rk(t,A)})),d&&cc(D,e,a,t,{getFormattedLabel:function(t,e,n,i,r,a){return rk(a?a.interpolatedValue:o,A)}}),g.add(D)}h.add(g)})),this.group.add(h),this._titleEls=c,this._detailEls=p},e.type="gauge",e}(kg),ak=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="itemStyle",n}return n(e,t),e.prototype.getInitialData=function(t,e){return MM(this,["value"])},e.type="series.gauge",e.defaultOption={z:2,colorBy:"data",center:["50%","50%"],legendHoverLink:!0,radius:"75%",startAngle:225,endAngle:-45,clockwise:!0,min:0,max:100,splitNumber:10,axisLine:{show:!0,roundCap:!1,lineStyle:{color:[[1,"#E6EBF8"]],width:10}},progress:{show:!1,overlap:!0,width:10,roundCap:!1,clip:!0},splitLine:{show:!0,length:10,distance:10,lineStyle:{color:"#63677A",width:3,type:"solid"}},axisTick:{show:!0,splitNumber:5,length:6,distance:10,lineStyle:{color:"#63677A",width:1,type:"solid"}},axisLabel:{show:!0,distance:15,color:"#464646",fontSize:12,rotate:0},pointer:{icon:null,offsetCenter:[0,0],show:!0,showAbove:!0,length:"60%",width:6,keepAspect:!1},anchor:{show:!1,showAbove:!1,size:6,icon:"circle",offsetCenter:[0,0],keepAspect:!1,itemStyle:{color:"#fff",borderWidth:0,borderColor:"#5470c6"}},title:{show:!0,offsetCenter:[0,"20%"],color:"#464646",fontSize:16,valueAnimation:!1},detail:{show:!0,backgroundColor:"rgba(0,0,0,0)",borderWidth:0,borderColor:"#ccc",width:100,height:null,padding:[5,10],offsetCenter:[0,"40%"],color:"#464646",fontSize:30,fontWeight:"bold",lineHeight:30,valueAnimation:!1}},e}(mg);var sk=["itemStyle","opacity"],lk=function(t){function e(e,n){var i=t.call(this)||this,r=i,o=new Yu,a=new Fs;return r.setTextContent(a),i.setTextGuideLine(o),i.updateData(e,n,!0),i}return n(e,t),e.prototype.updateData=function(t,e,n){var i=this,r=t.hostModel,o=t.getItemModel(e),a=t.getItemLayout(e),s=o.getModel("emphasis"),l=o.get(sk);l=null==l?1:l,n||_h(i),i.useStyle(t.getItemVisual(e,"style")),i.style.lineJoin="round",n?(i.setShape({points:a.points}),i.style.opacity=0,gh(i,{style:{opacity:l}},r,e)):fh(i,{style:{opacity:l},shape:{points:a.points}},r,e),jl(i,o),this._updateLabel(t,e),Yl(this,s.get("focus"),s.get("blurScope"),s.get("disabled"))},e.prototype._updateLabel=function(t,e){var n=this,i=this.getTextGuideLine(),r=n.getTextContent(),o=t.hostModel,a=t.getItemModel(e),s=t.getItemLayout(e).label,l=t.getItemVisual(e,"style"),u=l.fill;tc(r,ec(a),{labelFetcher:t.hostModel,labelDataIndex:e,defaultOpacity:l.opacity,defaultText:t.getName(e)},{normal:{align:s.textAlign,verticalAlign:s.verticalAlign}}),n.setTextConfig({local:!0,inside:!!s.inside,insideStroke:u,outsideFill:u});var h=s.linePoints;i.setShape({points:h}),n.textGuideLineConfig={anchor:h?new De(h[0][0],h[0][1]):null},fh(r,{style:{x:s.x,y:s.y}},o,e),r.attr({rotation:s.rotation,originX:s.x,originY:s.y,z2:10}),Tb(n,Cb(a),{stroke:u})},e}(Wu),uk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.ignoreLabelLineUpdate=!0,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this._data,o=this.group;i.diff(r).add((function(t){var e=new lk(i,t);i.setItemGraphicEl(t,e),o.add(e)})).update((function(t,e){var n=r.getItemGraphicEl(e);n.updateData(i,t),o.add(n),i.setItemGraphicEl(t,n)})).remove((function(e){xh(r.getItemGraphicEl(e),t,e)})).execute(),this._data=i},e.prototype.remove=function(){this.group.removeAll(),this._data=null},e.prototype.dispose=function(){},e.type="funnel",e}(kg),hk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new IM(W(this.getData,this),W(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.getInitialData=function(t,e){return MM(this,{coordDimensions:["value"],encodeDefaulter:H(Jp,this)})},e.prototype._defaultLabelLine=function(t){wo(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.prototype.getDataParams=function(e){var n=this.getData(),i=t.prototype.getDataParams.call(this,e),r=n.mapDimension("value"),o=n.getSum(r);return i.percent=o?+(n.get(r,e)/o*100).toFixed(2):0,i.$vars.push("percent"),i},e.type="series.funnel",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",left:80,top:60,right:80,bottom:60,minSize:"0%",maxSize:"100%",sort:"descending",orient:"vertical",gap:0,funnelAlign:"center",label:{show:!0,position:"outer"},labelLine:{show:!0,length:20,lineStyle:{width:1}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(mg);function ck(t,e){t.eachSeriesByType("funnel",(function(t){var n=t.getData(),i=n.mapDimension("value"),r=t.get("sort"),o=function(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e),a=t.get("orient"),s=o.width,l=o.height,u=function(t,e){for(var n=t.mapDimension("value"),i=t.mapArray(n,(function(t){return t})),r=[],o="ascending"===e,a=0,s=t.count();a5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(t){if(!this._mouseDownPoint&&Mk(this,"mousemove")){var e=this._model,n=e.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function Mk(t,e){var n=t._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===e}var Ik=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){t.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var e=this.option;t&&C(e,t,!0),this._initDimensions()},e.prototype.contains=function(t,e){var n=t.get("parallelIndex");return null!=n&&e.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){E(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(e){t.hasOwnProperty(e)&&(this.option[e]=t[e])}),this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],e=this.parallelAxisIndex=[];E(B(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(t){return(t.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){t.push("dim"+n.get("dim")),e.push(n.componentIndex)}))},e.type="parallel",e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(Rp),Tk=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.type=r||"value",a.axisIndex=o,a}return n(e,t),e.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},e}(nb);function Ck(t,e,n,i,r,o){t=t||0;var a=n[1]-n[0];if(null!=r&&(r=Ak(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i){var s=Math.abs(e[1]-e[0]);s=Ak(s,[0,a]),r=o=Ak(s,[r,o]),i=0}e[0]=Ak(e[0],n),e[1]=Ak(e[1],n);var l=Dk(e,i);e[i]+=t;var u,h=r||0,c=n.slice();return l.sign<0?c[0]+=h:c[1]-=h,e[i]=Ak(e[i],c),u=Dk(e,i),null!=r&&(u.sign!==l.sign||u.spano&&(e[1-i]=e[i]+u.sign*o),e}function Dk(t,e){var n=t[e]-t[1-e];return{span:Math.abs(n),sign:n>0?-1:n<0?1:e?-1:1}}function Ak(t,e){return Math.min(null!=e[1]?e[1]:1/0,Math.max(null!=e[0]?e[0]:-1/0,t))}var kk=E,Lk=Math.min,Pk=Math.max,Ok=Math.floor,Rk=Math.ceil,Nk=Zr,Ek=Math.PI,zk=function(){function t(t,e,n){this.type="parallel",this._axesMap=yt(),this._axesLayout={},this.dimensions=t.dimensions,this._model=t,this._init(t,e,n)}return t.prototype._init=function(t,e,n){var i=t.dimensions,r=t.parallelAxisIndex;kk(i,(function(t,n){var i=r[n],o=e.getComponent("parallelAxis",i),a=this._axesMap.set(t,new Tk(t,m_(o),[0,0],o.get("type"),i)),s="category"===a.type;a.onBand=s&&o.get("boundaryGap"),a.inverse=o.get("inverse"),o.axis=a,a.model=o,a.coordinateSystem=o.coordinateSystem=this}),this)},t.prototype.update=function(t,e){this._updateAxesFromSeries(this._model,t)},t.prototype.containPoint=function(t){var e=this._makeLayoutInfo(),n=e.axisBase,i=e.layoutBase,r=e.pixelDimIndex,o=t[1-r],a=t[r];return o>=n&&o<=n+e.axisLength&&a>=i&&a<=i+e.layoutLength},t.prototype.getModel=function(){return this._model},t.prototype._updateAxesFromSeries=function(t,e){e.eachSeries((function(n){if(t.contains(n,e)){var i=n.getData();kk(this.dimensions,(function(t){var e=this._axesMap.get(t);e.scale.unionExtentFromData(i,i.mapDimension(t)),v_(e.scale,e.model)}),this)}}),this)},t.prototype.resize=function(t,e){this._rect=Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()}),this._layoutAxes()},t.prototype.getRect=function(){return this._rect},t.prototype._makeLayoutInfo=function(){var t,e=this._model,n=this._rect,i=["x","y"],r=["width","height"],o=e.get("layout"),a="horizontal"===o?0:1,s=n[r[a]],l=[0,s],u=this.dimensions.length,h=Vk(e.get("axisExpandWidth"),l),c=Vk(e.get("axisExpandCount")||0,[0,u]),p=e.get("axisExpandable")&&u>3&&u>c&&c>1&&h>0&&s>0,d=e.get("axisExpandWindow");d?(t=Vk(d[1]-d[0],l),d[1]=d[0]+t):(t=Vk(h*(c-1),l),(d=[h*(e.get("axisExpandCenter")||Ok(u/2))-t/2])[1]=d[0]+t);var f=(s-t)/(u-c);f<3&&(f=0);var g=[Ok(Nk(d[0]/h,1))+1,Rk(Nk(d[1]/h,1))-1],y=f/h*d[0];return{layout:o,pixelDimIndex:a,layoutBase:n[i[a]],layoutLength:s,axisBase:n[i[1-a]],axisLength:n[r[1-a]],axisExpandable:p,axisExpandWidth:h,axisCollapseWidth:f,axisExpandWindow:d,axisCount:u,winInnerIndices:g,axisExpandWindow0Pos:y}},t.prototype._layoutAxes=function(){var t=this._rect,e=this._axesMap,n=this.dimensions,i=this._makeLayoutInfo(),r=i.layout;e.each((function(t){var e=[0,i.axisLength],n=t.inverse?1:0;t.setExtent(e[n],e[1-n])})),kk(n,(function(e,n){var o=(i.axisExpandable?Fk:Bk)(n,i),a={horizontal:{x:o.position,y:i.axisLength},vertical:{x:0,y:o.position}},s={horizontal:Ek/2,vertical:0},l=[a[r].x+t.x,a[r].y+t.y],u=s[r],h=[1,0,0,1,0,0];Se(h,h,u),we(h,h,l),this._axesLayout[e]={position:l,rotation:u,transform:h,axisNameAvailableWidth:o.axisNameAvailableWidth,axisLabelShow:o.axisLabelShow,nameTruncateMaxWidth:o.nameTruncateMaxWidth,tickDirection:1,labelDirection:1}}),this)},t.prototype.getAxis=function(t){return this._axesMap.get(t)},t.prototype.dataToPoint=function(t,e){return this.axisCoordToPoint(this._axesMap.get(e).dataToCoord(t),e)},t.prototype.eachActiveState=function(t,e,n,i){null==n&&(n=0),null==i&&(i=t.count());var r=this._axesMap,o=this.dimensions,a=[],s=[];E(o,(function(e){a.push(t.mapDimension(e)),s.push(r.get(e).model)}));for(var l=this.hasAxisBrushed(),u=n;ur*(1-h[0])?(l="jump",a=s-r*(1-h[2])):(a=s-r*h[1])>=0&&(a=s-r*(1-h[1]))<=0&&(a=0),(a*=e.axisExpandWidth/u)?Ck(a,i,o,"all"):l="none";else{var p=i[1]-i[0];(i=[Pk(0,o[1]*s/p-p/2)])[1]=Lk(o[1],i[0]+p),i[0]=i[1]-p}return{axisExpandWindow:i,behavior:l}},t}();function Vk(t,e){return Lk(Pk(t,e[0]),e[1])}function Bk(t,e){var n=e.layoutLength/(e.axisCount-1);return{position:n*t,axisNameAvailableWidth:n,axisLabelShow:!0}}function Fk(t,e){var n,i,r=e.layoutLength,o=e.axisExpandWidth,a=e.axisCount,s=e.axisCollapseWidth,l=e.winInnerIndices,u=s,h=!1;return t=0;n--)jr(e[n])},e.prototype.getActiveState=function(t){var e=this.activeIntervals;if(!e.length)return"normal";if(null==t||isNaN(+t))return"inactive";if(1===e.length){var n=e[0];if(n[0]<=t&&t<=n[1])return"active"}else for(var i=0,r=e.length;i6}(t)||o){if(a&&!o){"single"===s.brushMode&&sL(t);var l=T(s);l.brushType=ML(l.brushType,a),l.panelId=a===Hk?null:a.panelId,o=t._creatingCover=Qk(t,l),t._covers.push(o)}if(o){var u=CL[ML(t._brushType,a)];o.__brushOption.range=u.getCreatingRange(_L(t,o,t._track)),i&&(tL(t,o),u.updateCommon(t,o)),eL(t,o),r={isEnd:i}}}else i&&"single"===s.brushMode&&s.removeOnClick&&oL(t,e,n)&&sL(t)&&(r={isEnd:i,removeOnClick:!0});return r}function ML(t,e){return"auto"===t?e.defaultBrushType:t}var IL={mousedown:function(t){if(this._dragging)TL(this,t);else if(!t.target||!t.target.draggable){bL(t);var e=this.group.transformCoordToLocal(t.offsetX,t.offsetY);this._creatingCover=null,(this._creatingPanel=oL(this,t,e))&&(this._dragging=!0,this._track=[e.slice()])}},mousemove:function(t){var e=t.offsetX,n=t.offsetY,i=this.group.transformCoordToLocal(e,n);if(function(t,e,n){if(t._brushType&&!function(t,e,n){var i=t._zr;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}(t,e.offsetX,e.offsetY)){var i=t._zr,r=t._covers,o=oL(t,e,n);if(!t._dragging)for(var a=0;a=0&&(o[r[a].depth]=new Mc(r[a],this,e));if(i&&n){var s=QA(i,n,this,!0,(function(t,e){t.wrapMethod("getItemModel",(function(t,e){var n=t.parentModel,i=n.getData().getItemLayout(e);if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t})),e.wrapMethod("getItemModel",(function(t,e){var n=t.parentModel,i=n.getGraph().getEdgeByIndex(e).node1.getLayout();if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t}))}));return s.data}},e.prototype.setNodePosition=function(t,e){var n=(this.option.data||this.option.nodes)[t];n.localX=e[0],n.localY=e[1]},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.formatTooltip=function(t,e,n){function i(t){return isNaN(t)||null==t}if("edge"===n){var r=this.getDataParams(t,n),o=r.data,a=r.value;return ng("nameValue",{name:o.source+" -- "+o.target,value:a,noValue:i(a)})}var s=this.getGraph().getNodeByIndex(t).getLayout().value,l=this.getDataParams(t,n).data.name;return ng("nameValue",{name:null!=l?l+"":null,value:s,noValue:i(s)})},e.prototype.optionUpdated=function(){},e.prototype.getDataParams=function(e,n){var i=t.prototype.getDataParams.call(this,e,n);if(null==i.value&&"node"===n){var r=this.getGraph().getNodeByIndex(e).getLayout().value;i.value=r}return i},e.type="series.sankey",e.defaultOption={z:2,coordinateSystem:"view",left:"5%",top:"5%",right:"20%",bottom:"5%",orient:"horizontal",nodeWidth:20,nodeGap:8,draggable:!0,layoutIterations:32,label:{show:!0,position:"right",fontSize:12},edgeLabel:{show:!1,fontSize:12},levels:[],nodeAlign:"justify",lineStyle:{color:"#314656",opacity:.2,curveness:.5},emphasis:{label:{show:!0},lineStyle:{opacity:.5}},select:{itemStyle:{borderColor:"#212121"}},animationEasing:"linear",animationDuration:1e3},e}(mg);function HL(t,e){t.eachSeriesByType("sankey",(function(t){var n=t.get("nodeWidth"),i=t.get("nodeGap"),r=function(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e);t.layoutInfo=r;var o=r.width,a=r.height,s=t.getGraph(),l=s.nodes,u=s.edges;!function(t){E(t,(function(t){var e=QL(t.outEdges,JL),n=QL(t.inEdges,JL),i=t.getValue()||0,r=Math.max(e,n,i);t.setLayout({value:r},!0)}))}(l),function(t,e,n,i,r,o,a,s,l){(function(t,e,n,i,r,o,a){for(var s=[],l=[],u=[],h=[],c=0,p=0;p=0;v&&y.depth>d&&(d=y.depth),g.setLayout({depth:v?y.depth:c},!0),"vertical"===o?g.setLayout({dy:n},!0):g.setLayout({dx:n},!0);for(var m=0;mc-1?d:c-1;a&&"left"!==a&&function(t,e,n,i){if("right"===e){for(var r=[],o=t,a=0;o.length;){for(var s=0;s0;o--)UL(s,l*=.99,a),XL(s,r,n,i,a),tP(s,l,a),XL(s,r,n,i,a)}(t,e,o,r,i,a,s),function(t,e){var n="vertical"===e?"x":"y";E(t,(function(t){t.outEdges.sort((function(t,e){return t.node2.getLayout()[n]-e.node2.getLayout()[n]})),t.inEdges.sort((function(t,e){return t.node1.getLayout()[n]-e.node1.getLayout()[n]}))})),E(t,(function(t){var e=0,n=0;E(t.outEdges,(function(t){t.setLayout({sy:e},!0),e+=t.getLayout().dy})),E(t.inEdges,(function(t){t.setLayout({ty:n},!0),n+=t.getLayout().dy}))}))}(t,s)}(l,u,n,i,o,a,0!==B(l,(function(t){return 0===t.getLayout().value})).length?0:t.get("layoutIterations"),t.get("orient"),t.get("nodeAlign"))}))}function YL(t){var e=t.hostGraph.data.getRawDataItem(t.dataIndex);return null!=e.depth&&e.depth>=0}function XL(t,e,n,i,r){var o="vertical"===r?"x":"y";E(t,(function(t){var a,s,l;t.sort((function(t,e){return t.getLayout()[o]-e.getLayout()[o]}));for(var u=0,h=t.length,c="vertical"===r?"dx":"dy",p=0;p0&&(a=s.getLayout()[o]+l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]+s.getLayout()[c]+e;if((l=u-e-("vertical"===r?i:n))>0){a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0),u=a;for(p=h-2;p>=0;--p)(l=(s=t[p]).getLayout()[o]+s.getLayout()[c]+e-u)>0&&(a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]}}))}function UL(t,e,n){E(t.slice().reverse(),(function(t){E(t,(function(t){if(t.outEdges.length){var i=QL(t.outEdges,ZL,n)/QL(t.outEdges,JL);if(isNaN(i)){var r=t.outEdges.length;i=r?QL(t.outEdges,jL,n)/r:0}if("vertical"===n){var o=t.getLayout().x+(i-$L(t,n))*e;t.setLayout({x:o},!0)}else{var a=t.getLayout().y+(i-$L(t,n))*e;t.setLayout({y:a},!0)}}}))}))}function ZL(t,e){return $L(t.node2,e)*t.getValue()}function jL(t,e){return $L(t.node2,e)}function qL(t,e){return $L(t.node1,e)*t.getValue()}function KL(t,e){return $L(t.node1,e)}function $L(t,e){return"vertical"===e?t.getLayout().x+t.getLayout().dx/2:t.getLayout().y+t.getLayout().dy/2}function JL(t){return t.getValue()}function QL(t,e,n){for(var i=0,r=t.length,o=-1;++oo&&(o=e)})),E(n,(function(e){var n=new _D({type:"color",mappingMethod:"linear",dataExtent:[r,o],visual:t.get("color")}).mapValueToVisual(e.getLayout().value),i=e.getModel().get(["itemStyle","color"]);null!=i?(e.setVisual("color",i),e.setVisual("style",{fill:i})):(e.setVisual("color",n),e.setVisual("style",{fill:n}))}))}i.length&&E(i,(function(t){var e=t.getModel().get("lineStyle");t.setVisual("style",e)}))}))}var nP=function(){function t(){}return t.prototype.getInitialData=function(t,e){var n,i,r=e.getComponent("xAxis",this.get("xAxisIndex")),o=e.getComponent("yAxis",this.get("yAxisIndex")),a=r.get("type"),s=o.get("type");"category"===a?(t.layout="horizontal",n=r.getOrdinalMeta(),i=!0):"category"===s?(t.layout="vertical",n=o.getOrdinalMeta(),i=!0):t.layout=t.layout||"horizontal";var l=["x","y"],u="horizontal"===t.layout?0:1,h=this._baseAxisDim=l[u],c=l[1-u],p=[r,o],d=p[u].get("type"),f=p[1-u].get("type"),g=t.data;if(g&&i){var y=[];E(g,(function(t,e){var n;Y(t)?(n=t.slice(),t.unshift(e)):Y(t.value)?((n=A({},t)).value=n.value.slice(),t.value.unshift(e)):n=t,y.push(n)})),t.data=y}var v=this.defaultValueDimensions,m=[{name:h,type:Gm(d),ordinalMeta:n,otherDims:{tooltip:!1,itemName:0},dimsDef:["base"]},{name:c,type:Gm(f),dimsDef:v.slice()}];return MM(this,{coordDimensions:m,dimensionsCount:v.length+1,encodeDefaulter:H($p,m,this)})},t.prototype.getBaseAxis=function(){var t=this._baseAxisDim;return this.ecModel.getComponent(t+"Axis",this.get(t+"AxisIndex")).axis},t}(),iP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"min",defaultTooltip:!0},{name:"Q1",defaultTooltip:!0},{name:"median",defaultTooltip:!0},{name:"Q3",defaultTooltip:!0},{name:"max",defaultTooltip:!0}],n.visualDrawType="stroke",n}return n(e,t),e.type="series.boxplot",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,boxWidth:[7,50],itemStyle:{color:"#fff",borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2,shadowBlur:5,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0,0,0,0.2)"}},animationDuration:800},e}(mg);R(iP,nP,!0);var rP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this.group,o=this._data;this._data||r.removeAll();var a="horizontal"===t.get("layout")?1:0;i.diff(o).add((function(t){if(i.hasValue(t)){var e=sP(i.getItemLayout(t),i,t,a,!0);i.setItemGraphicEl(t,e),r.add(e)}})).update((function(t,e){var n=o.getItemGraphicEl(e);if(i.hasValue(t)){var s=i.getItemLayout(t);n?(_h(n),lP(s,n,i,t)):n=sP(s,i,t,a),r.add(n),i.setItemGraphicEl(t,n)}else r.remove(n)})).remove((function(t){var e=o.getItemGraphicEl(t);e&&r.remove(e)})).execute(),this._data=i},e.prototype.remove=function(t){var e=this.group,n=this._data;this._data=null,n&&n.eachItemGraphicEl((function(t){t&&e.remove(t)}))},e.type="boxplot",e}(kg),oP=function(){},aP=function(t){function e(e){var n=t.call(this,e)||this;return n.type="boxplotBoxPath",n}return n(e,t),e.prototype.getDefaultShape=function(){return new oP},e.prototype.buildPath=function(t,e){var n=e.points,i=0;for(t.moveTo(n[i][0],n[i][1]),i++;i<4;i++)t.lineTo(n[i][0],n[i][1]);for(t.closePath();ig){var _=[v,x];i.push(_)}}}return{boxData:n,outliers:i}}(e.getRawData(),t.config);return[{dimensions:["ItemName","Low","Q1","Q2","Q3","High"],data:i.boxData},{data:i.outliers}]}};var dP=["color","borderColor"],fP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeClipPath(),this._progressiveEls=null,this._updateDrawMode(t),this._isLargeDraw?this._renderLarge(t):this._renderNormal(t)},e.prototype.incrementalPrepareRender=function(t,e,n){this._clear(),this._updateDrawMode(t)},e.prototype.incrementalRender=function(t,e,n,i){this._progressiveEls=[],this._isLargeDraw?this._incrementalRenderLarge(t,e):this._incrementalRenderNormal(t,e)},e.prototype.eachRendered=function(t){qh(this._progressiveEls||this.group,t)},e.prototype._updateDrawMode=function(t){var e=t.pipelineContext.large;null!=this._isLargeDraw&&e===this._isLargeDraw||(this._isLargeDraw=e,this._clear())},e.prototype._renderNormal=function(t){var e=t.getData(),n=this._data,i=this.group,r=e.getLayout("isSimpleBox"),o=t.get("clip",!0),a=t.coordinateSystem,s=a.getArea&&a.getArea();this._data||i.removeAll(),e.diff(n).add((function(n){if(e.hasValue(n)){var a=e.getItemLayout(n);if(o&&mP(s,a))return;var l=vP(a,n,!0);gh(l,{shape:{points:a.ends}},t,n),xP(l,e,n,r),i.add(l),e.setItemGraphicEl(n,l)}})).update((function(a,l){var u=n.getItemGraphicEl(l);if(e.hasValue(a)){var h=e.getItemLayout(a);o&&mP(s,h)?i.remove(u):(u?(fh(u,{shape:{points:h.ends}},t,a),_h(u)):u=vP(h),xP(u,e,a,r),i.add(u),e.setItemGraphicEl(a,u))}else i.remove(u)})).remove((function(t){var e=n.getItemGraphicEl(t);e&&i.remove(e)})).execute(),this._data=e},e.prototype._renderLarge=function(t){this._clear(),SP(t,this.group);var e=t.get("clip",!0)?SS(t.coordinateSystem,!1,t):null;e?this.group.setClipPath(e):this.group.removeClipPath()},e.prototype._incrementalRenderNormal=function(t,e){for(var n,i=e.getData(),r=i.getLayout("isSimpleBox");null!=(n=t.next());){var o=vP(i.getItemLayout(n));xP(o,i,n,r),o.incremental=!0,this.group.add(o),this._progressiveEls.push(o)}},e.prototype._incrementalRenderLarge=function(t,e){SP(e,this.group,this._progressiveEls,!0)},e.prototype.remove=function(t){this._clear()},e.prototype._clear=function(){this.group.removeAll(),this._data=null},e.type="candlestick",e}(kg),gP=function(){},yP=function(t){function e(e){var n=t.call(this,e)||this;return n.type="normalCandlestickBox",n}return n(e,t),e.prototype.getDefaultShape=function(){return new gP},e.prototype.buildPath=function(t,e){var n=e.points;this.__simpleBox?(t.moveTo(n[4][0],n[4][1]),t.lineTo(n[6][0],n[6][1])):(t.moveTo(n[0][0],n[0][1]),t.lineTo(n[1][0],n[1][1]),t.lineTo(n[2][0],n[2][1]),t.lineTo(n[3][0],n[3][1]),t.closePath(),t.moveTo(n[4][0],n[4][1]),t.lineTo(n[5][0],n[5][1]),t.moveTo(n[6][0],n[6][1]),t.lineTo(n[7][0],n[7][1]))},e}(Is);function vP(t,e,n){var i=t.ends;return new yP({shape:{points:n?_P(i,t):i},z2:100})}function mP(t,e){for(var n=!0,i=0;i0?"borderColor":"borderColor0"])||n.get(["itemStyle",t>0?"color":"color0"]);0===t&&(r=n.get(["itemStyle","borderColorDoji"]));var o=n.getModel("itemStyle").getItemStyle(dP);e.useStyle(o),e.style.fill=null,e.style.stroke=r}var IP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"open",defaultTooltip:!0},{name:"close",defaultTooltip:!0},{name:"lowest",defaultTooltip:!0},{name:"highest",defaultTooltip:!0}],n}return n(e,t),e.prototype.getShadowDim=function(){return"open"},e.prototype.brushSelector=function(t,e,n){var i=e.getItemLayout(t);return i&&n.rect(i.brushRect)},e.type="series.candlestick",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,clip:!0,itemStyle:{color:"#eb5454",color0:"#47b262",borderColor:"#eb5454",borderColor0:"#47b262",borderColorDoji:null,borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,large:!0,largeThreshold:600,progressive:3e3,progressiveThreshold:1e4,progressiveChunkMode:"mod",animationEasing:"linear",animationDuration:300},e}(mg);function TP(t){t&&Y(t.series)&&E(t.series,(function(t){q(t)&&"k"===t.type&&(t.type="candlestick")}))}R(IP,nP,!0);var CP=["itemStyle","borderColor"],DP=["itemStyle","borderColor0"],AP=["itemStyle","borderColorDoji"],kP=["itemStyle","color"],LP=["itemStyle","color0"],PP={seriesType:"candlestick",plan:Cg(),performRawSeries:!0,reset:function(t,e){function n(t,e){return e.get(t>0?kP:LP)}function i(t,e){return e.get(0===t?AP:t>0?CP:DP)}if(!e.isSeriesFiltered(t))return!t.pipelineContext.large&&{progress:function(t,e){for(var r;null!=(r=t.next());){var o=e.getItemModel(r),a=e.getItemLayout(r).sign,s=o.getItemStyle();s.fill=n(a,o),s.stroke=i(a,o)||s.fill,A(e.ensureUniqueItemVisual(r,"style"),s)}}}}},OP={seriesType:"candlestick",plan:Cg(),reset:function(t){var e=t.coordinateSystem,n=t.getData(),i=function(t,e){var n,i=t.getBaseAxis(),r="category"===i.type?i.getBandWidth():(n=i.getExtent(),Math.abs(n[1]-n[0])/e.count()),o=Ur(rt(t.get("barMaxWidth"),r),r),a=Ur(rt(t.get("barMinWidth"),1),r),s=t.get("barWidth");return null!=s?Ur(s,r):Math.max(Math.min(r/2,o),a)}(t,n),r=["x","y"],o=n.getDimensionIndex(n.mapDimension(r[0])),a=z(n.mapDimensionsAll(r[1]),n.getDimensionIndex,n),s=a[0],l=a[1],u=a[2],h=a[3];if(n.setLayout({candleWidth:i,isSimpleBox:i<=1.3}),!(o<0||a.length<4))return{progress:t.pipelineContext.large?function(n,i){var r,a,c=Ex(4*n.count),p=0,d=[],f=[],g=i.getStore(),y=!!t.get(["itemStyle","borderColorDoji"]);for(;null!=(a=n.next());){var v=g.get(o,a),m=g.get(s,a),x=g.get(l,a),_=g.get(u,a),b=g.get(h,a);isNaN(v)||isNaN(_)||isNaN(b)?(c[p++]=NaN,p+=3):(c[p++]=RP(g,a,m,x,l,y),d[0]=v,d[1]=_,r=e.dataToPoint(d,null,f),c[p++]=r?r[0]:NaN,c[p++]=r?r[1]:NaN,d[1]=b,r=e.dataToPoint(d,null,f),c[p++]=r?r[1]:NaN)}i.setLayout("largePoints",c)}:function(t,n){var r,a=n.getStore();for(;null!=(r=t.next());){var c=a.get(o,r),p=a.get(s,r),d=a.get(l,r),f=a.get(u,r),g=a.get(h,r),y=Math.min(p,d),v=Math.max(p,d),m=M(y,c),x=M(v,c),_=M(f,c),b=M(g,c),w=[];I(w,x,0),I(w,m,1),w.push(C(b),C(x),C(_),C(m));var S=!!n.getItemModel(r).get(["itemStyle","borderColorDoji"]);n.setItemLayout(r,{sign:RP(a,r,p,d,l,S),initBaseline:p>d?x[1]:m[1],ends:w,brushRect:T(f,g,c)})}function M(t,n){var i=[];return i[0]=n,i[1]=t,isNaN(n)||isNaN(t)?[NaN,NaN]:e.dataToPoint(i)}function I(t,e,n){var r=e.slice(),o=e.slice();r[0]=Nh(r[0]+i/2,1,!1),o[0]=Nh(o[0]-i/2,1,!0),n?t.push(r,o):t.push(o,r)}function T(t,e,n){var r=M(t,n),o=M(e,n);return r[0]-=i/2,o[0]-=i/2,{x:r[0],y:r[1],width:i,height:o[1]-r[1]}}function C(t){return t[0]=Nh(t[0],1),t}}}}};function RP(t,e,n,i,r,o){return n>i?-1:n0?t.get(r,e-1)<=i?1:-1:1}function NP(t,e){var n=e.rippleEffectColor||e.color;t.eachChild((function(t){t.attr({z:e.z,zlevel:e.zlevel,style:{stroke:"stroke"===e.brushType?n:null,fill:"fill"===e.brushType?n:null}})}))}var EP=function(t){function e(e,n){var i=t.call(this)||this,r=new oS(e,n),o=new zr;return i.add(r),i.add(o),i.updateData(e,n),i}return n(e,t),e.prototype.stopEffectAnimation=function(){this.childAt(1).removeAll()},e.prototype.startEffectAnimation=function(t){for(var e=t.symbolType,n=t.color,i=t.rippleNumber,r=this.childAt(1),o=0;o0&&(o=this._getLineLength(i)/l*1e3),o!==this._period||a!==this._loop||s!==this._roundTrip){i.stopAnimation();var h=void 0;h=X(u)?u(n):u,i.__t>0&&(h=-o*i.__t),this._animateSymbol(i,o,h,a,s)}this._period=o,this._loop=a,this._roundTrip=s}},e.prototype._animateSymbol=function(t,e,n,i,r){if(e>0){t.__t=0;var o=this,a=t.animate("",i).when(r?2*e:e,{__t:r?2:1}).delay(n).during((function(){o._updateSymbolPosition(t)}));i||a.done((function(){o.remove(t)})),a.start()}},e.prototype._getLineLength=function(t){return Vt(t.__p1,t.__cp1)+Vt(t.__cp1,t.__p2)},e.prototype._updateAnimationPoints=function(t,e){t.__p1=e[0],t.__p2=e[1],t.__cp1=e[2]||[(e[0][0]+e[1][0])/2,(e[0][1]+e[1][1])/2]},e.prototype.updateData=function(t,e,n){this.childAt(0).updateData(t,e,n),this._updateEffectSymbol(t,e)},e.prototype._updateSymbolPosition=function(t){var e=t.__p1,n=t.__p2,i=t.__cp1,r=t.__t<1?t.__t:2-t.__t,o=[t.x,t.y],a=o.slice(),s=In,l=Tn;o[0]=s(e[0],i[0],n[0],r),o[1]=s(e[1],i[1],n[1],r);var u=t.__t<1?l(e[0],i[0],n[0],r):l(n[0],i[0],e[0],1-r),h=t.__t<1?l(e[1],i[1],n[1],r):l(n[1],i[1],e[1],1-r);t.rotation=-Math.atan2(h,u)-Math.PI/2,"line"!==this._symbolType&&"rect"!==this._symbolType&&"roundRect"!==this._symbolType||(void 0!==t.__lastT&&t.__lastT=0&&!(i[o]<=e);o--);o=Math.min(o,r-2)}else{for(o=a;oe);o++);o=Math.min(o-1,r-2)}var s=(e-i[o])/(i[o+1]-i[o]),l=n[o],u=n[o+1];t.x=l[0]*(1-s)+s*u[0],t.y=l[1]*(1-s)+s*u[1];var h=t.__t<1?u[0]-l[0]:l[0]-u[0],c=t.__t<1?u[1]-l[1]:l[1]-u[1];t.rotation=-Math.atan2(c,h)-Math.PI/2,this._lastFrame=o,this._lastFramePercent=e,t.ignore=!1}},e}(BP),WP=function(){this.polyline=!1,this.curveness=0,this.segs=[]},HP=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new WP},e.prototype.buildPath=function(t,e){var n,i=e.segs,r=e.curveness;if(e.polyline)for(n=this._off;n0){t.moveTo(i[n++],i[n++]);for(var a=1;a0){var c=(s+u)/2-(l-h)*r,p=(l+h)/2-(u-s)*r;t.quadraticCurveTo(c,p,u,h)}else t.lineTo(u,h)}this.incremental&&(this._off=n,this.notClear=!0)},e.prototype.findDataIndex=function(t,e){var n=this.shape,i=n.segs,r=n.curveness,o=this.style.lineWidth;if(n.polyline)for(var a=0,s=0;s0)for(var u=i[s++],h=i[s++],c=1;c0){if(ls(u,h,(u+p)/2-(h-d)*r,(h+d)/2-(p-u)*r,p,d,o,t,e))return a}else if(as(u,h,p,d,o,t,e))return a;a++}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape.segs,n=1/0,i=1/0,r=-1/0,o=-1/0,a=0;a0&&(o.dataIndex=n+t.__startIndex)}))},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),XP={seriesType:"lines",plan:Cg(),reset:function(t){var e=t.coordinateSystem;if(e){var n=t.get("polyline"),i=t.pipelineContext.large;return{progress:function(r,o){var a=[];if(i){var s=void 0,l=r.end-r.start;if(n){for(var u=0,h=r.start;h0&&(l||s.configLayer(o,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(a/10+.9,1),0)})),r.updateData(i);var u=t.get("clip",!0)&&SS(t.coordinateSystem,!1,t);u?this.group.setClipPath(u):this.group.removeClipPath(),this._lastZlevel=o,this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateLineDraw(i,t).incrementalPrepareUpdate(i),this._clearLayer(n),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._lineDraw.incrementalUpdate(t,e.getData()),this._finished=t.end===e.getData().count()},e.prototype.eachRendered=function(t){this._lineDraw&&this._lineDraw.eachRendered(t)},e.prototype.updateTransform=function(t,e,n){var i=t.getData(),r=t.pipelineContext;if(!this._finished||r.large||r.progressiveRender)return{update:!0};var o=XP.reset(t,e,n);o.progress&&o.progress({start:0,end:i.count(),count:i.count()},i),this._lineDraw.updateLayout(),this._clearLayer(n)},e.prototype._updateLineDraw=function(t,e){var n=this._lineDraw,i=this._showEffect(e),r=!!e.get("polyline"),o=e.pipelineContext.large;return n&&i===this._hasEffet&&r===this._isPolyline&&o===this._isLargeDraw||(n&&n.remove(),n=this._lineDraw=o?new YP:new RA(r?i?GP:FP:i?BP:OA),this._hasEffet=i,this._isPolyline=r,this._isLargeDraw=o),this.group.add(n.group),n},e.prototype._showEffect=function(t){return!!t.get(["effect","show"])},e.prototype._clearLayer=function(t){var e=t.getZr();"svg"===e.painter.getType()||null==this._lastZlevel||e.painter.getLayer(this._lastZlevel).clear(!0)},e.prototype.remove=function(t,e){this._lineDraw&&this._lineDraw.remove(),this._lineDraw=null,this._clearLayer(e)},e.prototype.dispose=function(t,e){this.remove(t,e)},e.type="lines",e}(kg),ZP="undefined"==typeof Uint32Array?Array:Uint32Array,jP="undefined"==typeof Float64Array?Array:Float64Array;function qP(t){var e=t.data;e&&e[0]&&e[0][0]&&e[0][0].coord&&(t.data=z(e,(function(t){var e={coords:[t[0].coord,t[1].coord]};return t[0].name&&(e.fromName=t[0].name),t[1].name&&(e.toName=t[1].name),D([e,t[0],t[1]])})))}var KP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="lineStyle",n.visualDrawType="stroke",n}return n(e,t),e.prototype.init=function(e){e.data=e.data||[],qP(e);var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count)),t.prototype.init.apply(this,arguments)},e.prototype.mergeOption=function(e){if(qP(e),e.data){var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count))}t.prototype.mergeOption.apply(this,arguments)},e.prototype.appendData=function(t){var e=this._processFlatCoordsArray(t.data);e.flatCoords&&(this._flatCoords?(this._flatCoords=vt(this._flatCoords,e.flatCoords),this._flatCoordsOffset=vt(this._flatCoordsOffset,e.flatCoordsOffset)):(this._flatCoords=e.flatCoords,this._flatCoordsOffset=e.flatCoordsOffset),t.data=new Float32Array(e.count)),this.getRawData().appendData(t.data)},e.prototype._getCoordsFromItemModel=function(t){var e=this.getData().getItemModel(t),n=e.option instanceof Array?e.option:e.getShallow("coords");return n},e.prototype.getLineCoordsCount=function(t){return this._flatCoordsOffset?this._flatCoordsOffset[2*t+1]:this._getCoordsFromItemModel(t).length},e.prototype.getLineCoords=function(t,e){if(this._flatCoordsOffset){for(var n=this._flatCoordsOffset[2*t],i=this._flatCoordsOffset[2*t+1],r=0;r ")})},e.prototype.preventIncremental=function(){return!!this.get(["effect","show"])},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?1e4:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?2e4:this.get("progressiveThreshold"):t},e.prototype.getZLevelKey=function(){var t=this.getModel("effect"),e=t.get("trailLength");return this.getData().count()>this.getProgressiveThreshold()?this.id:t.get("show")&&e>0?e+"":""},e.type="series.lines",e.dependencies=["grid","polar","geo","calendar"],e.defaultOption={coordinateSystem:"geo",z:2,legendHoverLink:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,clip:!0,label:{show:!1,position:"end"},lineStyle:{opacity:.5}},e}(mg);function $P(t){return t instanceof Array||(t=[t,t]),t}var JP={seriesType:"lines",reset:function(t){var e=$P(t.get("symbol")),n=$P(t.get("symbolSize")),i=t.getData();return i.setVisual("fromSymbol",e&&e[0]),i.setVisual("toSymbol",e&&e[1]),i.setVisual("fromSymbolSize",n&&n[0]),i.setVisual("toSymbolSize",n&&n[1]),{dataEach:i.hasItemOption?function(t,e){var n=t.getItemModel(e),i=$P(n.getShallow("symbol",!0)),r=$P(n.getShallow("symbolSize",!0));i[0]&&t.setItemVisual(e,"fromSymbol",i[0]),i[1]&&t.setItemVisual(e,"toSymbol",i[1]),r[0]&&t.setItemVisual(e,"fromSymbolSize",r[0]),r[1]&&t.setItemVisual(e,"toSymbolSize",r[1])}:null}}};var QP=function(){function t(){this.blurSize=30,this.pointSize=20,this.maxOpacity=1,this.minOpacity=0,this._gradientPixels={inRange:null,outOfRange:null};var t=h.createCanvas();this.canvas=t}return t.prototype.update=function(t,e,n,i,r,o){var a=this._getBrush(),s=this._getGradient(r,"inRange"),l=this._getGradient(r,"outOfRange"),u=this.pointSize+this.blurSize,h=this.canvas,c=h.getContext("2d"),p=t.length;h.width=e,h.height=n;for(var d=0;d0){var I=o(v)?s:l;v>0&&(v=v*S+w),x[_++]=I[M],x[_++]=I[M+1],x[_++]=I[M+2],x[_++]=I[M+3]*v*256}else _+=4}return c.putImageData(m,0,0),h},t.prototype._getBrush=function(){var t=this._brushCanvas||(this._brushCanvas=h.createCanvas()),e=this.pointSize+this.blurSize,n=2*e;t.width=n,t.height=n;var i=t.getContext("2d");return i.clearRect(0,0,n,n),i.shadowOffsetX=n,i.shadowBlur=this.blurSize,i.shadowColor="#000",i.beginPath(),i.arc(-e,e,this.pointSize,0,2*Math.PI,!0),i.closePath(),i.fill(),t},t.prototype._getGradient=function(t,e){for(var n=this._gradientPixels,i=n[e]||(n[e]=new Uint8ClampedArray(1024)),r=[0,0,0,0],o=0,a=0;a<256;a++)t[e](a/255,!0,r),i[o++]=r[0],i[o++]=r[1],i[o++]=r[2],i[o++]=r[3];return i},t}();function tO(t){var e=t.dimensions;return"lng"===e[0]&&"lat"===e[1]}var eO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i;e.eachComponent("visualMap",(function(e){e.eachTargetSeries((function(n){n===t&&(i=e)}))})),this._progressiveEls=null,this.group.removeAll();var r=t.coordinateSystem;"cartesian2d"===r.type||"calendar"===r.type?this._renderOnCartesianAndCalendar(t,n,0,t.getData().count()):tO(r)&&this._renderOnGeo(r,t,i,n)},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll()},e.prototype.incrementalRender=function(t,e,n,i){var r=e.coordinateSystem;r&&(tO(r)?this.render(e,n,i):(this._progressiveEls=[],this._renderOnCartesianAndCalendar(e,i,t.start,t.end,!0)))},e.prototype.eachRendered=function(t){qh(this._progressiveEls||this.group,t)},e.prototype._renderOnCartesianAndCalendar=function(t,e,n,i,r){var o,a,s,l,u=t.coordinateSystem,h=MS(u,"cartesian2d");if(h){var c=u.getAxis("x"),p=u.getAxis("y");0,o=c.getBandWidth()+.5,a=p.getBandWidth()+.5,s=c.scale.getExtent(),l=p.scale.getExtent()}for(var d=this.group,f=t.getData(),g=t.getModel(["emphasis","itemStyle"]).getItemStyle(),y=t.getModel(["blur","itemStyle"]).getItemStyle(),v=t.getModel(["select","itemStyle"]).getItemStyle(),m=t.get(["itemStyle","borderRadius"]),x=ec(t),_=t.getModel("emphasis"),b=_.get("focus"),w=_.get("blurScope"),S=_.get("disabled"),M=h?[f.mapDimension("x"),f.mapDimension("y"),f.mapDimension("value")]:[f.mapDimension("time"),f.mapDimension("value")],I=n;Is[1]||Al[1])continue;var k=u.dataToPoint([D,A]);T=new zs({shape:{x:k[0]-o/2,y:k[1]-a/2,width:o,height:a},style:C})}else{if(isNaN(f.get(M[1],I)))continue;T=new zs({z2:1,shape:u.dataToRect([f.get(M[0],I)]).contentShape,style:C})}if(f.hasItemOption){var L=f.getItemModel(I),P=L.getModel("emphasis");g=P.getModel("itemStyle").getItemStyle(),y=L.getModel(["blur","itemStyle"]).getItemStyle(),v=L.getModel(["select","itemStyle"]).getItemStyle(),m=L.get(["itemStyle","borderRadius"]),b=P.get("focus"),w=P.get("blurScope"),S=P.get("disabled"),x=ec(L)}T.shape.r=m;var O=t.getRawValue(I),R="-";O&&null!=O[2]&&(R=O[2]+""),tc(T,x,{labelFetcher:t,labelDataIndex:I,defaultOpacity:C.opacity,defaultText:R}),T.ensureState("emphasis").style=g,T.ensureState("blur").style=y,T.ensureState("select").style=v,Yl(T,b,w,S),T.incremental=r,r&&(T.states.emphasis.hoverLayer=!0),d.add(T),f.setItemGraphicEl(I,T),this._progressiveEls&&this._progressiveEls.push(T)}},e.prototype._renderOnGeo=function(t,e,n,i){var r=n.targetVisuals.inRange,o=n.targetVisuals.outOfRange,a=e.getData(),s=this._hmLayer||this._hmLayer||new QP;s.blurSize=e.get("blurSize"),s.pointSize=e.get("pointSize"),s.minOpacity=e.get("minOpacity"),s.maxOpacity=e.get("maxOpacity");var l=t.getViewRect().clone(),u=t.getRoamTransform();l.applyTransform(u);var h=Math.max(l.x,0),c=Math.max(l.y,0),p=Math.min(l.width+l.x,i.getWidth()),d=Math.min(l.height+l.y,i.getHeight()),f=p-h,g=d-c,y=[a.mapDimension("lng"),a.mapDimension("lat"),a.mapDimension("value")],v=a.mapArray(y,(function(e,n,i){var r=t.dataToPoint([e,n]);return r[0]-=h,r[1]-=c,r.push(i),r})),m=n.getExtent(),x="visualMap.continuous"===n.type?function(t,e){var n=t[1]-t[0];return e=[(e[0]-t[0])/n,(e[1]-t[0])/n],function(t){return t>=e[0]&&t<=e[1]}}(m,n.option.range):function(t,e,n){var i=t[1]-t[0],r=(e=z(e,(function(e){return{interval:[(e.interval[0]-t[0])/i,(e.interval[1]-t[0])/i]}}))).length,o=0;return function(t){var i;for(i=o;i=0;i--){var a;if((a=e[i].interval)[0]<=t&&t<=a[1]){o=i;break}}return i>=0&&i0?1:-1}(n,o,r,i,c),function(t,e,n,i,r,o,a,s,l,u){var h,c=l.valueDim,p=l.categoryDim,d=Math.abs(n[p.wh]),f=t.getItemVisual(e,"symbolSize");h=Y(f)?f.slice():null==f?["100%","100%"]:[f,f];h[p.index]=Ur(h[p.index],d),h[c.index]=Ur(h[c.index],i?d:Math.abs(o)),u.symbolSize=h;var g=u.symbolScale=[h[0]/s,h[1]/s];g[c.index]*=(l.isHorizontal?-1:1)*a}(t,e,r,o,0,c.boundingLength,c.pxSign,u,i,c),function(t,e,n,i,r){var o=t.get(iO)||0;o&&(oO.attr({scaleX:e[0],scaleY:e[1],rotation:n}),oO.updateTransform(),o/=oO.getLineScale(),o*=e[i.valueDim.index]);r.valueLineWidth=o||0}(n,c.symbolScale,l,i,c);var p=c.symbolSize,d=Yy(n.get("symbolOffset"),p);return function(t,e,n,i,r,o,a,s,l,u,h,c){var p=h.categoryDim,d=h.valueDim,f=c.pxSign,g=Math.max(e[d.index]+s,0),y=g;if(i){var v=Math.abs(l),m=it(t.get("symbolMargin"),"15%")+"",x=!1;m.lastIndexOf("!")===m.length-1&&(x=!0,m=m.slice(0,m.length-1));var _=Ur(m,e[d.index]),b=Math.max(g+2*_,0),w=x?0:2*_,S=co(i),M=S?i:SO((v+w)/b);b=g+2*(_=(v-M*g)/2/(x?M:Math.max(M-1,1))),w=x?0:2*_,S||"fixed"===i||(M=u?SO((Math.abs(u)+w)/b):0),y=M*b-w,c.repeatTimes=M,c.symbolMargin=_}var I=f*(y/2),T=c.pathPosition=[];T[p.index]=n[p.wh]/2,T[d.index]="start"===a?I:"end"===a?l-I:l/2,o&&(T[0]+=o[0],T[1]+=o[1]);var C=c.bundlePosition=[];C[p.index]=n[p.xy],C[d.index]=n[d.xy];var D=c.barRectShape=A({},n);D[d.wh]=f*Math.max(Math.abs(n[d.wh]),Math.abs(T[d.index]+I)),D[p.wh]=n[p.wh];var k=c.clipShape={};k[p.xy]=-n[p.xy],k[p.wh]=h.ecSize[p.wh],k[d.xy]=0,k[d.wh]=n[d.wh]}(n,p,r,o,0,d,s,c.valueLineWidth,c.boundingLength,c.repeatCutLength,i,c),c}function lO(t,e){return t.toGlobalCoord(t.dataToCoord(t.scale.parse(e)))}function uO(t){var e=t.symbolPatternSize,n=Wy(t.symbolType,-e/2,-e/2,e,e);return n.attr({culling:!0}),"image"!==n.type&&n.setStyle({strokeNoScale:!0}),n}function hO(t,e,n,i){var r=t.__pictorialBundle,o=n.symbolSize,a=n.valueLineWidth,s=n.pathPosition,l=e.valueDim,u=n.repeatTimes||0,h=0,c=o[e.valueDim.index]+a+2*n.symbolMargin;for(_O(t,(function(t){t.__pictorialAnimationIndex=h,t.__pictorialRepeatTimes=u,h0:i<0)&&(r=u-1-t),e[l.index]=c*(r-u/2+.5)+s[l.index],{x:e[0],y:e[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation}}}function cO(t,e,n,i){var r=t.__pictorialBundle,o=t.__pictorialMainPath;o?bO(o,null,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation},n,i):(o=t.__pictorialMainPath=uO(n),r.add(o),bO(o,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:0,scaleY:0,rotation:n.rotation},{scaleX:n.symbolScale[0],scaleY:n.symbolScale[1]},n,i))}function pO(t,e,n){var i=A({},e.barRectShape),r=t.__pictorialBarRect;r?bO(r,null,{shape:i},e,n):((r=t.__pictorialBarRect=new zs({z2:2,shape:i,silent:!0,style:{stroke:"transparent",fill:"transparent",lineWidth:0}})).disableMorphing=!0,t.add(r))}function dO(t,e,n,i){if(n.symbolClip){var r=t.__pictorialClipPath,o=A({},n.clipShape),a=e.valueDim,s=n.animationModel,l=n.dataIndex;if(r)fh(r,{shape:o},s,l);else{o[a.wh]=0,r=new zs({shape:o}),t.__pictorialBundle.setClipPath(r),t.__pictorialClipPath=r;var u={};u[a.wh]=n.clipShape[a.wh],Kh[i?"updateProps":"initProps"](r,{shape:u},s,l)}}}function fO(t,e){var n=t.getItemModel(e);return n.getAnimationDelayParams=gO,n.isAnimationEnabled=yO,n}function gO(t){return{index:t.__pictorialAnimationIndex,count:t.__pictorialRepeatTimes}}function yO(){return this.parentModel.isAnimationEnabled()&&!!this.getShallow("animation")}function vO(t,e,n,i){var r=new zr,o=new zr;return r.add(o),r.__pictorialBundle=o,o.x=n.bundlePosition[0],o.y=n.bundlePosition[1],n.symbolRepeat?hO(r,e,n):cO(r,0,n),pO(r,n,i),dO(r,e,n,i),r.__pictorialShapeStr=xO(t,n),r.__pictorialSymbolMeta=n,r}function mO(t,e,n,i){var r=i.__pictorialBarRect;r&&r.removeTextContent();var o=[];_O(i,(function(t){o.push(t)})),i.__pictorialMainPath&&o.push(i.__pictorialMainPath),i.__pictorialClipPath&&(n=null),E(o,(function(t){vh(t,{scaleX:0,scaleY:0},n,e,(function(){i.parent&&i.parent.remove(i)}))})),t.setItemGraphicEl(e,null)}function xO(t,e){return[t.getItemVisual(e.dataIndex,"symbol")||"none",!!e.symbolRepeat,!!e.symbolClip].join(":")}function _O(t,e,n){E(t.__pictorialBundle.children(),(function(i){i!==t.__pictorialBarRect&&e.call(n,i)}))}function bO(t,e,n,i,r,o){e&&t.attr(e),i.symbolClip&&!r?n&&t.attr(n):n&&Kh[r?"updateProps":"initProps"](t,n,i.animationModel,i.dataIndex,o)}function wO(t,e,n){var i=n.dataIndex,r=n.itemModel,o=r.getModel("emphasis"),a=o.getModel("itemStyle").getItemStyle(),s=r.getModel(["blur","itemStyle"]).getItemStyle(),l=r.getModel(["select","itemStyle"]).getItemStyle(),u=r.getShallow("cursor"),h=o.get("focus"),c=o.get("blurScope"),p=o.get("scale");_O(t,(function(t){if(t instanceof ks){var e=t.style;t.useStyle(A({image:e.image,x:e.x,y:e.y,width:e.width,height:e.height},n.style))}else t.useStyle(n.style);var i=t.ensureState("emphasis");i.style=a,p&&(i.scaleX=1.1*t.scaleX,i.scaleY=1.1*t.scaleY),t.ensureState("blur").style=s,t.ensureState("select").style=l,u&&(t.cursor=u),t.z2=n.z2}));var d=e.valueDim.posDesc[+(n.boundingLength>0)];tc(t.__pictorialBarRect,ec(r),{labelFetcher:e.seriesModel,labelDataIndex:i,defaultText:iS(e.seriesModel.getData(),i),inheritColor:n.style.fill,defaultOpacity:n.style.opacity,defaultOutsidePosition:d}),Yl(t,h,c,o.get("disabled"))}function SO(t){var e=Math.round(t);return Math.abs(t-e)<1e-4?e:Math.ceil(t)}var MO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n.defaultSymbol="roundRect",n}return n(e,t),e.prototype.getInitialData=function(e){return e.stack=null,t.prototype.getInitialData.apply(this,arguments)},e.type="series.pictorialBar",e.dependencies=["grid"],e.defaultOption=Cc(FS.defaultOption,{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",progressive:0,emphasis:{scale:!1},select:{itemStyle:{borderColor:"#212121"}}}),e}(FS);var IO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._layers=[],n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this,o=this.group,a=t.getLayerSeries(),s=i.getLayout("layoutInfo"),l=s.rect,u=s.boundaryGap;function h(t){return t.name}o.x=0,o.y=l.y+u[0];var c=new Vm(this._layersSeries||[],a,h,h),p=[];function d(e,n,s){var l=r._layers;if("remove"!==e){for(var u,h,c=[],d=[],f=a[n].indices,g=0;go&&(o=s),i.push(s)}for(var u=0;uo&&(o=c)}return{y0:r,max:o}}(l),h=u.y0,c=n/u.max,p=o.length,d=o[0].indices.length,f=0;fMath.PI/2?"right":"left"):S&&"center"!==S?"left"===S?(m=r.r0+w,a>Math.PI/2&&(S="right")):"right"===S&&(m=r.r-w,a>Math.PI/2&&(S="left")):(m=o===2*Math.PI&&0===r.r0?0:(r.r+r.r0)/2,S="center"),g.style.align=S,g.style.verticalAlign=f(p,"verticalAlign")||"middle",g.x=m*s+r.cx,g.y=m*l+r.cy;var M=f(p,"rotate"),I=0;"radial"===M?(I=hs(-a))>Math.PI/2&&I<1.5*Math.PI&&(I+=Math.PI):"tangential"===M?(I=Math.PI/2-a)>Math.PI/2?I-=Math.PI:I<-Math.PI/2&&(I+=Math.PI):j(M)&&(I=M*Math.PI/180),g.rotation=hs(I)})),h.dirtyStyle()},e}(zu),kO="sunburstRootToNode",LO="sunburstHighlight";var PO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n,i){var r=this;this.seriesModel=t,this.api=n,this.ecModel=e;var o=t.getData(),a=o.tree.root,s=t.getViewRoot(),l=this.group,u=t.get("renderLabelForZeroData"),h=[];s.eachNode((function(t){h.push(t)}));var c=this._oldChildren||[];!function(i,r){if(0===i.length&&0===r.length)return;function s(t){return t.getId()}function h(s,h){!function(i,r){u||!i||i.getValue()||(i=null);if(i!==a&&r!==a)if(r&&r.piece)i?(r.piece.updateData(!1,i,t,e,n),o.setItemGraphicEl(i.dataIndex,r.piece)):function(t){if(!t)return;t.piece&&(l.remove(t.piece),t.piece=null)}(r);else if(i){var s=new AO(i,t,e,n);l.add(s),o.setItemGraphicEl(i.dataIndex,s)}}(null==s?null:i[s],null==h?null:r[h])}new Vm(r,i,s,s).add(h).update(h).remove(H(h,null)).execute()}(h,c),function(i,o){o.depth>0?(r.virtualPiece?r.virtualPiece.updateData(!1,i,t,e,n):(r.virtualPiece=new AO(i,t,e,n),l.add(r.virtualPiece)),o.piece.off("click"),r.virtualPiece.on("click",(function(t){r._rootToNode(o.parentNode)}))):r.virtualPiece&&(l.remove(r.virtualPiece),r.virtualPiece=null)}(a,s),this._initEvents(),this._oldChildren=h},e.prototype._initEvents=function(){var t=this;this.group.off("click"),this.group.on("click",(function(e){var n=!1;t.seriesModel.getViewRoot().eachNode((function(i){if(!n&&i.piece&&i.piece===e.target){var r=i.getModel().get("nodeClick");if("rootToNode"===r)t._rootToNode(i);else if("link"===r){var o=i.getModel(),a=o.get("link");if(a)bp(a,o.get("target",!0)||"_blank")}n=!0}}))}))},e.prototype._rootToNode=function(t){t!==this.seriesModel.getViewRoot()&&this.api.dispatchAction({type:kO,from:this.uid,seriesId:this.seriesModel.id,targetNode:t})},e.prototype.containPoint=function(t,e){var n=e.getData().getItemLayout(0);if(n){var i=t[0]-n.cx,r=t[1]-n.cy,o=Math.sqrt(i*i+r*r);return o<=n.r&&o>=n.r0}},e.type="sunburst",e}(kg),OO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.ignoreStyleOnData=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};RO(n);var i=this._levelModels=z(t.levels||[],(function(t){return new Mc(t,this,e)}),this),r=UC.createTree(n,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=r.getNodeByDataIndex(e),o=i[n.depth];return o&&(t.parentModel=o),t}))}));return r.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treePathInfo=KC(i,this),n},e.prototype.getLevelModel=function(t){return this._levelModels&&this._levelModels[t.depth]},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){nD(this)},e.type="series.sunburst",e.defaultOption={z:2,center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,stillShowZeroSum:!0,nodeClick:"rootToNode",renderLabelForZeroData:!1,label:{rotate:"radial",show:!0,opacity:1,align:"center",position:"inside",distance:5,silent:!0},itemStyle:{borderWidth:1,borderColor:"white",borderType:"solid",shadowBlur:0,shadowColor:"rgba(0, 0, 0, 0.2)",shadowOffsetX:0,shadowOffsetY:0,opacity:1},emphasis:{focus:"descendant"},blur:{itemStyle:{opacity:.2},label:{opacity:.1}},animationType:"expansion",animationDuration:1e3,animationDurationUpdate:500,data:[],sort:"desc"},e}(mg);function RO(t){var e=0;E(t.children,(function(t){RO(t);var n=t.value;Y(n)&&(n=n[0]),e+=n}));var n=t.value;Y(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),Y(t.value)?t.value[0]=n:t.value=n}var NO=Math.PI/180;function EO(t,e,n){e.eachSeriesByType(t,(function(t){var e=t.get("center"),i=t.get("radius");Y(i)||(i=[0,i]),Y(e)||(e=[e,e]);var r=n.getWidth(),o=n.getHeight(),a=Math.min(r,o),s=Ur(e[0],r),l=Ur(e[1],o),u=Ur(i[0],a/2),h=Ur(i[1],a/2),c=-t.get("startAngle")*NO,p=t.get("minAngle")*NO,d=t.getData().tree.root,f=t.getViewRoot(),g=f.depth,y=t.get("sort");null!=y&&zO(f,y);var v=0;E(f.children,(function(t){!isNaN(t.getValue())&&v++}));var m=f.getValue(),x=Math.PI/(m||v)*2,_=f.depth>0,b=f.height-(_?-1:1),w=(h-u)/(b||1),S=t.get("clockwise"),M=t.get("stillShowZeroSum"),I=S?1:-1,T=function(e,n){if(e){var i=n;if(e!==d){var r=e.getValue(),o=0===m&&M?x:r*x;o1;)r=r.parentNode;var o=n.getColorFromPalette(r.name||r.dataIndex+"",e);return t.depth>1&&U(o)&&(o=$n(o,(t.depth-1)/(i-1)*.5)),o}(r,t,i.root.height)),A(n.ensureUniqueItemVisual(r.dataIndex,"style"),o)}))}))}var BO={color:"fill",borderColor:"stroke"},FO={symbol:1,symbolSize:1,symbolKeepAspect:1,legendIcon:1,visualMeta:1,liftZ:1,decal:1},GO=Oo(),WO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(){this.currentZLevel=this.get("zlevel",!0),this.currentZ=this.get("z",!0)},e.prototype.getInitialData=function(t,e){return vx(null,this)},e.prototype.getDataParams=function(e,n,i){var r=t.prototype.getDataParams.call(this,e,n);return i&&(r.info=GO(i).info),r},e.type="series.custom",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,clip:!1},e}(mg);function HO(t,e){return e=e||[0,0],z(["x","y"],(function(n,i){var r=this.getAxis(n),o=e[i],a=t[i]/2;return"category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a))}),this)}function YO(t,e){return e=e||[0,0],z([0,1],(function(n){var i=e[n],r=t[n]/2,o=[],a=[];return o[n]=i-r,a[n]=i+r,o[1-n]=a[1-n]=e[1-n],Math.abs(this.dataToPoint(o)[n]-this.dataToPoint(a)[n])}),this)}function XO(t,e){var n=this.getAxis(),i=e instanceof Array?e[0]:e,r=(t instanceof Array?t[0]:t)/2;return"category"===n.type?n.getBandWidth():Math.abs(n.dataToCoord(i-r)-n.dataToCoord(i+r))}function UO(t,e){return e=e||[0,0],z(["Radius","Angle"],(function(n,i){var r=this["get"+n+"Axis"](),o=e[i],a=t[i]/2,s="category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a));return"Angle"===n&&(s=s*Math.PI/180),s}),this)}function ZO(t,e,n,i){return t&&(t.legacy||!1!==t.legacy&&!n&&!i&&"tspan"!==e&&("text"===e||_t(t,"text")))}function jO(t,e,n){var i,r,o,a=t;if("text"===e)o=a;else{o={},_t(a,"text")&&(o.text=a.text),_t(a,"rich")&&(o.rich=a.rich),_t(a,"textFill")&&(o.fill=a.textFill),_t(a,"textStroke")&&(o.stroke=a.textStroke),_t(a,"fontFamily")&&(o.fontFamily=a.fontFamily),_t(a,"fontSize")&&(o.fontSize=a.fontSize),_t(a,"fontStyle")&&(o.fontStyle=a.fontStyle),_t(a,"fontWeight")&&(o.fontWeight=a.fontWeight),r={type:"text",style:o,silent:!0},i={};var s=_t(a,"textPosition");n?i.position=s?a.textPosition:"inside":s&&(i.position=a.textPosition),_t(a,"textPosition")&&(i.position=a.textPosition),_t(a,"textOffset")&&(i.offset=a.textOffset),_t(a,"textRotation")&&(i.rotation=a.textRotation),_t(a,"textDistance")&&(i.distance=a.textDistance)}return qO(o,t),E(o.rich,(function(t){qO(t,t)})),{textConfig:i,textContent:r}}function qO(t,e){e&&(e.font=e.textFont||e.font,_t(e,"textStrokeWidth")&&(t.lineWidth=e.textStrokeWidth),_t(e,"textAlign")&&(t.align=e.textAlign),_t(e,"textVerticalAlign")&&(t.verticalAlign=e.textVerticalAlign),_t(e,"textLineHeight")&&(t.lineHeight=e.textLineHeight),_t(e,"textWidth")&&(t.width=e.textWidth),_t(e,"textHeight")&&(t.height=e.textHeight),_t(e,"textBackgroundColor")&&(t.backgroundColor=e.textBackgroundColor),_t(e,"textPadding")&&(t.padding=e.textPadding),_t(e,"textBorderColor")&&(t.borderColor=e.textBorderColor),_t(e,"textBorderWidth")&&(t.borderWidth=e.textBorderWidth),_t(e,"textBorderRadius")&&(t.borderRadius=e.textBorderRadius),_t(e,"textBoxShadowColor")&&(t.shadowColor=e.textBoxShadowColor),_t(e,"textBoxShadowBlur")&&(t.shadowBlur=e.textBoxShadowBlur),_t(e,"textBoxShadowOffsetX")&&(t.shadowOffsetX=e.textBoxShadowOffsetX),_t(e,"textBoxShadowOffsetY")&&(t.shadowOffsetY=e.textBoxShadowOffsetY))}function KO(t,e,n){var i=t;i.textPosition=i.textPosition||n.position||"inside",null!=n.offset&&(i.textOffset=n.offset),null!=n.rotation&&(i.textRotation=n.rotation),null!=n.distance&&(i.textDistance=n.distance);var r=i.textPosition.indexOf("inside")>=0,o=t.fill||"#000";$O(i,e);var a=null==i.textFill;return r?a&&(i.textFill=n.insideFill||"#fff",!i.textStroke&&n.insideStroke&&(i.textStroke=n.insideStroke),!i.textStroke&&(i.textStroke=o),null==i.textStrokeWidth&&(i.textStrokeWidth=2)):(a&&(i.textFill=t.fill||n.outsideFill||"#000"),!i.textStroke&&n.outsideStroke&&(i.textStroke=n.outsideStroke)),i.text=e.text,i.rich=e.rich,E(e.rich,(function(t){$O(t,t)})),i}function $O(t,e){e&&(_t(e,"fill")&&(t.textFill=e.fill),_t(e,"stroke")&&(t.textStroke=e.fill),_t(e,"lineWidth")&&(t.textStrokeWidth=e.lineWidth),_t(e,"font")&&(t.font=e.font),_t(e,"fontStyle")&&(t.fontStyle=e.fontStyle),_t(e,"fontWeight")&&(t.fontWeight=e.fontWeight),_t(e,"fontSize")&&(t.fontSize=e.fontSize),_t(e,"fontFamily")&&(t.fontFamily=e.fontFamily),_t(e,"align")&&(t.textAlign=e.align),_t(e,"verticalAlign")&&(t.textVerticalAlign=e.verticalAlign),_t(e,"lineHeight")&&(t.textLineHeight=e.lineHeight),_t(e,"width")&&(t.textWidth=e.width),_t(e,"height")&&(t.textHeight=e.height),_t(e,"backgroundColor")&&(t.textBackgroundColor=e.backgroundColor),_t(e,"padding")&&(t.textPadding=e.padding),_t(e,"borderColor")&&(t.textBorderColor=e.borderColor),_t(e,"borderWidth")&&(t.textBorderWidth=e.borderWidth),_t(e,"borderRadius")&&(t.textBorderRadius=e.borderRadius),_t(e,"shadowColor")&&(t.textBoxShadowColor=e.shadowColor),_t(e,"shadowBlur")&&(t.textBoxShadowBlur=e.shadowBlur),_t(e,"shadowOffsetX")&&(t.textBoxShadowOffsetX=e.shadowOffsetX),_t(e,"shadowOffsetY")&&(t.textBoxShadowOffsetY=e.shadowOffsetY),_t(e,"textShadowColor")&&(t.textShadowColor=e.textShadowColor),_t(e,"textShadowBlur")&&(t.textShadowBlur=e.textShadowBlur),_t(e,"textShadowOffsetX")&&(t.textShadowOffsetX=e.textShadowOffsetX),_t(e,"textShadowOffsetY")&&(t.textShadowOffsetY=e.textShadowOffsetY))}var JO={position:["x","y"],scale:["scaleX","scaleY"],origin:["originX","originY"]},QO=G(JO),tR=(V(yr,(function(t,e){return t[e]=1,t}),{}),yr.join(", "),["","style","shape","extra"]),eR=Oo();function nR(t,e,n,i,r){var o=t+"Animation",a=ph(t,i,r)||{},s=eR(e).userDuring;return a.duration>0&&(a.during=s?W(uR,{el:e,userDuring:s}):null,a.setToFinal=!0,a.scope=t),A(a,n[o]),a}function iR(t,e,n,i){var r=(i=i||{}).dataIndex,o=i.isInit,a=i.clearStyle,s=n.isAnimationEnabled(),l=eR(t),u=e.style;l.userDuring=e.during;var h={},c={};if(function(t,e,n){for(var i=0;i=0)){var c=t.getAnimationStyleProps(),p=c?c.style:null;if(p){!r&&(r=i.style={});var d=G(n);for(u=0;u0&&t.animateFrom(p,d)}else!function(t,e,n,i,r){if(r){var o=nR("update",t,e,i,n);o.duration>0&&t.animateFrom(r,o)}}(t,e,r||0,n,h);rR(t,e),u?t.dirty():t.markRedraw()}function rR(t,e){for(var n=eR(t).leaveToProps,i=0;i=0){!o&&(o=i[t]={});var p=G(a);for(h=0;hi[1]&&i.reverse(),{coordSys:{type:"polar",cx:t.cx,cy:t.cy,r:i[1],r0:i[0]},api:{coord:function(i){var r=e.dataToRadius(i[0]),o=n.dataToAngle(i[1]),a=t.coordToPoint([r,o]);return a.push(r,o*Math.PI/180),a},size:W(UO,t)}}},calendar:function(t){var e=t.getRect(),n=t.getRangeInfo();return{coordSys:{type:"calendar",x:e.x,y:e.y,width:e.width,height:e.height,cellWidth:t.getCellWidth(),cellHeight:t.getCellHeight(),rangeInfo:{start:n.start,end:n.end,weeks:n.weeks,dayCount:n.allDay}},api:{coord:function(e,n){return t.dataToPoint(e,n)}}}}};function CR(t){return t instanceof Is}function DR(t){return t instanceof Sa}var AR=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n,i){this._progressiveEls=null;var r=this._data,o=t.getData(),a=this.group,s=RR(t,o,e,n);r||a.removeAll(),o.diff(r).add((function(e){ER(n,null,e,s(e,i),t,a,o)})).remove((function(e){var n=r.getItemGraphicEl(e);n&&oR(n,GO(n).option,t)})).update((function(e,l){var u=r.getItemGraphicEl(l);ER(n,u,e,s(e,i),t,a,o)})).execute();var l=t.get("clip",!0)?SS(t.coordinateSystem,!1,t):null;l?a.setClipPath(l):a.removeClipPath(),this._data=o},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll(),this._data=null},e.prototype.incrementalRender=function(t,e,n,i,r){var o=e.getData(),a=RR(e,o,n,i),s=this._progressiveEls=[];function l(t){t.isGroup||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}for(var u=t.start;u=0?e.getStore().get(r,n):void 0}var o=e.get(i.name,n),a=i&&i.ordinalMeta;return a?a.categories[o]:o},styleEmphasis:function(n,i){0;null==i&&(i=s);var r=m(i,vR).getItemStyle(),o=x(i,vR),a=nc(o,null,null,!0,!0);a.text=o.getShallow("show")?ot(t.getFormattedLabel(i,vR),t.getFormattedLabel(i,mR),iS(e,i)):null;var l=ic(o,null,!0);return b(n,r),r=KO(r,a,l),n&&_(r,n),r.legacy=!0,r},visual:function(t,n){if(null==n&&(n=s),_t(BO,t)){var i=e.getItemVisual(n,"style");return i?i[BO[t]]:null}if(_t(FO,t))return e.getItemVisual(n,t)},barLayout:function(t){if("cartesian2d"===o.type){return function(t){var e=[],n=t.axis,i="axis0";if("category"===n.type){for(var r=n.getBandWidth(),o=0;o=c;f--){var g=e.childAt(f);WR(e,g,r)}}(t,c,n,i,r),a>=0?o.replaceAt(c,a):o.add(c),c}function VR(t,e,n){var i,r=GO(t),o=e.type,a=e.shape,s=e.style;return n.isUniversalTransitionEnabled()||null!=o&&o!==r.customGraphicType||"path"===o&&((i=a)&&(_t(i,"pathData")||_t(i,"d")))&&UR(a)!==r.customPathData||"image"===o&&_t(s,"image")&&s.image!==r.customImagePath}function BR(t,e,n){var i=e?FR(t,e):t,r=e?GR(t,i,vR):t.style,o=t.type,a=i?i.textConfig:null,s=t.textContent,l=s?e?FR(s,e):s:null;if(r&&(n.isLegacy||ZO(r,o,!!a,!!l))){n.isLegacy=!0;var u=jO(r,o,!e);!a&&u.textConfig&&(a=u.textConfig),!l&&u.textContent&&(l=u.textContent)}if(!e&&l){var h=l;!h.type&&(h.type="text")}var c=e?n[e]:n.normal;c.cfg=a,c.conOpt=l}function FR(t,e){return e?t?t[e]:null:t}function GR(t,e,n){var i=e&&e.style;return null==i&&n===vR&&t&&(i=t.styleEmphasis),i}function WR(t,e,n){e&&oR(e,GO(t).option,n)}function HR(t,e){var n=t&&t.name;return null!=n?n:"e\0\0"+e}function YR(t,e){var n=this.context,i=null!=t?n.newChildren[t]:null,r=null!=e?n.oldChildren[e]:null;zR(n.api,r,n.dataIndex,i,n.seriesModel,n.group)}function XR(t){var e=this.context,n=e.oldChildren[t];n&&oR(n,GO(n).option,e.seriesModel)}function UR(t){return t&&(t.pathData||t.d)}var ZR=Oo(),jR=T,qR=W,KR=function(){function t(){this._dragging=!1,this.animationThreshold=15}return t.prototype.render=function(t,e,n,i){var r=e.get("value"),o=e.get("status");if(this._axisModel=t,this._axisPointerModel=e,this._api=n,i||this._lastValue!==r||this._lastStatus!==o){this._lastValue=r,this._lastStatus=o;var a=this._group,s=this._handle;if(!o||"hide"===o)return a&&a.hide(),void(s&&s.hide());a&&a.show(),s&&s.show();var l={};this.makeElOption(l,r,t,e,n);var u=l.graphicKey;u!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=u;var h=this._moveAnimation=this.determineAnimation(t,e);if(a){var c=H($R,e,h);this.updatePointerEl(a,l,c),this.updateLabelEl(a,l,c,e)}else a=this._group=new zr,this.createPointerEl(a,l,t,e),this.createLabelEl(a,l,t,e),n.getZr().add(a);eN(a,e,!0),this._renderHandle(r)}},t.prototype.remove=function(t){this.clear(t)},t.prototype.dispose=function(t){this.clear(t)},t.prototype.determineAnimation=function(t,e){var n=e.get("animation"),i=t.axis,r="category"===i.type,o=e.get("snap");if(!o&&!r)return!1;if("auto"===n||null==n){var a=this.animationThreshold;if(r&&i.getBandWidth()>a)return!0;if(o){var s=pI(t).seriesDataCount,l=i.getExtent();return Math.abs(l[0]-l[1])/s>a}return!1}return!0===n},t.prototype.makeElOption=function(t,e,n,i,r){},t.prototype.createPointerEl=function(t,e,n,i){var r=e.pointer;if(r){var o=ZR(t).pointerEl=new Kh[r.type](jR(e.pointer));t.add(o)}},t.prototype.createLabelEl=function(t,e,n,i){if(e.label){var r=ZR(t).labelEl=new Fs(jR(e.label));t.add(r),QR(r,i)}},t.prototype.updatePointerEl=function(t,e,n){var i=ZR(t).pointerEl;i&&e.pointer&&(i.setStyle(e.pointer.style),n(i,{shape:e.pointer.shape}))},t.prototype.updateLabelEl=function(t,e,n,i){var r=ZR(t).labelEl;r&&(r.setStyle(e.label.style),n(r,{x:e.label.x,y:e.label.y}),QR(r,i))},t.prototype._renderHandle=function(t){if(!this._dragging&&this.updateHandleTransform){var e,n=this._axisPointerModel,i=this._api.getZr(),r=this._handle,o=n.getModel("handle"),a=n.get("status");if(!o.get("show")||!a||"hide"===a)return r&&i.remove(r),void(this._handle=null);this._handle||(e=!0,r=this._handle=Hh(o.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(t){de(t.event)},onmousedown:qR(this._onHandleDragMove,this,0,0),drift:qR(this._onHandleDragMove,this),ondragend:qR(this._onHandleDragEnd,this)}),i.add(r)),eN(r,n,!1),r.setStyle(o.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var s=o.get("size");Y(s)||(s=[s,s]),r.scaleX=s[0]/2,r.scaleY=s[1]/2,Fg(this,"_doDispatchAxisPointer",o.get("throttle")||0,"fixRate"),this._moveHandleToValue(t,e)}},t.prototype._moveHandleToValue=function(t,e){$R(this._axisPointerModel,!e&&this._moveAnimation,this._handle,tN(this.getHandleTransform(t,this._axisModel,this._axisPointerModel)))},t.prototype._onHandleDragMove=function(t,e){var n=this._handle;if(n){this._dragging=!0;var i=this.updateHandleTransform(tN(n),[t,e],this._axisModel,this._axisPointerModel);this._payloadInfo=i,n.stopAnimation(),n.attr(tN(i)),ZR(n).lastProp=null,this._doDispatchAxisPointer()}},t.prototype._doDispatchAxisPointer=function(){if(this._handle){var t=this._payloadInfo,e=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:t.cursorPoint[0],y:t.cursorPoint[1],tooltipOption:t.tooltipOption,axesInfo:[{axisDim:e.axis.dim,axisIndex:e.componentIndex}]})}},t.prototype._onHandleDragEnd=function(){if(this._dragging=!1,this._handle){var t=this._axisPointerModel.get("value");this._moveHandleToValue(t),this._api.dispatchAction({type:"hideTip"})}},t.prototype.clear=function(t){this._lastValue=null,this._lastStatus=null;var e=t.getZr(),n=this._group,i=this._handle;e&&n&&(this._lastGraphicKey=null,n&&e.remove(n),i&&e.remove(i),this._group=null,this._handle=null,this._payloadInfo=null),Gg(this,"_doDispatchAxisPointer")},t.prototype.doClear=function(){},t.prototype.buildLabel=function(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}},t}();function $R(t,e,n,i){JR(ZR(n).lastProp,i)||(ZR(n).lastProp=i,e?fh(n,i,t):(n.stopAnimation(),n.attr(i)))}function JR(t,e){if(q(t)&&q(e)){var n=!0;return E(e,(function(e,i){n=n&&JR(t[i],e)})),!!n}return t===e}function QR(t,e){t[e.get(["label","show"])?"show":"hide"]()}function tN(t){return{x:t.x||0,y:t.y||0,rotation:t.rotation||0}}function eN(t,e,n){var i=e.get("z"),r=e.get("zlevel");t&&t.traverse((function(t){"group"!==t.type&&(null!=i&&(t.z=i),null!=r&&(t.zlevel=r),t.silent=n)}))}function nN(t){var e,n=t.get("type"),i=t.getModel(n+"Style");return"line"===n?(e=i.getLineStyle()).fill=null:"shadow"===n&&((e=i.getAreaStyle()).stroke=null),e}function iN(t,e,n,i,r){var o=rN(n.get("value"),e.axis,e.ecModel,n.get("seriesDataIndices"),{precision:n.get(["label","precision"]),formatter:n.get(["label","formatter"])}),a=n.getModel("label"),s=fp(a.get("padding")||0),l=a.getFont(),u=br(o,l),h=r.position,c=u.width+s[1]+s[3],p=u.height+s[0]+s[2],d=r.align;"right"===d&&(h[0]-=c),"center"===d&&(h[0]-=c/2);var f=r.verticalAlign;"bottom"===f&&(h[1]-=p),"middle"===f&&(h[1]-=p/2),function(t,e,n,i){var r=i.getWidth(),o=i.getHeight();t[0]=Math.min(t[0]+e,r)-e,t[1]=Math.min(t[1]+n,o)-n,t[0]=Math.max(t[0],0),t[1]=Math.max(t[1],0)}(h,c,p,i);var g=a.get("backgroundColor");g&&"auto"!==g||(g=e.get(["axisLine","lineStyle","color"])),t.label={x:h[0],y:h[1],style:nc(a,{text:o,font:l,fill:a.getTextColor(),padding:s,backgroundColor:g}),z2:10}}function rN(t,e,n,i,r){t=e.scale.parse(t);var o=e.scale.getLabel({value:t},{precision:r.precision}),a=r.formatter;if(a){var s={value:__(e,{value:t}),axisDimension:e.dim,axisIndex:e.index,seriesData:[]};E(i,(function(t){var e=n.getSeriesByIndex(t.seriesIndex),i=t.dataIndexInside,r=e&&e.getDataParams(i);r&&s.seriesData.push(r)})),U(a)?o=a.replace("{value}",o):X(a)&&(o=a(s))}return o}function oN(t,e,n){var i=[1,0,0,1,0,0];return Se(i,i,n.rotation),we(i,i,n.position),zh([t.dataToCoord(e),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],i)}function aN(t,e,n,i,r,o){var a=iI.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=r.get(["label","margin"]),iN(e,i,r,o,{position:oN(i.axis,t,n),align:a.textAlign,verticalAlign:a.textVerticalAlign})}function sN(t,e,n){return{x1:t[n=n||0],y1:t[1-n],x2:e[n],y2:e[1-n]}}function lN(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}}function uN(t,e,n,i,r,o){return{cx:t,cy:e,r0:n,r:i,startAngle:r,endAngle:o,clockwise:!0}}var hN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.grid,s=i.get("type"),l=cN(a,o).getOtherAxis(o).getGlobalExtent(),u=o.toGlobalCoord(o.dataToCoord(e,!0));if(s&&"none"!==s){var h=nN(i),c=pN[s](o,u,l);c.style=h,t.graphicKey=c.type,t.pointer=c}aN(e,t,ZM(a.model,n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=ZM(e.axis.grid.model,e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=oN(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.grid,a=r.getGlobalExtent(!0),s=cN(o,r).getOtherAxis(r).getGlobalExtent(),l="x"===r.dim?0:1,u=[t.x,t.y];u[l]+=e[l],u[l]=Math.min(a[1],u[l]),u[l]=Math.max(a[0],u[l]);var h=(s[1]+s[0])/2,c=[h,h];c[l]=u[l];return{x:u[0],y:u[1],rotation:t.rotation,cursorPoint:c,tooltipOption:[{verticalAlign:"middle"},{align:"center"}][l]}},e}(KR);function cN(t,e){var n={};return n[e.dim+"AxisIndex"]=e.index,t.getCartesian(n)}var pN={line:function(t,e,n){return{type:"Line",subPixelOptimize:!0,shape:sN([e,n[0]],[e,n[1]],dN(t))}},shadow:function(t,e,n){var i=Math.max(1,t.getBandWidth()),r=n[1]-n[0];return{type:"Rect",shape:lN([e-i/2,n[0]],[i,r],dN(t))}}};function dN(t){return"x"===t.dim?0:1}var fN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="axisPointer",e.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,triggerEmphasis:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:"#B9BEC9",width:1,type:"dashed"},shadowStyle:{color:"rgba(210,219,238,0.2)"},label:{show:!0,formatter:null,precision:"auto",margin:3,color:"#fff",padding:[5,7,5,7],backgroundColor:"auto",borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:"#333",shadowBlur:3,shadowColor:"#aaa",shadowOffsetX:0,shadowOffsetY:2,throttle:40}},e}(Rp),gN=Oo(),yN=E;function vN(t,e,n){if(!r.node){var i=e.getZr();gN(i).records||(gN(i).records={}),function(t,e){if(gN(t).initialized)return;function n(n,i){t.on(n,(function(n){var r=function(t){var e={showTip:[],hideTip:[]},n=function(i){var r=e[i.type];r?r.push(i):(i.dispatchAction=n,t.dispatchAction(i))};return{dispatchAction:n,pendings:e}}(e);yN(gN(t).records,(function(t){t&&i(t,n,r.dispatchAction)})),function(t,e){var n,i=t.showTip.length,r=t.hideTip.length;i?n=t.showTip[i-1]:r&&(n=t.hideTip[r-1]);n&&(n.dispatchAction=null,e.dispatchAction(n))}(r.pendings,e)}))}gN(t).initialized=!0,n("click",H(xN,"click")),n("mousemove",H(xN,"mousemove")),n("globalout",mN)}(i,e),(gN(i).records[t]||(gN(i).records[t]={})).handler=n}}function mN(t,e,n){t.handler("leave",null,n)}function xN(t,e,n,i){e.handler(t,n,i)}function _N(t,e){if(!r.node){var n=e.getZr();(gN(n).records||{})[t]&&(gN(n).records[t]=null)}}var bN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=e.getComponent("tooltip"),r=t.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click";vN("axisPointer",n,(function(t,e,n){"none"!==r&&("leave"===t||r.indexOf(t)>=0)&&n({type:"updateAxisPointer",currTrigger:t,x:e&&e.offsetX,y:e&&e.offsetY})}))},e.prototype.remove=function(t,e){_N("axisPointer",e)},e.prototype.dispose=function(t,e){_N("axisPointer",e)},e.type="axisPointer",e}(Tg);function wN(t,e){var n,i=[],r=t.seriesIndex;if(null==r||!(n=e.getSeriesByIndex(r)))return{point:[]};var o=n.getData(),a=Po(o,t);if(null==a||a<0||Y(a))return{point:[]};var s=o.getItemGraphicEl(a),l=n.coordinateSystem;if(n.getTooltipPosition)i=n.getTooltipPosition(a)||[];else if(l&&l.dataToPoint)if(t.isStacked){var u=l.getBaseAxis(),h=l.getOtherAxis(u).dim,c=u.dim,p="x"===h||"radius"===h?1:0,d=o.mapDimension(c),f=[];f[p]=o.get(d,a),f[1-p]=o.get(o.getCalculationInfo("stackResultDimension"),a),i=l.dataToPoint(f)||[]}else i=l.dataToPoint(o.getValues(z(l.dimensions,(function(t){return o.mapDimension(t)})),a))||[];else if(s){var g=s.getBoundingRect().clone();g.applyTransform(s.transform),i=[g.x+g.width/2,g.y+g.height/2]}return{point:i,el:s}}var SN=Oo();function MN(t,e,n){var i=t.currTrigger,r=[t.x,t.y],o=t,a=t.dispatchAction||W(n.dispatchAction,n),s=e.getComponent("axisPointer").coordSysAxesInfo;if(s){AN(r)&&(r=wN({seriesIndex:o.seriesIndex,dataIndex:o.dataIndex},e).point);var l=AN(r),u=o.axesInfo,h=s.axesInfo,c="leave"===i||AN(r),p={},d={},f={list:[],map:{}},g={showPointer:H(TN,d),showTooltip:H(CN,f)};E(s.coordSysMap,(function(t,e){var n=l||t.containPoint(r);E(s.coordSysAxesInfo[e],(function(t,e){var i=t.axis,o=function(t,e){for(var n=0;n<(t||[]).length;n++){var i=t[n];if(e.axis.dim===i.axisDim&&e.axis.model.componentIndex===i.axisIndex)return i}}(u,t);if(!c&&n&&(!u||o)){var a=o&&o.value;null!=a||l||(a=i.pointToData(r)),null!=a&&IN(t,a,g,!1,p)}}))}));var y={};return E(h,(function(t,e){var n=t.linkGroup;n&&!d[e]&&E(n.axesInfo,(function(e,i){var r=d[i];if(e!==t&&r){var o=r.value;n.mapper&&(o=t.axis.scale.parse(n.mapper(o,DN(e),DN(t)))),y[t.key]=o}}))})),E(y,(function(t,e){IN(h[e],t,g,!0,p)})),function(t,e,n){var i=n.axesInfo=[];E(e,(function(e,n){var r=e.axisPointerModel.option,o=t[n];o?(!e.useHandle&&(r.status="show"),r.value=o.value,r.seriesDataIndices=(o.payloadBatch||[]).slice()):!e.useHandle&&(r.status="hide"),"show"===r.status&&i.push({axisDim:e.axis.dim,axisIndex:e.axis.model.componentIndex,value:r.value})}))}(d,h,p),function(t,e,n,i){if(AN(e)||!t.list.length)return void i({type:"hideTip"});var r=((t.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};i({type:"showTip",escapeConnect:!0,x:e[0],y:e[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:r.dataIndexInside,dataIndex:r.dataIndex,seriesIndex:r.seriesIndex,dataByCoordSys:t.list})}(f,r,t,a),function(t,e,n){var i=n.getZr(),r="axisPointerLastHighlights",o=SN(i)[r]||{},a=SN(i)[r]={};E(t,(function(t,e){var n=t.axisPointerModel.option;"show"===n.status&&t.triggerEmphasis&&E(n.seriesDataIndices,(function(t){var e=t.seriesIndex+" | "+t.dataIndex;a[e]=t}))}));var s=[],l=[];E(o,(function(t,e){!a[e]&&l.push(t)})),E(a,(function(t,e){!o[e]&&s.push(t)})),l.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:l}),s.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:s})}(h,0,n),p}}function IN(t,e,n,i,r){var o=t.axis;if(!o.scale.isBlank()&&o.containData(e))if(t.involveSeries){var a=function(t,e){var n=e.axis,i=n.dim,r=t,o=[],a=Number.MAX_VALUE,s=-1;return E(e.seriesModels,(function(e,l){var u,h,c=e.getData().mapDimensionsAll(i);if(e.getAxisTooltipData){var p=e.getAxisTooltipData(c,t,n);h=p.dataIndices,u=p.nestestValue}else{if(!(h=e.getData().indicesOfNearest(c[0],t,"category"===n.type?.5:null)).length)return;u=e.getData().get(c[0],h[0])}if(null!=u&&isFinite(u)){var d=t-u,f=Math.abs(d);f<=a&&((f=0&&s<0)&&(a=f,s=d,r=u,o.length=0),E(h,(function(t){o.push({seriesIndex:e.seriesIndex,dataIndexInside:t,dataIndex:e.getData().getRawIndex(t)})})))}})),{payloadBatch:o,snapToValue:r}}(e,t),s=a.payloadBatch,l=a.snapToValue;s[0]&&null==r.seriesIndex&&A(r,s[0]),!i&&t.snap&&o.containData(l)&&null!=l&&(e=l),n.showPointer(t,e,s),n.showTooltip(t,a,l)}else n.showPointer(t,e)}function TN(t,e,n,i){t[e.key]={value:n,payloadBatch:i}}function CN(t,e,n,i){var r=n.payloadBatch,o=e.axis,a=o.model,s=e.axisPointerModel;if(e.triggerTooltip&&r.length){var l=e.coordSys.model,u=fI(l),h=t.map[u];h||(h=t.map[u]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},t.list.push(h)),h.dataByAxis.push({axisDim:o.dim,axisIndex:a.componentIndex,axisType:a.type,axisId:a.id,value:i,valueLabelOpt:{precision:s.get(["label","precision"]),formatter:s.get(["label","formatter"])},seriesDataIndices:r.slice()})}}function DN(t){var e=t.axis.model,n={},i=n.axisDim=t.axis.dim;return n.axisIndex=n[i+"AxisIndex"]=e.componentIndex,n.axisName=n[i+"AxisName"]=e.name,n.axisId=n[i+"AxisId"]=e.id,n}function AN(t){return!t||null==t[0]||isNaN(t[0])||null==t[1]||isNaN(t[1])}function kN(t){yI.registerAxisPointerClass("CartesianAxisPointer",hN),t.registerComponentModel(fN),t.registerComponentView(bN),t.registerPreprocessor((function(t){if(t){(!t.axisPointer||0===t.axisPointer.length)&&(t.axisPointer={});var e=t.axisPointer.link;e&&!Y(e)&&(t.axisPointer.link=[e])}})),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,(function(t,e){t.getComponent("axisPointer").coordSysAxesInfo=uI(t,e)})),t.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},MN)}var LN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis;"angle"===o.dim&&(this.animationThreshold=Math.PI/18);var a=o.polar,s=a.getOtherAxis(o).getExtent(),l=o.dataToCoord(e),u=i.get("type");if(u&&"none"!==u){var h=nN(i),c=PN[u](o,a,l,s);c.style=h,t.graphicKey=c.type,t.pointer=c}var p=function(t,e,n,i,r){var o=e.axis,a=o.dataToCoord(t),s=i.getAngleAxis().getExtent()[0];s=s/180*Math.PI;var l,u,h,c=i.getRadiusAxis().getExtent();if("radius"===o.dim){var p=[1,0,0,1,0,0];Se(p,p,s),we(p,p,[i.cx,i.cy]),l=zh([a,-r],p);var d=e.getModel("axisLabel").get("rotate")||0,f=iI.innerTextLayout(s,d*Math.PI/180,-1);u=f.textAlign,h=f.textVerticalAlign}else{var g=c[1];l=i.coordToPoint([g+r,a]);var y=i.cx,v=i.cy;u=Math.abs(l[0]-y)/g<.3?"center":l[0]>y?"left":"right",h=Math.abs(l[1]-v)/g<.3?"middle":l[1]>v?"top":"bottom"}return{position:l,align:u,verticalAlign:h}}(e,n,0,a,i.get(["label","margin"]));iN(t,n,i,r,p)},e}(KR);var PN={line:function(t,e,n,i){return"angle"===t.dim?{type:"Line",shape:sN(e.coordToPoint([i[0],n]),e.coordToPoint([i[1],n]))}:{type:"Circle",shape:{cx:e.cx,cy:e.cy,r:n}}},shadow:function(t,e,n,i){var r=Math.max(1,t.getBandWidth()),o=Math.PI/180;return"angle"===t.dim?{type:"Sector",shape:uN(e.cx,e.cy,i[0],i[1],(-n-r/2)*o,(r/2-n)*o)}:{type:"Sector",shape:uN(e.cx,e.cy,n-r/2,n+r/2,0,2*Math.PI)}}},ON=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.findAxisModel=function(t){var e;return this.ecModel.eachComponent(t,(function(t){t.getCoordSysModel()===this&&(e=t)}),this),e},e.type="polar",e.dependencies=["radiusAxis","angleAxis"],e.defaultOption={z:0,center:["50%","50%"],radius:"80%"},e}(Rp),RN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("polar",zo).models[0]},e.type="polarAxis",e}(Rp);R(RN,I_);var NN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="angleAxis",e}(RN),EN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="radiusAxis",e}(RN),zN=function(t){function e(e,n){return t.call(this,"radius",e,n)||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e}(nb);zN.prototype.dataToRadius=nb.prototype.dataToCoord,zN.prototype.radiusToData=nb.prototype.coordToData;var VN=Oo(),BN=function(t){function e(e,n){return t.call(this,"angle",e,n||[0,360])||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e.prototype.calculateCategoryInterval=function(){var t=this,e=t.getLabelModel(),n=t.scale,i=n.getExtent(),r=n.count();if(i[1]-i[0]<1)return 0;var o=i[0],a=t.dataToCoord(o+1)-t.dataToCoord(o),s=Math.abs(a),l=br(null==o?"":o+"",e.getFont(),"center","top"),u=Math.max(l.height,7)/s;isNaN(u)&&(u=1/0);var h=Math.max(0,Math.floor(u)),c=VN(t.model),p=c.lastAutoInterval,d=c.lastTickCount;return null!=p&&null!=d&&Math.abs(p-h)<=1&&Math.abs(d-r)<=1&&p>h?h=p:(c.lastTickCount=r,c.lastAutoInterval=h),h},e}(nb);BN.prototype.dataToAngle=nb.prototype.dataToCoord,BN.prototype.angleToData=nb.prototype.coordToData;var FN=["radius","angle"],GN=function(){function t(t){this.dimensions=FN,this.type="polar",this.cx=0,this.cy=0,this._radiusAxis=new zN,this._angleAxis=new BN,this.axisPointerEnabled=!0,this.name=t||"",this._radiusAxis.polar=this._angleAxis.polar=this}return t.prototype.containPoint=function(t){var e=this.pointToCoord(t);return this._radiusAxis.contain(e[0])&&this._angleAxis.contain(e[1])},t.prototype.containData=function(t){return this._radiusAxis.containData(t[0])&&this._angleAxis.containData(t[1])},t.prototype.getAxis=function(t){return this["_"+t+"Axis"]},t.prototype.getAxes=function(){return[this._radiusAxis,this._angleAxis]},t.prototype.getAxesByScale=function(t){var e=[],n=this._angleAxis,i=this._radiusAxis;return n.scale.type===t&&e.push(n),i.scale.type===t&&e.push(i),e},t.prototype.getAngleAxis=function(){return this._angleAxis},t.prototype.getRadiusAxis=function(){return this._radiusAxis},t.prototype.getOtherAxis=function(t){var e=this._angleAxis;return t===e?this._radiusAxis:e},t.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},t.prototype.getTooltipAxes=function(t){var e=null!=t&&"auto"!==t?this.getAxis(t):this.getBaseAxis();return{baseAxes:[e],otherAxes:[this.getOtherAxis(e)]}},t.prototype.dataToPoint=function(t,e){return this.coordToPoint([this._radiusAxis.dataToRadius(t[0],e),this._angleAxis.dataToAngle(t[1],e)])},t.prototype.pointToData=function(t,e){var n=this.pointToCoord(t);return[this._radiusAxis.radiusToData(n[0],e),this._angleAxis.angleToData(n[1],e)]},t.prototype.pointToCoord=function(t){var e=t[0]-this.cx,n=t[1]-this.cy,i=this.getAngleAxis(),r=i.getExtent(),o=Math.min(r[0],r[1]),a=Math.max(r[0],r[1]);i.inverse?o=a-360:a=o+360;var s=Math.sqrt(e*e+n*n);e/=s,n/=s;for(var l=Math.atan2(-n,e)/Math.PI*180,u=la;)l+=360*u;return[s,l]},t.prototype.coordToPoint=function(t){var e=t[0],n=t[1]/180*Math.PI;return[Math.cos(n)*e+this.cx,-Math.sin(n)*e+this.cy]},t.prototype.getArea=function(){var t=this.getAngleAxis(),e=this.getRadiusAxis().getExtent().slice();e[0]>e[1]&&e.reverse();var n=t.getExtent(),i=Math.PI/180;return{cx:this.cx,cy:this.cy,r0:e[0],r:e[1],startAngle:-n[0]*i,endAngle:-n[1]*i,clockwise:t.inverse,contain:function(t,e){var n=t-this.cx,i=e-this.cy,r=n*n+i*i-1e-4,o=this.r,a=this.r0;return r<=o*o&&r>=a*a}}},t.prototype.convertToPixel=function(t,e,n){return WN(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return WN(e)===this?this.pointToData(n):null},t}();function WN(t){var e=t.seriesModel,n=t.polarModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}function HN(t,e){var n=this,i=n.getAngleAxis(),r=n.getRadiusAxis();if(i.scale.setExtent(1/0,-1/0),r.scale.setExtent(1/0,-1/0),t.eachSeries((function(t){if(t.coordinateSystem===n){var e=t.getData();E(M_(e,"radius"),(function(t){r.scale.unionExtentFromData(e,t)})),E(M_(e,"angle"),(function(t){i.scale.unionExtentFromData(e,t)}))}})),v_(i.scale,i.model),v_(r.scale,r.model),"category"===i.type&&!i.onBand){var o=i.getExtent(),a=360/i.scale.count();i.inverse?o[1]+=a:o[1]-=a,i.setExtent(o[0],o[1])}}function YN(t,e){if(t.type=e.get("type"),t.scale=m_(e),t.onBand=e.get("boundaryGap")&&"category"===t.type,t.inverse=e.get("inverse"),function(t){return"angleAxis"===t.mainType}(e)){t.inverse=t.inverse!==e.get("clockwise");var n=e.get("startAngle");t.setExtent(n,n+(t.inverse?-360:360))}e.axis=t,t.model=e}var XN={dimensions:FN,create:function(t,e){var n=[];return t.eachComponent("polar",(function(t,i){var r=new GN(i+"");r.update=HN;var o=r.getRadiusAxis(),a=r.getAngleAxis(),s=t.findAxisModel("radiusAxis"),l=t.findAxisModel("angleAxis");YN(o,s),YN(a,l),function(t,e,n){var i=e.get("center"),r=n.getWidth(),o=n.getHeight();t.cx=Ur(i[0],r),t.cy=Ur(i[1],o);var a=t.getRadiusAxis(),s=Math.min(r,o)/2,l=e.get("radius");null==l?l=[0,"100%"]:Y(l)||(l=[0,l]);var u=[Ur(l[0],s),Ur(l[1],s)];a.inverse?a.setExtent(u[1],u[0]):a.setExtent(u[0],u[1])}(r,t,e),n.push(r),t.coordinateSystem=r,r.model=t})),t.eachSeries((function(t){if("polar"===t.get("coordinateSystem")){var e=t.getReferringComponents("polar",zo).models[0];0,t.coordinateSystem=e.coordinateSystem}})),n}},UN=["axisLine","axisLabel","axisTick","minorTick","splitLine","minorSplitLine","splitArea"];function ZN(t,e,n){e[1]>e[0]&&(e=e.slice().reverse());var i=t.coordToPoint([e[0],n]),r=t.coordToPoint([e[1],n]);return{x1:i[0],y1:i[1],x2:r[0],y2:r[1]}}function jN(t){return t.getRadiusAxis().inverse?0:1}function qN(t){var e=t[0],n=t[t.length-1];e&&n&&Math.abs(Math.abs(e.coord-n.coord)-360)<1e-4&&t.pop()}var KN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="PolarAxisPointer",n}return n(e,t),e.prototype.render=function(t,e){if(this.group.removeAll(),t.get("show")){var n=t.axis,i=n.polar,r=i.getRadiusAxis().getExtent(),o=n.getTicksCoords(),a=n.getMinorTicksCoords(),s=z(n.getViewLabels(),(function(t){t=T(t);var e=n.scale,i="ordinal"===e.type?e.getRawOrdinalNumber(t.tickValue):t.tickValue;return t.coord=n.dataToCoord(i),t}));qN(s),qN(o),E(UN,(function(e){!t.get([e,"show"])||n.scale.isBlank()&&"axisLine"!==e||$N[e](this.group,t,i,o,a,r,s)}),this)}},e.type="angleAxis",e}(yI),$N={axisLine:function(t,e,n,i,r,o){var a,s=e.getModel(["axisLine","lineStyle"]),l=jN(n),u=l?0:1;(a=0===o[u]?new _u({shape:{cx:n.cx,cy:n.cy,r:o[l]},style:s.getLineStyle(),z2:1,silent:!0}):new Bu({shape:{cx:n.cx,cy:n.cy,r:o[l],r0:o[u]},style:s.getLineStyle(),z2:1,silent:!0})).style.fill=null,t.add(a)},axisTick:function(t,e,n,i,r,o){var a=e.getModel("axisTick"),s=(a.get("inside")?-1:1)*a.get("length"),l=o[jN(n)],u=z(i,(function(t){return new Zu({shape:ZN(n,[l,l+s],t.coord)})}));t.add(Ph(u,{style:k(a.getModel("lineStyle").getLineStyle(),{stroke:e.get(["axisLine","lineStyle","color"])})}))},minorTick:function(t,e,n,i,r,o){if(r.length){for(var a=e.getModel("axisTick"),s=e.getModel("minorTick"),l=(a.get("inside")?-1:1)*s.get("length"),u=o[jN(n)],h=[],c=0;cf?"left":"right",v=Math.abs(d[1]-g)/p<.3?"middle":d[1]>g?"top":"bottom";if(s&&s[c]){var m=s[c];q(m)&&m.textStyle&&(a=new Mc(m.textStyle,l,l.ecModel))}var x=new Fs({silent:iI.isLabelSilent(e),style:nc(a,{x:d[0],y:d[1],fill:a.getTextColor()||e.get(["axisLine","lineStyle","color"]),text:i.formattedLabel,align:y,verticalAlign:v})});if(t.add(x),h){var _=iI.makeAxisEventDataBase(e);_.targetType="axisLabel",_.value=i.rawLabel,Qs(x).eventData=_}}),this)},splitLine:function(t,e,n,i,r,o){var a=e.getModel("splitLine").getModel("lineStyle"),s=a.get("color"),l=0;s=s instanceof Array?s:[s];for(var u=[],h=0;h=0?"p":"n",T=_;m&&(i[s][M]||(i[s][M]={p:_,n:_}),T=i[s][M][I]);var C=void 0,D=void 0,A=void 0,k=void 0;if("radius"===c.dim){var L=c.dataToCoord(S)-_,P=o.dataToCoord(M);Math.abs(L)=k})}}}))}var oE={startAngle:90,clockwise:!0,splitNumber:12,axisLabel:{rotate:0}},aE={splitNumber:5},sE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="polar",e}(Tg);function lE(t,e){e=e||{};var n=t.coordinateSystem,i=t.axis,r={},o=i.position,a=i.orient,s=n.getRect(),l=[s.x,s.x+s.width,s.y,s.y+s.height],u={horizontal:{top:l[2],bottom:l[3]},vertical:{left:l[0],right:l[1]}};r.position=["vertical"===a?u.vertical[o]:l[0],"horizontal"===a?u.horizontal[o]:l[3]];r.rotation=Math.PI/2*{horizontal:0,vertical:1}[a];r.labelDirection=r.tickDirection=r.nameDirection={top:-1,bottom:1,right:1,left:-1}[o],t.get(["axisTick","inside"])&&(r.tickDirection=-r.tickDirection),it(e.labelInside,t.get(["axisLabel","inside"]))&&(r.labelDirection=-r.labelDirection);var h=e.rotate;return null==h&&(h=t.get(["axisLabel","rotate"])),r.labelRotation="top"===o?-h:h,r.z2=1,r}var uE=["axisLine","axisTickLabel","axisName"],hE=["splitArea","splitLine"],cE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="SingleAxisPointer",n}return n(e,t),e.prototype.render=function(e,n,i,r){var o=this.group;o.removeAll();var a=this._axisGroup;this._axisGroup=new zr;var s=lE(e),l=new iI(e,s);E(uE,l.add,l),o.add(this._axisGroup),o.add(l.getGroup()),E(hE,(function(t){e.get([t,"show"])&&pE[t](this,this.group,this._axisGroup,e)}),this),Fh(a,this._axisGroup,e),t.prototype.render.call(this,e,n,i,r)},e.prototype.remove=function(){xI(this)},e.type="singleAxis",e}(yI),pE={splitLine:function(t,e,n,i){var r=i.axis;if(!r.scale.isBlank()){var o=i.getModel("splitLine"),a=o.getModel("lineStyle"),s=a.get("color");s=s instanceof Array?s:[s];for(var l=a.get("width"),u=i.coordinateSystem.getRect(),h=r.isHorizontal(),c=[],p=0,d=r.getTicksCoords({tickModel:o}),f=[],g=[],y=0;y=e.y&&t[1]<=e.y+e.height:n.contain(n.toLocalCoord(t[1]))&&t[0]>=e.y&&t[0]<=e.y+e.height},t.prototype.pointToData=function(t){var e=this.getAxis();return[e.coordToData(e.toLocalCoord(t["horizontal"===e.orient?0:1]))]},t.prototype.dataToPoint=function(t){var e=this.getAxis(),n=this.getRect(),i=[],r="horizontal"===e.orient?0:1;return t instanceof Array&&(t=t[0]),i[r]=e.toGlobalCoord(e.dataToCoord(+t)),i[1-r]=0===r?n.y+n.height/2:n.x+n.width/2,i},t.prototype.convertToPixel=function(t,e,n){return vE(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return vE(e)===this?this.pointToData(n):null},t}();function vE(t){var e=t.seriesModel,n=t.singleAxisModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}var mE={create:function(t,e){var n=[];return t.eachComponent("singleAxis",(function(i,r){var o=new yE(i,t,e);o.name="single_"+r,o.resize(i,e),i.coordinateSystem=o,n.push(o)})),t.eachSeries((function(t){if("singleAxis"===t.get("coordinateSystem")){var e=t.getReferringComponents("singleAxis",zo).models[0];t.coordinateSystem=e&&e.coordinateSystem}})),n},dimensions:gE},xE=["x","y"],_E=["width","height"],bE=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.coordinateSystem,s=ME(a,1-SE(o)),l=a.dataToPoint(e)[0],u=i.get("type");if(u&&"none"!==u){var h=nN(i),c=wE[u](o,l,s);c.style=h,t.graphicKey=c.type,t.pointer=c}aN(e,t,lE(n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=lE(e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=oN(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.coordinateSystem,a=SE(r),s=ME(o,a),l=[t.x,t.y];l[a]+=e[a],l[a]=Math.min(s[1],l[a]),l[a]=Math.max(s[0],l[a]);var u=ME(o,1-a),h=(u[1]+u[0])/2,c=[h,h];return c[a]=l[a],{x:l[0],y:l[1],rotation:t.rotation,cursorPoint:c,tooltipOption:{verticalAlign:"middle"}}},e}(KR),wE={line:function(t,e,n){return{type:"Line",subPixelOptimize:!0,shape:sN([e,n[0]],[e,n[1]],SE(t))}},shadow:function(t,e,n){var i=t.getBandWidth(),r=n[1]-n[0];return{type:"Rect",shape:lN([e-i/2,n[0]],[i,r],SE(t))}}};function SE(t){return t.isHorizontal()?0:1}function ME(t,e){var n=t.getRect();return[n[xE[e]],n[xE[e]]+n[_E[e]]]}var IE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="single",e}(Tg);var TE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e,n,i){var r=Lp(e);t.prototype.init.apply(this,arguments),CE(e,r)},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),CE(this.option,e)},e.prototype.getCellSize=function(){return this.option.cellSize},e.type="calendar",e.defaultOption={z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:"#000",width:1,type:"solid"}},itemStyle:{color:"#fff",borderWidth:1,borderColor:"#ccc"},dayLabel:{show:!0,firstDay:0,position:"start",margin:"50%",color:"#000"},monthLabel:{show:!0,position:"start",margin:5,align:"center",formatter:null,color:"#000"},yearLabel:{show:!0,position:null,margin:30,formatter:null,color:"#ccc",fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}},e}(Rp);function CE(t,e){var n,i=t.cellSize;1===(n=Y(i)?i:t.cellSize=[i,i]).length&&(n[1]=n[0]);var r=z([0,1],(function(t){return function(t,e){return null!=t[Mp[e][0]]||null!=t[Mp[e][1]]&&null!=t[Mp[e][2]]}(e,t)&&(n[t]="auto"),null!=n[t]&&"auto"!==n[t]}));kp(t,e,{type:"box",ignoreSize:r})}var DE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=this.group;i.removeAll();var r=t.coordinateSystem,o=r.getRangeInfo(),a=r.getOrient(),s=e.getLocaleModel();this._renderDayRect(t,o,i),this._renderLines(t,o,a,i),this._renderYearText(t,o,a,i),this._renderMonthText(t,s,a,i),this._renderWeekText(t,s,o,a,i)},e.prototype._renderDayRect=function(t,e,n){for(var i=t.coordinateSystem,r=t.getModel("itemStyle").getItemStyle(),o=i.getCellWidth(),a=i.getCellHeight(),s=e.start.time;s<=e.end.time;s=i.getNextNDay(s,1).time){var l=i.dataToRect([s],!1).tl,u=new zs({shape:{x:l[0],y:l[1],width:o,height:a},cursor:"default",style:r});n.add(u)}},e.prototype._renderLines=function(t,e,n,i){var r=this,o=t.coordinateSystem,a=t.getModel(["splitLine","lineStyle"]).getLineStyle(),s=t.get(["splitLine","show"]),l=a.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var u=e.start,h=0;u.time<=e.end.time;h++){p(u.formatedDate),0===h&&(u=o.getDateInfo(e.start.y+"-"+e.start.m));var c=u.date;c.setMonth(c.getMonth()+1),u=o.getDateInfo(c)}function p(e){r._firstDayOfMonth.push(o.getDateInfo(e)),r._firstDayPoints.push(o.dataToRect([e],!1).tl);var l=r._getLinePointsOfOneWeek(t,e,n);r._tlpoints.push(l[0]),r._blpoints.push(l[l.length-1]),s&&r._drawSplitline(l,a,i)}p(o.getNextNDay(e.end.time,1).formatedDate),s&&this._drawSplitline(r._getEdgesPoints(r._tlpoints,l,n),a,i),s&&this._drawSplitline(r._getEdgesPoints(r._blpoints,l,n),a,i)},e.prototype._getEdgesPoints=function(t,e,n){var i=[t[0].slice(),t[t.length-1].slice()],r="horizontal"===n?0:1;return i[0][r]=i[0][r]-e/2,i[1][r]=i[1][r]+e/2,i},e.prototype._drawSplitline=function(t,e,n){var i=new Yu({z2:20,shape:{points:t},style:e});n.add(i)},e.prototype._getLinePointsOfOneWeek=function(t,e,n){for(var i=t.coordinateSystem,r=i.getDateInfo(e),o=[],a=0;a<7;a++){var s=i.getNextNDay(r.time,a),l=i.dataToRect([s.time],!1);o[2*s.day]=l.tl,o[2*s.day+1]=l["horizontal"===n?"bl":"tr"]}return o},e.prototype._formatterLabel=function(t,e){return U(t)&&t?(n=t,E(e,(function(t,e){n=n.replace("{"+e+"}",i?re(t):t)})),n):X(t)?t(e):e.nameMap;var n,i},e.prototype._yearTextPositionControl=function(t,e,n,i,r){var o=e[0],a=e[1],s=["center","bottom"];"bottom"===i?(a+=r,s=["center","top"]):"left"===i?o-=r:"right"===i?(o+=r,s=["center","top"]):a-=r;var l=0;return"left"!==i&&"right"!==i||(l=Math.PI/2),{rotation:l,x:o,y:a,style:{align:s[0],verticalAlign:s[1]}}},e.prototype._renderYearText=function(t,e,n,i){var r=t.getModel("yearLabel");if(r.get("show")){var o=r.get("margin"),a=r.get("position");a||(a="horizontal"!==n?"top":"left");var s=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],l=(s[0][0]+s[1][0])/2,u=(s[0][1]+s[1][1])/2,h="horizontal"===n?0:1,c={top:[l,s[h][1]],bottom:[l,s[1-h][1]],left:[s[1-h][0],u],right:[s[h][0],u]},p=e.start.y;+e.end.y>+e.start.y&&(p=p+"-"+e.end.y);var d=r.get("formatter"),f={start:e.start.y,end:e.end.y,nameMap:p},g=this._formatterLabel(d,f),y=new Fs({z2:30,style:nc(r,{text:g})});y.attr(this._yearTextPositionControl(y,c[a],n,a,o)),i.add(y)}},e.prototype._monthTextPositionControl=function(t,e,n,i,r){var o="left",a="top",s=t[0],l=t[1];return"horizontal"===n?(l+=r,e&&(o="center"),"start"===i&&(a="bottom")):(s+=r,e&&(a="middle"),"start"===i&&(o="right")),{x:s,y:l,align:o,verticalAlign:a}},e.prototype._renderMonthText=function(t,e,n,i){var r=t.getModel("monthLabel");if(r.get("show")){var o=r.get("nameMap"),a=r.get("margin"),s=r.get("position"),l=r.get("align"),u=[this._tlpoints,this._blpoints];o&&!U(o)||(o&&(e=Nc(o)||e),o=e.get(["time","monthAbbr"])||[]);var h="start"===s?0:1,c="horizontal"===n?0:1;a="start"===s?-a:a;for(var p="center"===l,d=0;d=i.start.time&&n.timea.end.time&&t.reverse(),t},t.prototype._getRangeInfo=function(t){var e,n=[this.getDateInfo(t[0]),this.getDateInfo(t[1])];n[0].time>n[1].time&&(e=!0,n.reverse());var i=Math.floor(n[1].time/AE)-Math.floor(n[0].time/AE)+1,r=new Date(n[0].time),o=r.getDate(),a=n[1].date.getDate();r.setDate(o+i-1);var s=r.getDate();if(s!==a)for(var l=r.getTime()-n[1].time>0?1:-1;(s=r.getDate())!==a&&(r.getTime()-n[1].time)*l>0;)i-=l,r.setDate(s-l);var u=Math.floor((i+n[0].day+6)/7),h=e?1-u:u-1;return e&&n.reverse(),{range:[n[0].formatedDate,n[1].formatedDate],start:n[0],end:n[1],allDay:i,weeks:u,nthWeek:h,fweek:n[0].day,lweek:n[1].day}},t.prototype._getDateByWeeksAndDay=function(t,e,n){var i=this._getRangeInfo(n);if(t>i.weeks||0===t&&ei.lweek)return null;var r=7*(t-1)-i.fweek+e,o=new Date(i.start.time);return o.setDate(+i.start.d+r),this.getDateInfo(o)},t.create=function(e,n){var i=[];return e.eachComponent("calendar",(function(r){var o=new t(r,e,n);i.push(o),r.coordinateSystem=o})),e.eachSeries((function(t){"calendar"===t.get("coordinateSystem")&&(t.coordinateSystem=i[t.get("calendarIndex")||0])})),i},t.dimensions=["time","value"],t}();function LE(t){var e=t.calendarModel,n=t.seriesModel;return e?e.coordinateSystem:n?n.coordinateSystem:null}function PE(t,e){var n;return E(e,(function(e){null!=t[e]&&"auto"!==t[e]&&(n=!0)})),n}var OE=["transition","enterFrom","leaveTo"],RE=OE.concat(["enterAnimation","updateAnimation","leaveAnimation"]);function NE(t,e,n){if(n&&(!t[n]&&e[n]&&(t[n]={}),t=t[n],e=e[n]),t&&e)for(var i=n?OE:RE,r=0;r=0;l--){var p,d,f;if(f=null!=(d=Ao((p=n[l]).id,null))?r.get(d):null){var g=f.parent,y=(c=VE(g),{}),v=Dp(f,p,g===i?{width:o,height:a}:{width:c.width,height:c.height},null,{hv:p.hv,boundingMode:p.bounding},y);if(!VE(f).isNew&&v){for(var m=p.transition,x={},_=0;_=0)?x[b]=w:f[b]=w}fh(f,x,t,0)}else f.attr(y)}}},e.prototype._clear=function(){var t=this,e=this._elMap;e.each((function(n){WE(n,VE(n).option,e,t._lastGraphicModel)})),this._elMap=yt()},e.prototype.dispose=function(){this._clear()},e.type="graphic",e}(Tg);function FE(t){var e=_t(zE,t)?zE[t]:Dh(t);var n=new e({});return VE(n).type=t,n}function GE(t,e,n,i){var r=FE(n);return e.add(r),i.set(t,r),VE(r).id=t,VE(r).isNew=!0,r}function WE(t,e,n,i){t&&t.parent&&("group"===t.type&&t.traverse((function(t){WE(t,e,n,i)})),oR(t,e,i),n.removeKey(VE(t).id))}function HE(t,e,n,i){t.isGroup||E([["cursor",Sa.prototype.cursor],["zlevel",i||0],["z",n||0],["z2",0]],(function(n){var i=n[0];_t(e,i)?t[i]=rt(e[i],n[1]):null==t[i]&&(t[i]=n[1])})),E(G(e),(function(n){if(0===n.indexOf("on")){var i=e[n];t[n]=X(i)?i:null}})),_t(e,"draggable")&&(t.draggable=e.draggable),null!=e.name&&(t.name=e.name),null!=e.id&&(t.id=e.id)}var YE=["x","y","radius","angle","single"],XE=["cartesian2d","polar","singleAxis"];function UE(t){return t+"Axis"}function ZE(t,e){var n,i=yt(),r=[],o=yt();t.eachComponent({mainType:"dataZoom",query:e},(function(t){o.get(t.uid)||s(t)}));do{n=!1,t.eachComponent("dataZoom",a)}while(n);function a(t){!o.get(t.uid)&&function(t){var e=!1;return t.eachTargetAxis((function(t,n){var r=i.get(t);r&&r[n]&&(e=!0)})),e}(t)&&(s(t),n=!0)}function s(t){o.set(t.uid,!0),r.push(t),t.eachTargetAxis((function(t,e){(i.get(t)||i.set(t,[]))[e]=!0}))}return r}function jE(t){var e=t.ecModel,n={infoList:[],infoMap:yt()};return t.eachTargetAxis((function(t,i){var r=e.getComponent(UE(t),i);if(r){var o=r.getCoordSysModel();if(o){var a=o.uid,s=n.infoMap.get(a);s||(s={model:o,axisModels:[]},n.infoList.push(s),n.infoMap.set(a,s)),s.axisModels.push(r)}}})),n}var qE=function(){function t(){this.indexList=[],this.indexMap=[]}return t.prototype.add=function(t){this.indexMap[t]||(this.indexList.push(t),this.indexMap[t]=!0)},t}(),KE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._autoThrottle=!0,n._noTarget=!0,n._rangePropMode=["percent","percent"],n}return n(e,t),e.prototype.init=function(t,e,n){var i=$E(t);this.settledOption=i,this.mergeDefaultAndTheme(t,n),this._doInit(i)},e.prototype.mergeOption=function(t){var e=$E(t);C(this.option,t,!0),C(this.settledOption,e,!0),this._doInit(e)},e.prototype._doInit=function(t){var e=this.option;this._setDefaultThrottle(t),this._updateRangeUse(t);var n=this.settledOption;E([["start","startValue"],["end","endValue"]],(function(t,i){"value"===this._rangePropMode[i]&&(e[t[0]]=n[t[0]]=null)}),this),this._resetTarget()},e.prototype._resetTarget=function(){var t=this.get("orient",!0),e=this._targetAxisInfoMap=yt();this._fillSpecifiedTargetAxis(e)?this._orient=t||this._makeAutoOrientByTargetAxis():(this._orient=t||"horizontal",this._fillAutoTargetAxisByOrient(e,this._orient)),this._noTarget=!0,e.each((function(t){t.indexList.length&&(this._noTarget=!1)}),this)},e.prototype._fillSpecifiedTargetAxis=function(t){var e=!1;return E(YE,(function(n){var i=this.getReferringComponents(UE(n),Vo);if(i.specified){e=!0;var r=new qE;E(i.models,(function(t){r.add(t.componentIndex)})),t.set(n,r)}}),this),e},e.prototype._fillAutoTargetAxisByOrient=function(t,e){var n=this.ecModel,i=!0;if(i){var r="vertical"===e?"y":"x";o(n.findComponents({mainType:r+"Axis"}),r)}i&&o(n.findComponents({mainType:"singleAxis",filter:function(t){return t.get("orient",!0)===e}}),"single");function o(e,n){var r=e[0];if(r){var o=new qE;if(o.add(r.componentIndex),t.set(n,o),i=!1,"x"===n||"y"===n){var a=r.getReferringComponents("grid",zo).models[0];a&&E(e,(function(t){r.componentIndex!==t.componentIndex&&a===t.getReferringComponents("grid",zo).models[0]&&o.add(t.componentIndex)}))}}}i&&E(YE,(function(e){if(i){var r=n.findComponents({mainType:UE(e),filter:function(t){return"category"===t.get("type",!0)}});if(r[0]){var o=new qE;o.add(r[0].componentIndex),t.set(e,o),i=!1}}}),this)},e.prototype._makeAutoOrientByTargetAxis=function(){var t;return this.eachTargetAxis((function(e){!t&&(t=e)}),this),"y"===t?"vertical":"horizontal"},e.prototype._setDefaultThrottle=function(t){if(t.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var e=this.ecModel.option;this.option.throttle=e.animation&&e.animationDurationUpdate>0?100:20}},e.prototype._updateRangeUse=function(t){var e=this._rangePropMode,n=this.get("rangeMode");E([["start","startValue"],["end","endValue"]],(function(i,r){var o=null!=t[i[0]],a=null!=t[i[1]];o&&!a?e[r]="percent":!o&&a?e[r]="value":n?e[r]=n[r]:o&&(e[r]="percent")}))},e.prototype.noTarget=function(){return this._noTarget},e.prototype.getFirstTargetAxisModel=function(){var t;return this.eachTargetAxis((function(e,n){null==t&&(t=this.ecModel.getComponent(UE(e),n))}),this),t},e.prototype.eachTargetAxis=function(t,e){this._targetAxisInfoMap.each((function(n,i){E(n.indexList,(function(n){t.call(e,i,n)}))}))},e.prototype.getAxisProxy=function(t,e){var n=this.getAxisModel(t,e);if(n)return n.__dzAxisProxy},e.prototype.getAxisModel=function(t,e){var n=this._targetAxisInfoMap.get(t);if(n&&n.indexMap[e])return this.ecModel.getComponent(UE(t),e)},e.prototype.setRawRange=function(t){var e=this.option,n=this.settledOption;E([["start","startValue"],["end","endValue"]],(function(i){null==t[i[0]]&&null==t[i[1]]||(e[i[0]]=n[i[0]]=t[i[0]],e[i[1]]=n[i[1]]=t[i[1]])}),this),this._updateRangeUse(t)},e.prototype.setCalculatedRange=function(t){var e=this.option;E(["start","startValue","end","endValue"],(function(n){e[n]=t[n]}))},e.prototype.getPercentRange=function(){var t=this.findRepresentativeAxisProxy();if(t)return t.getDataPercentWindow()},e.prototype.getValueRange=function(t,e){if(null!=t||null!=e)return this.getAxisProxy(t,e).getDataValueWindow();var n=this.findRepresentativeAxisProxy();return n?n.getDataValueWindow():void 0},e.prototype.findRepresentativeAxisProxy=function(t){if(t)return t.__dzAxisProxy;for(var e,n=this._targetAxisInfoMap.keys(),i=0;i=0}(e)){var n=UE(this._dimName),i=e.getReferringComponents(n,zo).models[0];i&&this._axisIndex===i.componentIndex&&t.push(e)}}),this),t},t.prototype.getAxisModel=function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},t.prototype.getMinMaxSpan=function(){return T(this._minMaxSpan)},t.prototype.calculateDataWindow=function(t){var e,n=this._dataExtent,i=this.getAxisModel().axis.scale,r=this._dataZoomModel.getRangePropMode(),o=[0,100],a=[],s=[];ez(["start","end"],(function(l,u){var h=t[l],c=t[l+"Value"];"percent"===r[u]?(null==h&&(h=o[u]),c=i.parse(Xr(h,o,n))):(e=!0,h=Xr(c=null==c?n[u]:i.parse(c),n,o)),s[u]=null==c||isNaN(c)?n[u]:c,a[u]=null==h||isNaN(h)?o[u]:h})),nz(s),nz(a);var l=this._minMaxSpan;function u(t,e,n,r,o){var a=o?"Span":"ValueSpan";Ck(0,t,n,"all",l["min"+a],l["max"+a]);for(var s=0;s<2;s++)e[s]=Xr(t[s],n,r,!0),o&&(e[s]=i.parse(e[s]))}return e?u(s,a,n,o,!1):u(a,s,o,n,!0),{valueWindow:s,percentWindow:a}},t.prototype.reset=function(t){if(t===this._dataZoomModel){var e=this.getTargetSeriesModels();this._dataExtent=function(t,e,n){var i=[1/0,-1/0];ez(n,(function(t){!function(t,e,n){e&&E(M_(e,n),(function(n){var i=e.getApproximateExtent(n);i[0]t[1]&&(t[1]=i[1])}))}(i,t.getData(),e)}));var r=t.getAxisModel(),o=f_(r.axis.scale,r,i).calculate();return[o.min,o.max]}(this,this._dimName,e),this._updateMinMaxSpan();var n=this.calculateDataWindow(t.settledOption);this._valueWindow=n.valueWindow,this._percentWindow=n.percentWindow,this._setAxisModel()}},t.prototype.filterData=function(t,e){if(t===this._dataZoomModel){var n=this._dimName,i=this.getTargetSeriesModels(),r=t.get("filterMode"),o=this._valueWindow;"none"!==r&&ez(i,(function(t){var e=t.getData(),i=e.mapDimensionsAll(n);if(i.length){if("weakFilter"===r){var a=e.getStore(),s=z(i,(function(t){return e.getDimensionIndex(t)}),e);e.filterSelf((function(t){for(var e,n,r,l=0;lo[1];if(h&&!c&&!p)return!0;h&&(r=!0),c&&(e=!0),p&&(n=!0)}return r&&e&&n}))}else ez(i,(function(n){if("empty"===r)t.setData(e=e.map(n,(function(t){return function(t){return t>=o[0]&&t<=o[1]}(t)?t:NaN})));else{var i={};i[n]=o,e.selectRange(i)}}));ez(i,(function(t){e.setApproximateExtent(o,t)}))}}))}},t.prototype._updateMinMaxSpan=function(){var t=this._minMaxSpan={},e=this._dataZoomModel,n=this._dataExtent;ez(["min","max"],(function(i){var r=e.get(i+"Span"),o=e.get(i+"ValueSpan");null!=o&&(o=this.getAxisModel().axis.scale.parse(o)),null!=o?r=Xr(n[0]+o,n,[0,100],!0):null!=r&&(o=Xr(r,[0,100],n,!0)-n[0]),t[i+"Span"]=r,t[i+"ValueSpan"]=o}),this)},t.prototype._setAxisModel=function(){var t=this.getAxisModel(),e=this._percentWindow,n=this._valueWindow;if(e){var i=$r(n,[0,500]);i=Math.min(i,20);var r=t.axis.scale.rawExtentInfo;0!==e[0]&&r.setDeterminedMinMax("min",+n[0].toFixed(i)),100!==e[1]&&r.setDeterminedMinMax("max",+n[1].toFixed(i)),r.freeze()}},t}();var rz={getTargetSeries:function(t){function e(e){t.eachComponent("dataZoom",(function(n){n.eachTargetAxis((function(i,r){var o=t.getComponent(UE(i),r);e(i,r,o,n)}))}))}e((function(t,e,n,i){n.__dzAxisProxy=null}));var n=[];e((function(e,i,r,o){r.__dzAxisProxy||(r.__dzAxisProxy=new iz(e,i,o,t),n.push(r.__dzAxisProxy))}));var i=yt();return E(n,(function(t){E(t.getTargetSeriesModels(),(function(t){i.set(t.uid,t)}))})),i},overallReset:function(t,e){t.eachComponent("dataZoom",(function(t){t.eachTargetAxis((function(e,n){t.getAxisProxy(e,n).reset(t)})),t.eachTargetAxis((function(n,i){t.getAxisProxy(n,i).filterData(t,e)}))})),t.eachComponent("dataZoom",(function(t){var e=t.findRepresentativeAxisProxy();if(e){var n=e.getDataPercentWindow(),i=e.getDataValueWindow();t.setCalculatedRange({start:n[0],end:n[1],startValue:i[0],endValue:i[1]})}}))}};var oz=!1;function az(t){oz||(oz=!0,t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,rz),function(t){t.registerAction("dataZoom",(function(t,e){E(ZE(e,t),(function(e){e.setRawRange({start:t.start,end:t.end,startValue:t.startValue,endValue:t.endValue})}))}))}(t),t.registerSubTypeDefaulter("dataZoom",(function(){return"slider"})))}function sz(t){t.registerComponentModel(JE),t.registerComponentView(tz),az(t)}var lz=function(){},uz={};function hz(t,e){uz[t]=e}function cz(t){return uz[t]}var pz=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(){t.prototype.optionUpdated.apply(this,arguments);var e=this.ecModel;E(this.option.feature,(function(t,n){var i=cz(n);i&&(i.getDefaultOption&&(i.defaultOption=i.getDefaultOption(e)),C(t,i.defaultOption))}))},e.type="toolbox",e.layoutMode={type:"box",ignoreSize:!0},e.defaultOption={show:!0,z:6,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{borderColor:"#666",color:"none"},emphasis:{iconStyle:{borderColor:"#3E98C5"}},tooltip:{show:!1,position:"bottom"}},e}(Rp);function dz(t,e){var n=fp(e.get("padding")),i=e.getItemStyle(["color","opacity"]);return i.fill=e.get("backgroundColor"),t=new zs({shape:{x:t.x-n[3],y:t.y-n[0],width:t.width+n[1]+n[3],height:t.height+n[0]+n[2],r:e.get("borderRadius")},style:i,silent:!0,z2:-1})}var fz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){var r=this.group;if(r.removeAll(),t.get("show")){var o=+t.get("itemSize"),a="vertical"===t.get("orient"),s=t.get("feature")||{},l=this._features||(this._features={}),u=[];E(s,(function(t,e){u.push(e)})),new Vm(this._featureNames||[],u).add(h).update(h).remove(H(h,null)).execute(),this._featureNames=u,function(t,e,n){var i=e.getBoxLayoutParams(),r=e.get("padding"),o={width:n.getWidth(),height:n.getHeight()},a=Cp(i,o,r);Tp(e.get("orient"),t,e.get("itemGap"),a.width,a.height),Dp(t,i,o,r)}(r,t,n),r.add(dz(r.getBoundingRect(),t)),a||r.eachChild((function(t){var e=t.__title,i=t.ensureState("emphasis"),a=i.textConfig||(i.textConfig={}),s=t.getTextContent(),l=s&&s.ensureState("emphasis");if(l&&!X(l)&&e){var u=l.style||(l.style={}),h=br(e,Fs.makeFont(u)),c=t.x+r.x,p=!1;t.y+r.y+o+h.height>n.getHeight()&&(a.position="top",p=!0);var d=p?-5-h.height:o+10;c+h.width/2>n.getWidth()?(a.position=["100%",d],u.align="right"):c-h.width/2<0&&(a.position=[0,d],u.align="left")}}))}function h(h,c){var p,d=u[h],f=u[c],g=s[d],y=new Mc(g,t,t.ecModel);if(i&&null!=i.newTitle&&i.featureName===d&&(g.title=i.newTitle),d&&!f){if(function(t){return 0===t.indexOf("my")}(d))p={onclick:y.option.onclick,featureName:d};else{var v=cz(d);if(!v)return;p=new v}l[d]=p}else if(!(p=l[f]))return;p.uid=Tc("toolbox-feature"),p.model=y,p.ecModel=e,p.api=n;var m=p instanceof lz;d||!f?!y.get("show")||m&&p.unusable?m&&p.remove&&p.remove(e,n):(!function(i,s,l){var u,h,c=i.getModel("iconStyle"),p=i.getModel(["emphasis","iconStyle"]),d=s instanceof lz&&s.getIcons?s.getIcons():i.get("icon"),f=i.get("title")||{};U(d)?(u={})[l]=d:u=d;U(f)?(h={})[l]=f:h=f;var g=i.iconPaths={};E(u,(function(l,u){var d=Hh(l,{},{x:-o/2,y:-o/2,width:o,height:o});d.setStyle(c.getItemStyle()),d.ensureState("emphasis").style=p.getItemStyle();var f=new Fs({style:{text:h[u],align:p.get("textAlign"),borderRadius:p.get("textBorderRadius"),padding:p.get("textPadding"),fill:null},ignore:!0});d.setTextContent(f),Zh({el:d,componentModel:t,itemName:u,formatterParamsExtra:{title:h[u]}}),d.__title=h[u],d.on("mouseover",(function(){var e=p.getItemStyle(),i=a?null==t.get("right")&&"right"!==t.get("left")?"right":"left":null==t.get("bottom")&&"bottom"!==t.get("top")?"bottom":"top";f.setStyle({fill:p.get("textFill")||e.fill||e.stroke||"#000",backgroundColor:p.get("textBackgroundColor")}),d.setTextConfig({position:p.get("textPosition")||i}),f.ignore=!t.get("showTitle"),n.enterEmphasis(this)})).on("mouseout",(function(){"emphasis"!==i.get(["iconStatus",u])&&n.leaveEmphasis(this),f.hide()})),("emphasis"===i.get(["iconStatus",u])?kl:Ll)(d),r.add(d),d.on("click",W(s.onclick,s,e,n,u)),g[u]=d}))}(y,p,d),y.setIconStatus=function(t,e){var n=this.option,i=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[t]=e,i[t]&&("emphasis"===e?kl:Ll)(i[t])},p instanceof lz&&p.render&&p.render(y,e,n,i)):m&&p.dispose&&p.dispose(e,n)}},e.prototype.updateView=function(t,e,n,i){E(this._features,(function(t){t instanceof lz&&t.updateView&&t.updateView(t.model,e,n,i)}))},e.prototype.remove=function(t,e){E(this._features,(function(n){n instanceof lz&&n.remove&&n.remove(t,e)})),this.group.removeAll()},e.prototype.dispose=function(t,e){E(this._features,(function(n){n instanceof lz&&n.dispose&&n.dispose(t,e)}))},e.type="toolbox",e}(Tg);var gz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.onclick=function(t,e){var n=this.model,i=n.get("name")||t.get("title.0.text")||"echarts",o="svg"===e.getZr().painter.getType(),a=o?"svg":n.get("type",!0)||"png",s=e.getConnectedDataURL({type:a,backgroundColor:n.get("backgroundColor",!0)||t.get("backgroundColor")||"#fff",connectedBackgroundColor:n.get("connectedBackgroundColor"),excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")}),l=r.browser;if(X(MouseEvent)&&(l.newEdge||!l.ie&&!l.edge)){var u=document.createElement("a");u.download=i+"."+a,u.target="_blank",u.href=s;var h=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});u.dispatchEvent(h)}else if(window.navigator.msSaveOrOpenBlob||o){var c=s.split(","),p=c[0].indexOf("base64")>-1,d=o?decodeURIComponent(c[1]):c[1];p&&(d=window.atob(d));var f=i+"."+a;if(window.navigator.msSaveOrOpenBlob){for(var g=d.length,y=new Uint8Array(g);g--;)y[g]=d.charCodeAt(g);var v=new Blob([y]);window.navigator.msSaveOrOpenBlob(v,f)}else{var m=document.createElement("iframe");document.body.appendChild(m);var x=m.contentWindow,_=x.document;_.open("image/svg+xml","replace"),_.write(d),_.close(),x.focus(),_.execCommand("SaveAs",!0,f),document.body.removeChild(m)}}else{var b=n.get("lang"),w='',S=window.open();S.document.write(w),S.document.title=i}},e.getDefaultOption=function(t){return{show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:t.getLocaleModel().get(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:"#fff",name:"",excludeComponents:["toolbox"],lang:t.getLocaleModel().get(["toolbox","saveAsImage","lang"])}},e}(lz),yz="__ec_magicType_stack__",vz=[["line","bar"],["stack"]],mz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getIcons=function(){var t=this.model,e=t.get("icon"),n={};return E(t.get("type"),(function(t){e[t]&&(n[t]=e[t])})),n},e.getDefaultOption=function(t){return{show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:t.getLocaleModel().get(["toolbox","magicType","title"]),option:{},seriesIndex:{}}},e.prototype.onclick=function(t,e,n){var i=this.model,r=i.get(["seriesIndex",n]);if(xz[n]){var o,a={series:[]};E(vz,(function(t){P(t,n)>=0&&E(t,(function(t){i.setIconStatus(t,"normal")}))})),i.setIconStatus(n,"emphasis"),t.eachComponent({mainType:"series",query:null==r?null:{seriesIndex:r}},(function(t){var e=t.subType,r=t.id,o=xz[n](e,r,t,i);o&&(k(o,t.option),a.series.push(o));var s=t.coordinateSystem;if(s&&"cartesian2d"===s.type&&("line"===n||"bar"===n)){var l=s.getAxesByScale("ordinal")[0];if(l){var u=l.dim+"Axis",h=t.getReferringComponents(u,zo).models[0].componentIndex;a[u]=a[u]||[];for(var c=0;c<=h;c++)a[u][h]=a[u][h]||{};a[u][h].boundaryGap="bar"===n}}}));var s=n;"stack"===n&&(o=C({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),"emphasis"!==i.get(["iconStatus",n])&&(s="tiled")),e.dispatchAction({type:"changeMagicType",currentType:s,newOption:a,newTitle:o,featureName:"magicType"})}},e}(lz),xz={line:function(t,e,n,i){if("bar"===t)return C({id:e,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","line"])||{},!0)},bar:function(t,e,n,i){if("line"===t)return C({id:e,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","bar"])||{},!0)},stack:function(t,e,n,i){var r=n.get("stack")===yz;if("line"===t||"bar"===t)return i.setIconStatus("stack",r?"normal":"emphasis"),C({id:e,stack:r?"":yz},i.get(["option","stack"])||{},!0)}};Mm({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},(function(t,e){e.mergeOption(t.newOption)}));var _z=new Array(60).join("-"),bz="\t";function wz(t){return t.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}var Sz=new RegExp("[\t]+","g");function Mz(t,e){var n=t.split(new RegExp("\n*"+_z+"\n*","g")),i={series:[]};return E(n,(function(t,n){if(function(t){if(t.slice(0,t.indexOf("\n")).indexOf(bz)>=0)return!0}(t)){var r=function(t){for(var e=t.split(/\n+/g),n=[],i=z(wz(e.shift()).split(Sz),(function(t){return{name:t,data:[]}})),r=0;r=0)&&t(r,i._targetInfoList)}))}return t.prototype.setOutputRanges=function(t,e){return this.matchOutputRanges(t,e,(function(t,e,n){if((t.coordRanges||(t.coordRanges=[])).push(e),!t.coordRange){t.coordRange=e;var i=Vz[t.brushType](0,n,e);t.__rangeOffset={offset:Fz[t.brushType](i.values,t.range,[1,1]),xyMinMax:i.xyMinMax}}})),t},t.prototype.matchOutputRanges=function(t,e,n){E(t,(function(t){var i=this.findTargetInfo(t,e);i&&!0!==i&&E(i.coordSyses,(function(i){var r=Vz[t.brushType](1,i,t.range,!0);n(t,r.values,i,e)}))}),this)},t.prototype.setInputRanges=function(t,e){E(t,(function(t){var n,i,r,o,a,s=this.findTargetInfo(t,e);if(t.range=t.range||[],s&&!0!==s){t.panelId=s.panelId;var l=Vz[t.brushType](0,s.coordSys,t.coordRange),u=t.__rangeOffset;t.range=u?Fz[t.brushType](l.values,u.offset,(n=l.xyMinMax,i=u.xyMinMax,r=Wz(n),o=Wz(i),a=[r[0]/o[0],r[1]/o[1]],isNaN(a[0])&&(a[0]=1),isNaN(a[1])&&(a[1]=1),a)):l.values}}),this)},t.prototype.makePanelOpts=function(t,e){return z(this._targetInfoList,(function(n){var i=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:e?e(n):null,clipPath:AL(i),isTargetByCursor:LL(i,t,n.coordSysModel),getLinearBrushOtherExtent:kL(i)}}))},t.prototype.controlSeries=function(t,e,n){var i=this.findTargetInfo(t,n);return!0===i||i&&P(i.coordSyses,e.coordinateSystem)>=0},t.prototype.findTargetInfo=function(t,e){for(var n=this._targetInfoList,i=Rz(e,t),r=0;rt[1]&&t.reverse(),t}function Rz(t,e){return No(t,e,{includeMainTypes:Lz})}var Nz={grid:function(t,e){var n=t.xAxisModels,i=t.yAxisModels,r=t.gridModels,o=yt(),a={},s={};(n||i||r)&&(E(n,(function(t){var e=t.axis.grid.model;o.set(e.id,e),a[e.id]=!0})),E(i,(function(t){var e=t.axis.grid.model;o.set(e.id,e),s[e.id]=!0})),E(r,(function(t){o.set(t.id,t),a[t.id]=!0,s[t.id]=!0})),o.each((function(t){var r=t.coordinateSystem,o=[];E(r.getCartesians(),(function(t,e){(P(n,t.getAxis("x").model)>=0||P(i,t.getAxis("y").model)>=0)&&o.push(t)})),e.push({panelId:"grid--"+t.id,gridModel:t,coordSysModel:t,coordSys:o[0],coordSyses:o,getPanelRect:zz.grid,xAxisDeclared:a[t.id],yAxisDeclared:s[t.id]})})))},geo:function(t,e){E(t.geoModels,(function(t){var n=t.coordinateSystem;e.push({panelId:"geo--"+t.id,geoModel:t,coordSysModel:t,coordSys:n,coordSyses:[n],getPanelRect:zz.geo})}))}},Ez=[function(t,e){var n=t.xAxisModel,i=t.yAxisModel,r=t.gridModel;return!r&&n&&(r=n.axis.grid.model),!r&&i&&(r=i.axis.grid.model),r&&r===e.gridModel},function(t,e){var n=t.geoModel;return n&&n===e.geoModel}],zz={grid:function(){return this.coordSys.master.getRect().clone()},geo:function(){var t=this.coordSys,e=t.getBoundingRect().clone();return e.applyTransform(Eh(t)),e}},Vz={lineX:H(Bz,0),lineY:H(Bz,1),rect:function(t,e,n,i){var r=t?e.pointToData([n[0][0],n[1][0]],i):e.dataToPoint([n[0][0],n[1][0]],i),o=t?e.pointToData([n[0][1],n[1][1]],i):e.dataToPoint([n[0][1],n[1][1]],i),a=[Oz([r[0],o[0]]),Oz([r[1],o[1]])];return{values:a,xyMinMax:a}},polygon:function(t,e,n,i){var r=[[1/0,-1/0],[1/0,-1/0]];return{values:z(n,(function(n){var o=t?e.pointToData(n,i):e.dataToPoint(n,i);return r[0][0]=Math.min(r[0][0],o[0]),r[1][0]=Math.min(r[1][0],o[1]),r[0][1]=Math.max(r[0][1],o[0]),r[1][1]=Math.max(r[1][1],o[1]),o})),xyMinMax:r}}};function Bz(t,e,n,i){var r=n.getAxis(["x","y"][t]),o=Oz(z([0,1],(function(t){return e?r.coordToData(r.toLocalCoord(i[t]),!0):r.toGlobalCoord(r.dataToCoord(i[t]))}))),a=[];return a[t]=o,a[1-t]=[NaN,NaN],{values:o,xyMinMax:a}}var Fz={lineX:H(Gz,0),lineY:H(Gz,1),rect:function(t,e,n){return[[t[0][0]-n[0]*e[0][0],t[0][1]-n[0]*e[0][1]],[t[1][0]-n[1]*e[1][0],t[1][1]-n[1]*e[1][1]]]},polygon:function(t,e,n){return z(t,(function(t,i){return[t[0]-n[0]*e[i][0],t[1]-n[1]*e[i][1]]}))}};function Gz(t,e,n,i){return[e[0]-i[t]*n[0],e[1]-i[t]*n[1]]}function Wz(t){return t?[t[0][1]-t[0][0],t[1][1]-t[1][0]]:[NaN,NaN]}var Hz,Yz,Xz=E,Uz=_o+"toolbox-dataZoom_",Zz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){this._brushController||(this._brushController=new Jk(n.getZr()),this._brushController.on("brush",W(this._onBrush,this)).mount()),function(t,e,n,i,r){var o=n._isZoomActive;i&&"takeGlobalCursor"===i.type&&(o="dataZoomSelect"===i.key&&i.dataZoomSelectActive);n._isZoomActive=o,t.setIconStatus("zoom",o?"emphasis":"normal");var a=new Pz(qz(t),e,{include:["grid"]}),s=a.makePanelOpts(r,(function(t){return t.xAxisDeclared&&!t.yAxisDeclared?"lineX":!t.xAxisDeclared&&t.yAxisDeclared?"lineY":"rect"}));n._brushController.setPanels(s).enableBrush(!(!o||!s.length)&&{brushType:"auto",brushStyle:t.getModel("brushStyle").getItemStyle()})}(t,e,this,i,n),function(t,e){t.setIconStatus("back",function(t){return Az(t).length}(e)>1?"emphasis":"normal")}(t,e)},e.prototype.onclick=function(t,e,n){jz[n].call(this)},e.prototype.remove=function(t,e){this._brushController&&this._brushController.unmount()},e.prototype.dispose=function(t,e){this._brushController&&this._brushController.dispose()},e.prototype._onBrush=function(t){var e=t.areas;if(t.isEnd&&e.length){var n={},i=this.ecModel;this._brushController.updateCovers([]),new Pz(qz(this.model),i,{include:["grid"]}).matchOutputRanges(e,i,(function(t,e,n){if("cartesian2d"===n.type){var i=t.brushType;"rect"===i?(r("x",n,e[0]),r("y",n,e[1])):r({lineX:"x",lineY:"y"}[i],n,e)}})),function(t,e){var n=Az(t);Cz(e,(function(e,i){for(var r=n.length-1;r>=0&&!n[r][i];r--);if(r<0){var o=t.queryComponents({mainType:"dataZoom",subType:"select",id:i})[0];if(o){var a=o.getPercentRange();n[0][i]={dataZoomId:i,start:a[0],end:a[1]}}}})),n.push(e)}(i,n),this._dispatchZoomAction(n)}function r(t,e,r){var o=e.getAxis(t),a=o.model,s=function(t,e,n){var i;return n.eachComponent({mainType:"dataZoom",subType:"select"},(function(n){n.getAxisModel(t,e.componentIndex)&&(i=n)})),i}(t,a,i),l=s.findRepresentativeAxisProxy(a).getMinMaxSpan();null==l.minValueSpan&&null==l.maxValueSpan||(r=Ck(0,r.slice(),o.scale.getExtent(),0,l.minValueSpan,l.maxValueSpan)),s&&(n[s.id]={dataZoomId:s.id,startValue:r[0],endValue:r[1]})}},e.prototype._dispatchZoomAction=function(t){var e=[];Xz(t,(function(t,n){e.push(T(t))})),e.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:e})},e.getDefaultOption=function(t){return{show:!0,filterMode:"filter",icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:t.getLocaleModel().get(["toolbox","dataZoom","title"]),brushStyle:{borderWidth:0,color:"rgba(210,219,238,0.2)"}}},e}(lz),jz={zoom:function(){var t=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:t})},back:function(){this._dispatchZoomAction(function(t){var e=Az(t),n=e[e.length-1];e.length>1&&e.pop();var i={};return Cz(n,(function(t,n){for(var r=e.length-1;r>=0;r--)if(t=e[r][n]){i[n]=t;break}})),i}(this.ecModel))}};function qz(t){var e={xAxisIndex:t.get("xAxisIndex",!0),yAxisIndex:t.get("yAxisIndex",!0),xAxisId:t.get("xAxisId",!0),yAxisId:t.get("yAxisId",!0)};return null==e.xAxisIndex&&null==e.xAxisId&&(e.xAxisIndex="all"),null==e.yAxisIndex&&null==e.yAxisId&&(e.yAxisIndex="all"),e}Hz="dataZoom",Yz=function(t){var e=t.getComponent("toolbox",0),n=["feature","dataZoom"];if(e&&null!=e.get(n)){var i=e.getModel(n),r=[],o=No(t,qz(i));return Xz(o.xAxisModels,(function(t){return a(t,"xAxis","xAxisIndex")})),Xz(o.yAxisModels,(function(t){return a(t,"yAxis","yAxisIndex")})),r}function a(t,e,n){var o=t.componentIndex,a={type:"select",$fromToolbox:!0,filterMode:i.get("filterMode",!0)||"filter",id:Uz+e+o};a[n]=o,r.push(a)}},lt(null==nd.get(Hz)&&Yz),nd.set(Hz,Yz);var Kz=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="tooltip",e.dependencies=["axisPointer"],e.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"#fff",shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#666",fontSize:14}},e}(Rp);function $z(t){var e=t.get("confine");return null!=e?!!e:"richText"===t.get("renderMode")}function Jz(t){if(r.domSupported)for(var e=document.documentElement.style,n=0,i=t.length;n-1?(u+="top:50%",h+="translateY(-50%) rotate("+(a="left"===s?-225:-45)+"deg)"):(u+="left:50%",h+="translateX(-50%) rotate("+(a="top"===s?225:45)+"deg)");var c=a*Math.PI/180,p=l+r,d=p*Math.abs(Math.cos(c))+p*Math.abs(Math.sin(c)),f=e+" solid "+r+"px;";return'
'}(n,i,r)),U(t))o.innerHTML=t+a;else if(t){o.innerHTML="",Y(t)||(t=[t]);for(var s=0;s=0?this._tryShow(n,i):"leave"===e&&this._hide(i))}),this))},e.prototype._keepShow=function(){var t=this._tooltipModel,e=this._ecModel,n=this._api,i=t.get("triggerOn");if(null!=this._lastX&&null!=this._lastY&&"none"!==i&&"click"!==i){var r=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout((function(){!n.isDisposed()&&r.manuallyShowTip(t,e,n,{x:r._lastX,y:r._lastY,dataByCoordSys:r._lastDataByCoordSys})}))}},e.prototype.manuallyShowTip=function(t,e,n,i){if(i.from!==this.uid&&!r.node&&n.getDom()){var o=gV(i,n);this._ticket="";var a=i.dataByCoordSys,s=function(t,e,n){var i=Eo(t).queryOptionMap,r=i.keys()[0];if(!r||"series"===r)return;var o=Bo(e,r,i.get(r),{useDefault:!1,enableAll:!1,enableNone:!1}),a=o.models[0];if(!a)return;var s,l=n.getViewOfComponentModel(a);if(l.group.traverse((function(e){var n=Qs(e).tooltipConfig;if(n&&n.name===t.name)return s=e,!0})),s)return{componentMainType:r,componentIndex:a.componentIndex,el:s}}(i,e,n);if(s){var l=s.el.getBoundingRect().clone();l.applyTransform(s.el.transform),this._tryShow({offsetX:l.x+l.width/2,offsetY:l.y+l.height/2,target:s.el,position:i.position,positionDefault:"bottom"},o)}else if(i.tooltip&&null!=i.x&&null!=i.y){var u=pV;u.x=i.x,u.y=i.y,u.update(),Qs(u).tooltipConfig={name:null,option:i.tooltip},this._tryShow({offsetX:i.x,offsetY:i.y,target:u},o)}else if(a)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,dataByCoordSys:a,tooltipOption:i.tooltipOption},o);else if(null!=i.seriesIndex){if(this._manuallyAxisShowTip(t,e,n,i))return;var h=wN(i,e),c=h.point[0],p=h.point[1];null!=c&&null!=p&&this._tryShow({offsetX:c,offsetY:p,target:h.el,position:i.position,positionDefault:"bottom"},o)}else null!=i.x&&null!=i.y&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target},o))}},e.prototype.manuallyHideTip=function(t,e,n,i){var r=this._tooltipContent;this._tooltipModel&&r.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,i.from!==this.uid&&this._hide(gV(i,n))},e.prototype._manuallyAxisShowTip=function(t,e,n,i){var r=i.seriesIndex,o=i.dataIndex,a=e.getComponent("axisPointer").coordSysAxesInfo;if(null!=r&&null!=o&&null!=a){var s=e.getSeriesByIndex(r);if(s)if("axis"===fV([s.getData().getItemModel(o),s,(s.coordinateSystem||{}).model],this._tooltipModel).get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:r,dataIndex:o,position:i.position}),!0}},e.prototype._tryShow=function(t,e){var n=t.target;if(this._tooltipModel){this._lastX=t.offsetX,this._lastY=t.offsetY;var i=t.dataByCoordSys;if(i&&i.length)this._showAxisTooltip(i,t);else if(n){var r,o;this._lastDataByCoordSys=null,ky(n,(function(t){return null!=Qs(t).dataIndex?(r=t,!0):null!=Qs(t).tooltipConfig?(o=t,!0):void 0}),!0),r?this._showSeriesItemTooltip(t,r,e):o?this._showComponentItemTooltip(t,o,e):this._hide(e)}else this._lastDataByCoordSys=null,this._hide(e)}},e.prototype._showOrMove=function(t,e){var n=t.get("showDelay");e=W(e,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(e,n):e()},e.prototype._showAxisTooltip=function(t,e){var n=this._ecModel,i=this._tooltipModel,r=[e.offsetX,e.offsetY],o=fV([e.tooltipOption],i),a=this._renderMode,s=[],l=ng("section",{blocks:[],noHeader:!0}),u=[],h=new dg;E(t,(function(t){E(t.dataByAxis,(function(t){var e=n.getComponent(t.axisDim+"Axis",t.axisIndex),r=t.value;if(e&&null!=r){var o=rN(r,e.axis,n,t.seriesDataIndices,t.valueLabelOpt),c=ng("section",{header:o,noHeader:!ut(o),sortBlocks:!0,blocks:[]});l.blocks.push(c),E(t.seriesDataIndices,(function(l){var p=n.getSeriesByIndex(l.seriesIndex),d=l.dataIndexInside,f=p.getDataParams(d);if(!(f.dataIndex<0)){f.axisDim=t.axisDim,f.axisIndex=t.axisIndex,f.axisType=t.axisType,f.axisId=t.axisId,f.axisValue=__(e.axis,{value:r}),f.axisValueLabel=o,f.marker=h.makeTooltipMarker("item",_p(f.color),a);var g=mf(p.formatTooltip(d,!0,null)),y=g.frag;if(y){var v=fV([p],i).get("valueFormatter");c.blocks.push(v?A({valueFormatter:v},y):y)}g.text&&u.push(g.text),s.push(f)}}))}}))})),l.blocks.reverse(),u.reverse();var c=e.position,p=o.get("order"),d=lg(l,h,a,p,n.get("useUTC"),o.get("textStyle"));d&&u.unshift(d);var f="richText"===a?"\n\n":"
",g=u.join(f);this._showOrMove(o,(function(){this._updateContentNotChangedOnAxis(t,s)?this._updatePosition(o,c,r[0],r[1],this._tooltipContent,s):this._showTooltipContent(o,g,s,Math.random()+"",r[0],r[1],c,null,h)}))},e.prototype._showSeriesItemTooltip=function(t,e,n){var i=this._ecModel,r=Qs(e),o=r.seriesIndex,a=i.getSeriesByIndex(o),s=r.dataModel||a,l=r.dataIndex,u=r.dataType,h=s.getData(u),c=this._renderMode,p=t.positionDefault,d=fV([h.getItemModel(l),s,a&&(a.coordinateSystem||{}).model],this._tooltipModel,p?{position:p}:null),f=d.get("trigger");if(null==f||"item"===f){var g=s.getDataParams(l,u),y=new dg;g.marker=y.makeTooltipMarker("item",_p(g.color),c);var v=mf(s.formatTooltip(l,!1,u)),m=d.get("order"),x=d.get("valueFormatter"),_=v.frag,b=_?lg(x?A({valueFormatter:x},_):_,y,c,m,i.get("useUTC"),d.get("textStyle")):v.text,w="item_"+s.name+"_"+l;this._showOrMove(d,(function(){this._showTooltipContent(d,b,g,w,t.offsetX,t.offsetY,t.position,t.target,y)})),n({type:"showTip",dataIndexInside:l,dataIndex:h.getRawIndex(l),seriesIndex:o,from:this.uid})}},e.prototype._showComponentItemTooltip=function(t,e,n){var i=Qs(e),r=i.tooltipConfig.option||{};if(U(r)){r={content:r,formatter:r}}var o=[r],a=this._ecModel.getComponent(i.componentMainType,i.componentIndex);a&&o.push(a),o.push({formatter:r.content});var s=t.positionDefault,l=fV(o,this._tooltipModel,s?{position:s}:null),u=l.get("content"),h=Math.random()+"",c=new dg;this._showOrMove(l,(function(){var n=T(l.get("formatterParams")||{});this._showTooltipContent(l,u,n,h,t.offsetX,t.offsetY,t.position,e,c)})),n({type:"showTip",from:this.uid})},e.prototype._showTooltipContent=function(t,e,n,i,r,o,a,s,l){if(this._ticket="",t.get("showContent")&&t.get("show")){var u=this._tooltipContent;u.setEnterable(t.get("enterable"));var h=t.get("formatter");a=a||t.get("position");var c=e,p=this._getNearestPoint([r,o],n,t.get("trigger"),t.get("borderColor")).color;if(h)if(U(h)){var d=t.ecModel.get("useUTC"),f=Y(n)?n[0]:n;c=h,f&&f.axisType&&f.axisType.indexOf("time")>=0&&(c=qc(f.axisValue,c,d)),c=mp(c,n,!0)}else if(X(h)){var g=W((function(e,i){e===this._ticket&&(u.setContent(i,l,t,p,a),this._updatePosition(t,a,r,o,u,n,s))}),this);this._ticket=i,c=h(n,i,g)}else c=h;u.setContent(c,l,t,p,a),u.show(t,p),this._updatePosition(t,a,r,o,u,n,s)}},e.prototype._getNearestPoint=function(t,e,n,i){return"axis"===n||Y(e)?{color:i||("html"===this._renderMode?"#fff":"none")}:Y(e)?void 0:{color:i||e.color||e.borderColor}},e.prototype._updatePosition=function(t,e,n,i,r,o,a){var s=this._api.getWidth(),l=this._api.getHeight();e=e||t.get("position");var u=r.getSize(),h=t.get("align"),c=t.get("verticalAlign"),p=a&&a.getBoundingRect().clone();if(a&&p.applyTransform(a.transform),X(e)&&(e=e([n,i],o,r.el,p,{viewSize:[s,l],contentSize:u.slice()})),Y(e))n=Ur(e[0],s),i=Ur(e[1],l);else if(q(e)){var d=e;d.width=u[0],d.height=u[1];var f=Cp(d,{width:s,height:l});n=f.x,i=f.y,h=null,c=null}else if(U(e)&&a){var g=function(t,e,n,i){var r=n[0],o=n[1],a=Math.ceil(Math.SQRT2*i)+8,s=0,l=0,u=e.width,h=e.height;switch(t){case"inside":s=e.x+u/2-r/2,l=e.y+h/2-o/2;break;case"top":s=e.x+u/2-r/2,l=e.y-o-a;break;case"bottom":s=e.x+u/2-r/2,l=e.y+h+a;break;case"left":s=e.x-r-a,l=e.y+h/2-o/2;break;case"right":s=e.x+u+a,l=e.y+h/2-o/2}return[s,l]}(e,p,u,t.get("borderWidth"));n=g[0],i=g[1]}else{g=function(t,e,n,i,r,o,a){var s=n.getSize(),l=s[0],u=s[1];null!=o&&(t+l+o+2>i?t-=l+o:t+=o);null!=a&&(e+u+a>r?e-=u+a:e+=a);return[t,e]}(n,i,r,s,l,h?null:20,c?null:20);n=g[0],i=g[1]}if(h&&(n-=yV(h)?u[0]/2:"right"===h?u[0]:0),c&&(i-=yV(c)?u[1]/2:"bottom"===c?u[1]:0),$z(t)){g=function(t,e,n,i,r){var o=n.getSize(),a=o[0],s=o[1];return t=Math.min(t+a,i)-a,e=Math.min(e+s,r)-s,t=Math.max(t,0),e=Math.max(e,0),[t,e]}(n,i,r,s,l);n=g[0],i=g[1]}r.moveTo(n,i)},e.prototype._updateContentNotChangedOnAxis=function(t,e){var n=this._lastDataByCoordSys,i=this._cbParamsList,r=!!n&&n.length===t.length;return r&&E(n,(function(n,o){var a=n.dataByAxis||[],s=(t[o]||{}).dataByAxis||[];(r=r&&a.length===s.length)&&E(a,(function(t,n){var o=s[n]||{},a=t.seriesDataIndices||[],l=o.seriesDataIndices||[];(r=r&&t.value===o.value&&t.axisType===o.axisType&&t.axisId===o.axisId&&a.length===l.length)&&E(a,(function(t,e){var n=l[e];r=r&&t.seriesIndex===n.seriesIndex&&t.dataIndex===n.dataIndex})),i&&E(t.seriesDataIndices,(function(t){var n=t.seriesIndex,o=e[n],a=i[n];o&&a&&a.data!==o.data&&(r=!1)}))}))})),this._lastDataByCoordSys=t,this._cbParamsList=e,!!r},e.prototype._hide=function(t){this._lastDataByCoordSys=null,t({type:"hideTip",from:this.uid})},e.prototype.dispose=function(t,e){!r.node&&e.getDom()&&(Gg(this,"_updatePosition"),this._tooltipContent.dispose(),_N("itemTooltip",e))},e.type="tooltip",e}(Tg);function fV(t,e,n){var i,r=e.ecModel;n?(i=new Mc(n,r,r),i=new Mc(e.option,i,r)):i=e;for(var o=t.length-1;o>=0;o--){var a=t[o];a&&(a instanceof Mc&&(a=a.get("tooltip",!0)),U(a)&&(a={formatter:a}),a&&(i=new Mc(a,i,r)))}return i}function gV(t,e){return t.dispatchAction||W(e.dispatchAction,e)}function yV(t){return"center"===t||"middle"===t}var vV=["rect","polygon","keep","clear"];function mV(t,e){var n=bo(t?t.brush:[]);if(n.length){var i=[];E(n,(function(t){var e=t.hasOwnProperty("toolbox")?t.toolbox:[];e instanceof Array&&(i=i.concat(e))}));var r=t&&t.toolbox;Y(r)&&(r=r[0]),r||(r={feature:{}},t.toolbox=[r]);var o=r.feature||(r.feature={}),a=o.brush||(o.brush={}),s=a.type||(a.type=[]);s.push.apply(s,i),function(t){var e={};E(t,(function(t){e[t]=1})),t.length=0,E(e,(function(e,n){t.push(n)}))}(s),e&&!s.length&&s.push.apply(s,vV)}}var xV=E;function _V(t){if(t)for(var e in t)if(t.hasOwnProperty(e))return!0}function bV(t,e,n){var i={};return xV(e,(function(e){var r,o=i[e]=((r=function(){}).prototype.__hidden=r.prototype,new r);xV(t[e],(function(t,i){if(_D.isValidType(i)){var r={type:i,visual:t};n&&n(r,e),o[i]=new _D(r),"opacity"===i&&((r=T(r)).type="colorAlpha",o.__hidden.__alphaForOpacity=new _D(r))}}))})),i}function wV(t,e,n){var i;E(n,(function(t){e.hasOwnProperty(t)&&_V(e[t])&&(i=!0)})),i&&E(n,(function(n){e.hasOwnProperty(n)&&_V(e[n])?t[n]=T(e[n]):delete t[n]}))}var SV={lineX:MV(0),lineY:MV(1),rect:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])},rect:function(t,e,n){return t&&n.boundingRect.intersect(t)}},polygon:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])&&A_(n.range,t[0],t[1])},rect:function(t,e,n){var i=n.range;if(!t||i.length<=1)return!1;var r=t.x,o=t.y,a=t.width,s=t.height,l=i[0];return!!(A_(i,r,o)||A_(i,r+a,o)||A_(i,r,o+s)||A_(i,r+a,o+s)||ze.create(t).contain(l[0],l[1])||Yh(r,o,r+a,o,i)||Yh(r,o,r,o+s,i)||Yh(r+a,o,r+a,o+s,i)||Yh(r,o+s,r+a,o+s,i))||void 0}}};function MV(t){var e=["x","y"],n=["width","height"];return{point:function(e,n,i){if(e){var r=i.range;return IV(e[t],r)}},rect:function(i,r,o){if(i){var a=o.range,s=[i[e[t]],i[e[t]]+i[n[t]]];return s[1]e[0][1]&&(e[0][1]=o[0]),o[1]e[1][1]&&(e[1][1]=o[1])}return e&&RV(e)}};function RV(t){return new ze(t[0][0],t[1][0],t[0][1]-t[0][0],t[1][1]-t[1][0])}var NV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.ecModel=t,this.api=e,this.model,(this._brushController=new Jk(e.getZr())).on("brush",W(this._onBrush,this)).mount()},e.prototype.render=function(t,e,n,i){this.model=t,this._updateController(t,e,n,i)},e.prototype.updateTransform=function(t,e,n,i){AV(e),this._updateController(t,e,n,i)},e.prototype.updateVisual=function(t,e,n,i){this.updateTransform(t,e,n,i)},e.prototype.updateView=function(t,e,n,i){this._updateController(t,e,n,i)},e.prototype._updateController=function(t,e,n,i){(!i||i.$from!==t.id)&&this._brushController.setPanels(t.brushTargetManager.makePanelOpts(n)).enableBrush(t.brushOption).updateCovers(t.areas.slice())},e.prototype.dispose=function(){this._brushController.dispose()},e.prototype._onBrush=function(t){var e=this.model.id,n=this.model.brushTargetManager.setOutputRanges(t.areas,this.ecModel);(!t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:"brush",brushId:e,areas:T(n),$from:e}),t.isEnd&&this.api.dispatchAction({type:"brushEnd",brushId:e,areas:T(n),$from:e})},e.type="brush",e}(Tg),EV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.areas=[],n.brushOption={},n}return n(e,t),e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&wV(n,t,["inBrush","outOfBrush"]);var i=n.inBrush=n.inBrush||{};n.outOfBrush=n.outOfBrush||{color:"#ddd"},i.hasOwnProperty("liftZ")||(i.liftZ=5)},e.prototype.setAreas=function(t){t&&(this.areas=z(t,(function(t){return zV(this.option,t)}),this))},e.prototype.setBrushOption=function(t){this.brushOption=zV(this.option,t),this.brushType=this.brushOption.brushType},e.type="brush",e.dependencies=["geo","grid","xAxis","yAxis","parallel","series"],e.defaultOption={seriesIndex:"all",brushType:"rect",brushMode:"single",transformable:!0,brushStyle:{borderWidth:1,color:"rgba(210,219,238,0.3)",borderColor:"#D2DBEE"},throttleType:"fixRate",throttleDelay:0,removeOnClick:!0,z:1e4},e}(Rp);function zV(t,e){return C({brushType:t.brushType,brushMode:t.brushMode,transformable:t.transformable,brushStyle:new Mc(t.brushStyle).getItemStyle(),removeOnClick:t.removeOnClick,z:t.z},e,!0)}var VV=["rect","polygon","lineX","lineY","keep","clear"],BV=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n){var i,r,o;e.eachComponent({mainType:"brush"},(function(t){i=t.brushType,r=t.brushOption.brushMode||"single",o=o||!!t.areas.length})),this._brushType=i,this._brushMode=r,E(t.get("type",!0),(function(e){t.setIconStatus(e,("keep"===e?"multiple"===r:"clear"===e?o:e===i)?"emphasis":"normal")}))},e.prototype.updateView=function(t,e,n){this.render(t,e,n)},e.prototype.getIcons=function(){var t=this.model,e=t.get("icon",!0),n={};return E(t.get("type",!0),(function(t){e[t]&&(n[t]=e[t])})),n},e.prototype.onclick=function(t,e,n){var i=this._brushType,r=this._brushMode;"clear"===n?(e.dispatchAction({type:"axisAreaSelect",intervals:[]}),e.dispatchAction({type:"brush",command:"clear",areas:[]})):e.dispatchAction({type:"takeGlobalCursor",key:"brush",brushOption:{brushType:"keep"===n?i:i!==n&&n,brushMode:"keep"===n?"multiple"===r?"single":"multiple":r}})},e.getDefaultOption=function(t){return{show:!0,type:VV.slice(),icon:{rect:"M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13",polygon:"M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2",lineX:"M15.2,30 M19.7,15.6V1.9H29 M34.8,1.9H40.4 M55.3,15.6V1.9H45.9 M19.7,44.4V58.1H29 M34.8,58.1H40.4 M55.3,44.4 V58.1H45.9 M12.5,20.3l-9.4,9.6l9.6,9.8 M3.1,29.9h16.5 M62.5,20.3l9.4,9.6L62.3,39.7 M71.9,29.9H55.4",lineY:"M38.8,7.7 M52.7,12h13.2v9 M65.9,26.6V32 M52.7,46.3h13.2v-9 M24.9,12H11.8v9 M11.8,26.6V32 M24.9,46.3H11.8v-9 M48.2,5.1l-9.3-9l-9.4,9.2 M38.9-3.9V12 M48.2,53.3l-9.3,9l-9.4-9.2 M38.9,62.3V46.4",keep:"M4,10.5V1h10.3 M20.7,1h6.1 M33,1h6.1 M55.4,10.5V1H45.2 M4,17.3v6.6 M55.6,17.3v6.6 M4,30.5V40h10.3 M20.7,40 h6.1 M33,40h6.1 M55.4,30.5V40H45.2 M21,18.9h62.9v48.6H21V18.9z",clear:"M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2"},title:t.getLocaleModel().get(["toolbox","brush","title"])}},e}(lz);var FV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode={type:"box",ignoreSize:!0},n}return n(e,t),e.type="title",e.defaultOption={z:6,show:!0,text:"",target:"blank",subtext:"",subtarget:"blank",left:0,top:0,backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,padding:5,itemGap:10,textStyle:{fontSize:18,fontWeight:"bold",color:"#464646"},subtextStyle:{fontSize:12,color:"#6E7079"}},e}(Rp),GV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){if(this.group.removeAll(),t.get("show")){var i=this.group,r=t.getModel("textStyle"),o=t.getModel("subtextStyle"),a=t.get("textAlign"),s=rt(t.get("textBaseline"),t.get("textVerticalAlign")),l=new Fs({style:nc(r,{text:t.get("text"),fill:r.getTextColor()},{disableBox:!0}),z2:10}),u=l.getBoundingRect(),h=t.get("subtext"),c=new Fs({style:nc(o,{text:h,fill:o.getTextColor(),y:u.height+t.get("itemGap"),verticalAlign:"top"},{disableBox:!0}),z2:10}),p=t.get("link"),d=t.get("sublink"),f=t.get("triggerEvent",!0);l.silent=!p&&!f,c.silent=!d&&!f,p&&l.on("click",(function(){bp(p,"_"+t.get("target"))})),d&&c.on("click",(function(){bp(d,"_"+t.get("subtarget"))})),Qs(l).eventData=Qs(c).eventData=f?{componentType:"title",componentIndex:t.componentIndex}:null,i.add(l),h&&i.add(c);var g=i.getBoundingRect(),y=t.getBoxLayoutParams();y.width=g.width,y.height=g.height;var v=Cp(y,{width:n.getWidth(),height:n.getHeight()},t.get("padding"));a||("middle"===(a=t.get("left")||t.get("right"))&&(a="center"),"right"===a?v.x+=v.width:"center"===a&&(v.x+=v.width/2)),s||("center"===(s=t.get("top")||t.get("bottom"))&&(s="middle"),"bottom"===s?v.y+=v.height:"middle"===s&&(v.y+=v.height/2),s=s||"top"),i.x=v.x,i.y=v.y,i.markRedraw();var m={align:a,verticalAlign:s};l.setStyle(m),c.setStyle(m),g=i.getBoundingRect();var x=v.margin,_=t.getItemStyle(["color","opacity"]);_.fill=t.get("backgroundColor");var b=new zs({shape:{x:g.x-x[3],y:g.y-x[0],width:g.width+x[1]+x[3],height:g.height+x[0]+x[2],r:t.get("borderRadius")},style:_,subPixelOptimize:!0,silent:!0});i.add(b)}},e.type="title",e}(Tg);var WV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode="box",n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n),this._initData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this._initData()},e.prototype.setCurrentIndex=function(t){null==t&&(t=this.option.currentIndex);var e=this._data.count();this.option.loop?t=(t%e+e)%e:(t>=e&&(t=e-1),t<0&&(t=0)),this.option.currentIndex=t},e.prototype.getCurrentIndex=function(){return this.option.currentIndex},e.prototype.isIndexMax=function(){return this.getCurrentIndex()>=this._data.count()-1},e.prototype.setPlayState=function(t){this.option.autoPlay=!!t},e.prototype.getPlayState=function(){return!!this.option.autoPlay},e.prototype._initData=function(){var t,e=this.option,n=e.data||[],i=e.axisType,r=this._names=[];"category"===i?(t=[],E(n,(function(e,n){var i,o=Ao(Mo(e),"");q(e)?(i=T(e)).value=n:i=n,t.push(i),r.push(o)}))):t=n;var o={category:"ordinal",time:"time",value:"number"}[i]||"number";(this._data=new lx([{name:"value",type:o}],this)).initData(t,r)},e.prototype.getData=function(){return this._data},e.prototype.getCategories=function(){if("category"===this.get("axisType"))return this._names.slice()},e.type="timeline",e.defaultOption={z:4,show:!0,axisType:"time",realtime:!0,left:"20%",top:null,right:"20%",bottom:0,width:null,height:40,padding:5,controlPosition:"left",autoPlay:!1,rewind:!1,loop:!0,playInterval:2e3,currentIndex:0,itemStyle:{},label:{color:"#000"},data:[]},e}(Rp),HV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline.slider",e.defaultOption=Cc(WV.defaultOption,{backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,orient:"horizontal",inverse:!1,tooltip:{trigger:"item"},symbol:"circle",symbolSize:12,lineStyle:{show:!0,width:2,color:"#DAE1F5"},label:{position:"auto",show:!0,interval:"auto",rotate:0,color:"#A4B1D7"},itemStyle:{color:"#A4B1D7",borderWidth:1},checkpointStyle:{symbol:"circle",symbolSize:15,color:"#316bf3",borderColor:"#fff",borderWidth:2,shadowBlur:2,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0, 0, 0, 0.3)",animation:!0,animationDuration:300,animationEasing:"quinticInOut"},controlStyle:{show:!0,showPlayBtn:!0,showPrevBtn:!0,showNextBtn:!0,itemSize:24,itemGap:12,position:"left",playIcon:"path://M31.6,53C17.5,53,6,41.5,6,27.4S17.5,1.8,31.6,1.8C45.7,1.8,57.2,13.3,57.2,27.4S45.7,53,31.6,53z M31.6,3.3 C18.4,3.3,7.5,14.1,7.5,27.4c0,13.3,10.8,24.1,24.1,24.1C44.9,51.5,55.7,40.7,55.7,27.4C55.7,14.1,44.9,3.3,31.6,3.3z M24.9,21.3 c0-2.2,1.6-3.1,3.5-2l10.5,6.1c1.899,1.1,1.899,2.9,0,4l-10.5,6.1c-1.9,1.1-3.5,0.2-3.5-2V21.3z",stopIcon:"path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z",nextIcon:"M2,18.5A1.52,1.52,0,0,1,.92,18a1.49,1.49,0,0,1,0-2.12L7.81,9.36,1,3.11A1.5,1.5,0,1,1,3,.89l8,7.34a1.48,1.48,0,0,1,.49,1.09,1.51,1.51,0,0,1-.46,1.1L3,18.08A1.5,1.5,0,0,1,2,18.5Z",prevIcon:"M10,.5A1.52,1.52,0,0,1,11.08,1a1.49,1.49,0,0,1,0,2.12L4.19,9.64,11,15.89a1.5,1.5,0,1,1-2,2.22L1,10.77A1.48,1.48,0,0,1,.5,9.68,1.51,1.51,0,0,1,1,8.58L9,.92A1.5,1.5,0,0,1,10,.5Z",prevBtnSize:18,nextBtnSize:18,color:"#A4B1D7",borderColor:"#A4B1D7",borderWidth:1},emphasis:{label:{show:!0,color:"#6f778d"},itemStyle:{color:"#316BF3"},controlStyle:{color:"#316BF3",borderColor:"#316BF3",borderWidth:2}},progress:{lineStyle:{color:"#316BF3"},itemStyle:{color:"#316BF3"},label:{color:"#6f778d"}},data:[]}),e}(WV);R(HV,vf.prototype);var YV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline",e}(Tg),XV=function(t){function e(e,n,i,r){var o=t.call(this,e,n,i)||this;return o.type=r||"value",o}return n(e,t),e.prototype.getLabelModel=function(){return this.model.getModel("label")},e.prototype.isHorizontal=function(){return"horizontal"===this.model.get("orient")},e}(nb),UV=Math.PI,ZV=Oo(),jV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.api=e},e.prototype.render=function(t,e,n){if(this.model=t,this.api=n,this.ecModel=e,this.group.removeAll(),t.get("show",!0)){var i=this._layout(t,n),r=this._createGroup("_mainGroup"),o=this._createGroup("_labelGroup"),a=this._axis=this._createAxis(i,t);t.formatTooltip=function(t){return ng("nameValue",{noName:!0,value:a.scale.getLabel({value:t})})},E(["AxisLine","AxisTick","Control","CurrentPointer"],(function(e){this["_render"+e](i,r,a,t)}),this),this._renderAxisLabel(i,o,a,t),this._position(i,t)}this._doPlayStop(),this._updateTicksStatus()},e.prototype.remove=function(){this._clearTimer(),this.group.removeAll()},e.prototype.dispose=function(){this._clearTimer()},e.prototype._layout=function(t,e){var n,i,r,o,a=t.get(["label","position"]),s=t.get("orient"),l=function(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()},t.get("padding"))}(t,e),u={horizontal:"center",vertical:(n=null==a||"auto"===a?"horizontal"===s?l.y+l.height/2=0||"+"===n?"left":"right"},h={horizontal:n>=0||"+"===n?"top":"bottom",vertical:"middle"},c={horizontal:0,vertical:UV/2},p="vertical"===s?l.height:l.width,d=t.getModel("controlStyle"),f=d.get("show",!0),g=f?d.get("itemSize"):0,y=f?d.get("itemGap"):0,v=g+y,m=t.get(["label","rotate"])||0;m=m*UV/180;var x=d.get("position",!0),_=f&&d.get("showPlayBtn",!0),b=f&&d.get("showPrevBtn",!0),w=f&&d.get("showNextBtn",!0),S=0,M=p;"left"===x||"bottom"===x?(_&&(i=[0,0],S+=v),b&&(r=[S,0],S+=v),w&&(o=[M-g,0],M-=v)):(_&&(i=[M-g,0],M-=v),b&&(r=[0,0],S+=v),w&&(o=[M-g,0],M-=v));var I=[S,M];return t.get("inverse")&&I.reverse(),{viewRect:l,mainLength:p,orient:s,rotation:c[s],labelRotation:m,labelPosOpt:n,labelAlign:t.get(["label","align"])||u[s],labelBaseline:t.get(["label","verticalAlign"])||t.get(["label","baseline"])||h[s],playPosition:i,prevBtnPosition:r,nextBtnPosition:o,axisExtent:I,controlSize:g,controlGap:y}},e.prototype._position=function(t,e){var n=this._mainGroup,i=this._labelGroup,r=t.viewRect;if("vertical"===t.orient){var o=[1,0,0,1,0,0],a=r.x,s=r.y+r.height;we(o,o,[-a,-s]),Se(o,o,-UV/2),we(o,o,[a,s]),(r=r.clone()).applyTransform(o)}var l=y(r),u=y(n.getBoundingRect()),h=y(i.getBoundingRect()),c=[n.x,n.y],p=[i.x,i.y];p[0]=c[0]=l[0][0];var d,f=t.labelPosOpt;null==f||U(f)?(v(c,u,l,1,d="+"===f?0:1),v(p,h,l,1,1-d)):(v(c,u,l,1,d=f>=0?0:1),p[1]=c[1]+f);function g(t){t.originX=l[0][0]-t.x,t.originY=l[1][0]-t.y}function y(t){return[[t.x,t.x+t.width],[t.y,t.y+t.height]]}function v(t,e,n,i,r){t[i]+=n[i][r]-e[i][r]}n.setPosition(c),i.setPosition(p),n.rotation=i.rotation=t.rotation,g(n),g(i)},e.prototype._createAxis=function(t,e){var n=e.getData(),i=e.get("axisType"),r=function(t,e){if(e=e||t.get("type"),e)switch(e){case"category":return new Lx({ordinalMeta:t.getCategories(),extent:[1/0,-1/0]});case"time":return new Zx({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC")});default:return new Ox}}(e,i);r.getTicks=function(){return n.mapArray(["value"],(function(t){return{value:t}}))};var o=n.getDataExtent("value");r.setExtent(o[0],o[1]),r.calcNiceTicks();var a=new XV("value",r,t.axisExtent,i);return a.model=e,a},e.prototype._createGroup=function(t){var e=this[t]=new zr;return this.group.add(e),e},e.prototype._renderAxisLine=function(t,e,n,i){var r=n.getExtent();if(i.get(["lineStyle","show"])){var o=new Zu({shape:{x1:r[0],y1:0,x2:r[1],y2:0},style:A({lineCap:"round"},i.getModel("lineStyle").getLineStyle()),silent:!0,z2:1});e.add(o);var a=this._progressLine=new Zu({shape:{x1:r[0],x2:this._currentPointer?this._currentPointer.x:r[0],y1:0,y2:0},style:k({lineCap:"round",lineWidth:o.style.lineWidth},i.getModel(["progress","lineStyle"]).getLineStyle()),silent:!0,z2:1});e.add(a)}},e.prototype._renderAxisTick=function(t,e,n,i){var r=this,o=i.getData(),a=n.scale.getTicks();this._tickSymbols=[],E(a,(function(t){var a=n.dataToCoord(t.value),s=o.getItemModel(t.value),l=s.getModel("itemStyle"),u=s.getModel(["emphasis","itemStyle"]),h=s.getModel(["progress","itemStyle"]),c={x:a,y:0,onclick:W(r._changeTimeline,r,t.value)},p=qV(s,l,e,c);p.ensureState("emphasis").style=u.getItemStyle(),p.ensureState("progress").style=h.getItemStyle(),Hl(p);var d=Qs(p);s.get("tooltip")?(d.dataIndex=t.value,d.dataModel=i):d.dataIndex=d.dataModel=null,r._tickSymbols.push(p)}))},e.prototype._renderAxisLabel=function(t,e,n,i){var r=this;if(n.getLabelModel().get("show")){var o=i.getData(),a=n.getViewLabels();this._tickLabels=[],E(a,(function(i){var a=i.tickValue,s=o.getItemModel(a),l=s.getModel("label"),u=s.getModel(["emphasis","label"]),h=s.getModel(["progress","label"]),c=n.dataToCoord(i.tickValue),p=new Fs({x:c,y:0,rotation:t.labelRotation-t.rotation,onclick:W(r._changeTimeline,r,a),silent:!1,style:nc(l,{text:i.formattedLabel,align:t.labelAlign,verticalAlign:t.labelBaseline})});p.ensureState("emphasis").style=nc(u),p.ensureState("progress").style=nc(h),e.add(p),Hl(p),ZV(p).dataIndex=a,r._tickLabels.push(p)}))}},e.prototype._renderControl=function(t,e,n,i){var r=t.controlSize,o=t.rotation,a=i.getModel("controlStyle").getItemStyle(),s=i.getModel(["emphasis","controlStyle"]).getItemStyle(),l=i.getPlayState(),u=i.get("inverse",!0);function h(t,n,l,u){if(t){var h=Ir(rt(i.get(["controlStyle",n+"BtnSize"]),r),r),c=function(t,e,n,i){var r=i.style,o=Hh(t.get(["controlStyle",e]),i||{},new ze(n[0],n[1],n[2],n[3]));r&&o.setStyle(r);return o}(i,n+"Icon",[0,-h/2,h,h],{x:t[0],y:t[1],originX:r/2,originY:0,rotation:u?-o:0,rectHover:!0,style:a,onclick:l});c.ensureState("emphasis").style=s,e.add(c),Hl(c)}}h(t.nextBtnPosition,"next",W(this._changeTimeline,this,u?"-":"+")),h(t.prevBtnPosition,"prev",W(this._changeTimeline,this,u?"+":"-")),h(t.playPosition,l?"stop":"play",W(this._handlePlayClick,this,!l),!0)},e.prototype._renderCurrentPointer=function(t,e,n,i){var r=i.getData(),o=i.getCurrentIndex(),a=r.getItemModel(o).getModel("checkpointStyle"),s=this,l={onCreate:function(t){t.draggable=!0,t.drift=W(s._handlePointerDrag,s),t.ondragend=W(s._handlePointerDragend,s),KV(t,s._progressLine,o,n,i,!0)},onUpdate:function(t){KV(t,s._progressLine,o,n,i)}};this._currentPointer=qV(a,a,this._mainGroup,{},this._currentPointer,l)},e.prototype._handlePlayClick=function(t){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:t,from:this.uid})},e.prototype._handlePointerDrag=function(t,e,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},e.prototype._handlePointerDragend=function(t){this._pointerChangeTimeline([t.offsetX,t.offsetY],!0)},e.prototype._pointerChangeTimeline=function(t,e){var n=this._toAxisCoord(t)[0],i=jr(this._axis.getExtent().slice());n>i[1]&&(n=i[1]),n=0&&(a[o]=+a[o].toFixed(c)),[a,h]}var sB={min:H(aB,"min"),max:H(aB,"max"),average:H(aB,"average"),median:H(aB,"median")};function lB(t,e){if(e){var n=t.getData(),i=t.coordinateSystem,r=i&&i.dimensions;if(!function(t){return!isNaN(parseFloat(t.x))&&!isNaN(parseFloat(t.y))}(e)&&!Y(e.coord)&&Y(r)){var o=uB(e,n,i,t);if((e=T(e)).type&&sB[e.type]&&o.baseAxis&&o.valueAxis){var a=P(r,o.baseAxis.dim),s=P(r,o.valueAxis.dim),l=sB[e.type](n,o.baseDataDim,o.valueDataDim,a,s);e.coord=l[0],e.value=l[1]}else e.coord=[null!=e.xAxis?e.xAxis:e.radiusAxis,null!=e.yAxis?e.yAxis:e.angleAxis]}if(null!=e.coord&&Y(r))for(var u=e.coord,h=0;h<2;h++)sB[u[h]]&&(u[h]=pB(n,n.mapDimension(r[h]),u[h]));else e.coord=[];return e}}function uB(t,e,n,i){var r={};return null!=t.valueIndex||null!=t.valueDim?(r.valueDataDim=null!=t.valueIndex?e.getDimension(t.valueIndex):t.valueDim,r.valueAxis=n.getAxis(function(t,e){var n=t.getData().getDimensionInfo(e);return n&&n.coordDim}(i,r.valueDataDim)),r.baseAxis=n.getOtherAxis(r.valueAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim)):(r.baseAxis=i.getBaseAxis(),r.valueAxis=n.getOtherAxis(r.baseAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim),r.valueDataDim=e.mapDimension(r.valueAxis.dim)),r}function hB(t,e){return!(t&&t.containData&&e.coord&&!oB(e))||t.containData(e.coord)}function cB(t,e){return t?function(t,n,i,r){return wf(r<2?t.coord&&t.coord[r]:t.value,e[r])}:function(t,n,i,r){return wf(t.value,e[r])}}function pB(t,e,n){if("average"===n){var i=0,r=0;return t.each(e,(function(t,e){isNaN(t)||(i+=t,r++)})),i/r}return"median"===n?t.getMedian(e):t.getDataExtent(e)["max"===n?1:0]}var dB=Oo(),fB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){this.markerGroupMap=yt()},e.prototype.render=function(t,e,n){var i=this,r=this.markerGroupMap;r.each((function(t){dB(t).keep=!1})),e.eachSeries((function(t){var r=iB.getMarkerModelFromSeries(t,i.type);r&&i.renderSeries(t,r,e,n)})),r.each((function(t){!dB(t).keep&&i.group.remove(t.group)}))},e.prototype.markKeep=function(t){dB(t).keep=!0},e.prototype.toggleBlurSeries=function(t,e){var n=this;E(t,(function(t){var i=iB.getMarkerModelFromSeries(t,n.type);i&&i.getData().eachItemGraphicEl((function(t){t&&(e?Pl(t):Ol(t))}))}))},e.type="marker",e}(Tg);function gB(t,e,n){var i=e.coordinateSystem;t.each((function(r){var o,a=t.getItemModel(r),s=Ur(a.get("x"),n.getWidth()),l=Ur(a.get("y"),n.getHeight());if(isNaN(s)||isNaN(l)){if(e.getMarkerPosition)o=e.getMarkerPosition(t.getValues(t.dimensions,r));else if(i){var u=t.get(i.dimensions[0],r),h=t.get(i.dimensions[1],r);o=i.dataToPoint([u,h])}}else o=[s,l];isNaN(s)||(o[0]=s),isNaN(l)||(o[1]=l),t.setItemLayout(r,o)}))}var yB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=iB.getMarkerModelFromSeries(t,"markPoint");e&&(gB(e.getData(),t,n),this.markerGroupMap.get(t.id).updateLayout())}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new hS),u=function(t,e,n){var i;i=t?z(t&&t.dimensions,(function(t){return A(A({},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{}),{name:t,ordinalMeta:null})})):[{name:"value",type:"float"}];var r=new lx(i,n),o=z(n.get("data"),H(lB,e));t&&(o=B(o,H(hB,t)));var a=cB(!!t,i);return r.initData(o,null,a),r}(r,t,e);e.setData(u),gB(e.getData(),t,i),u.each((function(t){var n=u.getItemModel(t),i=n.getShallow("symbol"),r=n.getShallow("symbolSize"),o=n.getShallow("symbolRotate"),s=n.getShallow("symbolOffset"),l=n.getShallow("symbolKeepAspect");if(X(i)||X(r)||X(o)||X(s)){var h=e.getRawValue(t),c=e.getDataParams(t);X(i)&&(i=i(h,c)),X(r)&&(r=r(h,c)),X(o)&&(o=o(h,c)),X(s)&&(s=s(h,c))}var p=n.getModel("itemStyle").getItemStyle(),d=Ty(a,"color");p.fill||(p.fill=d),u.setItemVisual(t,{symbol:i,symbolSize:r,symbolRotate:o,symbolOffset:s,symbolKeepAspect:l,style:p})})),l.updateData(u),this.group.add(l.group),u.eachItemGraphicEl((function(t){t.traverse((function(t){Qs(t).dataModel=e}))})),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markPoint",e}(fB);var vB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markLine",e.defaultOption={z:5,symbol:["circle","arrow"],symbolSize:[8,16],symbolOffset:0,precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end",distance:5},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"},e}(iB),mB=Oo(),xB=function(t,e,n,i){var r,o=t.getData();if(Y(i))r=i;else{var a=i.type;if("min"===a||"max"===a||"average"===a||"median"===a||null!=i.xAxis||null!=i.yAxis){var s=void 0,l=void 0;if(null!=i.yAxis||null!=i.xAxis)s=e.getAxis(null!=i.yAxis?"y":"x"),l=it(i.yAxis,i.xAxis);else{var u=uB(i,o,e,t);s=u.valueAxis,l=pB(o,yx(o,u.valueDataDim),a)}var h="x"===s.dim?0:1,c=1-h,p=T(i),d={coord:[]};p.type=null,p.coord=[],p.coord[c]=-1/0,d.coord[c]=1/0;var f=n.get("precision");f>=0&&j(l)&&(l=+l.toFixed(Math.min(f,20))),p.coord[h]=d.coord[h]=l,r=[p,d,{type:a,valueIndex:i.valueIndex,value:l}]}else r=[]}var g=[lB(t,r[0]),lB(t,r[1]),A({},r[2])];return g[2].type=g[2].type||null,C(g[2],g[0]),C(g[2],g[1]),g};function _B(t){return!isNaN(t)&&!isFinite(t)}function bB(t,e,n,i){var r=1-t,o=i.dimensions[t];return _B(e[r])&&_B(n[r])&&e[t]===n[t]&&i.getAxis(o).containData(e[t])}function wB(t,e){if("cartesian2d"===t.type){var n=e[0].coord,i=e[1].coord;if(n&&i&&(bB(1,n,i,t)||bB(0,n,i,t)))return!0}return hB(t,e[0])&&hB(t,e[1])}function SB(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=Ur(s.get("x"),r.getWidth()),u=Ur(s.get("y"),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition)o=i.getMarkerPosition(t.getValues(t.dimensions,e));else{var h=a.dimensions,c=t.get(h[0],e),p=t.get(h[1],e);o=a.dataToPoint([c,p])}if(MS(a,"cartesian2d")){var d=a.getAxis("x"),f=a.getAxis("y");h=a.dimensions;_B(t.get(h[0],e))?o[0]=d.toGlobalCoord(d.getExtent()[n?0:1]):_B(t.get(h[1],e))&&(o[1]=f.toGlobalCoord(f.getExtent()[n?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];t.setItemLayout(e,o)}var MB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=iB.getMarkerModelFromSeries(t,"markLine");if(e){var i=e.getData(),r=mB(e).from,o=mB(e).to;r.each((function(e){SB(r,e,!0,t,n),SB(o,e,!1,t,n)})),i.each((function(t){i.setItemLayout(t,[r.getItemLayout(t),o.getItemLayout(t)])})),this.markerGroupMap.get(t.id).updateLayout()}}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new RA);this.group.add(l.group);var u=function(t,e,n){var i;i=t?z(t&&t.dimensions,(function(t){return A(A({},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{}),{name:t,ordinalMeta:null})})):[{name:"value",type:"float"}];var r=new lx(i,n),o=new lx(i,n),a=new lx([],n),s=z(n.get("data"),H(xB,e,t,n));t&&(s=B(s,H(wB,t)));var l=cB(!!t,i);return r.initData(z(s,(function(t){return t[0]})),null,l),o.initData(z(s,(function(t){return t[1]})),null,l),a.initData(z(s,(function(t){return t[2]}))),a.hasItemOption=!0,{from:r,to:o,line:a}}(r,t,e),h=u.from,c=u.to,p=u.line;mB(e).from=h,mB(e).to=c,e.setData(p);var d=e.get("symbol"),f=e.get("symbolSize"),g=e.get("symbolRotate"),y=e.get("symbolOffset");function v(e,n,r){var o=e.getItemModel(n);SB(e,n,r,t,i);var s=o.getModel("itemStyle").getItemStyle();null==s.fill&&(s.fill=Ty(a,"color")),e.setItemVisual(n,{symbolKeepAspect:o.get("symbolKeepAspect"),symbolOffset:rt(o.get("symbolOffset",!0),y[r?0:1]),symbolRotate:rt(o.get("symbolRotate",!0),g[r?0:1]),symbolSize:rt(o.get("symbolSize"),f[r?0:1]),symbol:rt(o.get("symbol",!0),d[r?0:1]),style:s})}Y(d)||(d=[d,d]),Y(f)||(f=[f,f]),Y(g)||(g=[g,g]),Y(y)||(y=[y,y]),u.from.each((function(t){v(h,t,!0),v(c,t,!1)})),p.each((function(t){var e=p.getItemModel(t).getModel("lineStyle").getLineStyle();p.setItemLayout(t,[h.getItemLayout(t),c.getItemLayout(t)]),null==e.stroke&&(e.stroke=h.getItemVisual(t,"style").fill),p.setItemVisual(t,{fromSymbolKeepAspect:h.getItemVisual(t,"symbolKeepAspect"),fromSymbolOffset:h.getItemVisual(t,"symbolOffset"),fromSymbolRotate:h.getItemVisual(t,"symbolRotate"),fromSymbolSize:h.getItemVisual(t,"symbolSize"),fromSymbol:h.getItemVisual(t,"symbol"),toSymbolKeepAspect:c.getItemVisual(t,"symbolKeepAspect"),toSymbolOffset:c.getItemVisual(t,"symbolOffset"),toSymbolRotate:c.getItemVisual(t,"symbolRotate"),toSymbolSize:c.getItemVisual(t,"symbolSize"),toSymbol:c.getItemVisual(t,"symbol"),style:e})})),l.updateData(p),u.line.eachItemGraphicEl((function(t){Qs(t).dataModel=e,t.traverse((function(t){Qs(t).dataModel=e}))})),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markLine",e}(fB);var IB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markArea",e.defaultOption={z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}},e}(iB),TB=Oo(),CB=function(t,e,n,i){var r=i[0],o=i[1];if(r&&o){var a=lB(t,r),s=lB(t,o),l=a.coord,u=s.coord;l[0]=it(l[0],-1/0),l[1]=it(l[1],-1/0),u[0]=it(u[0],1/0),u[1]=it(u[1],1/0);var h=D([{},a,s]);return h.coord=[a.coord,s.coord],h.x0=a.x,h.y0=a.y,h.x1=s.x,h.y1=s.y,h}};function DB(t){return!isNaN(t)&&!isFinite(t)}function AB(t,e,n,i){var r=1-t;return DB(e[r])&&DB(n[r])}function kB(t,e){var n=e.coord[0],i=e.coord[1],r={coord:n,x:e.x0,y:e.y0},o={coord:i,x:e.x1,y:e.y1};return MS(t,"cartesian2d")?!(!n||!i||!AB(1,n,i)&&!AB(0,n,i))||function(t,e,n){return!(t&&t.containZone&&e.coord&&n.coord&&!oB(e)&&!oB(n))||t.containZone(e.coord,n.coord)}(t,r,o):hB(t,r)||hB(t,o)}function LB(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=Ur(s.get(n[0]),r.getWidth()),u=Ur(s.get(n[1]),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition){var h=t.getValues(["x0","y0"],e),c=t.getValues(["x1","y1"],e),p=a.clampData(h),d=a.clampData(c),f=[];"x0"===n[0]?f[0]=p[0]>d[0]?c[0]:h[0]:f[0]=p[0]>d[0]?h[0]:c[0],"y0"===n[1]?f[1]=p[1]>d[1]?c[1]:h[1]:f[1]=p[1]>d[1]?h[1]:c[1],o=i.getMarkerPosition(f,n,!0)}else{var g=[m=t.get(n[0],e),x=t.get(n[1],e)];a.clampData&&a.clampData(g,g),o=a.dataToPoint(g,!0)}if(MS(a,"cartesian2d")){var y=a.getAxis("x"),v=a.getAxis("y"),m=t.get(n[0],e),x=t.get(n[1],e);DB(m)?o[0]=y.toGlobalCoord(y.getExtent()["x0"===n[0]?0:1]):DB(x)&&(o[1]=v.toGlobalCoord(v.getExtent()["y0"===n[1]?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];return o}var PB=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]],OB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=iB.getMarkerModelFromSeries(t,"markArea");if(e){var i=e.getData();i.each((function(e){var r=z(PB,(function(r){return LB(i,e,r,t,n)}));i.setItemLayout(e,r),i.getItemGraphicEl(e).setShape("points",r)}))}}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,{group:new zr});this.group.add(l.group),this.markKeep(l);var u=function(t,e,n){var i,r,o=["x0","y0","x1","y1"];if(t){var a=z(t&&t.dimensions,(function(t){var n=e.getData();return A(A({},n.getDimensionInfo(n.mapDimension(t))||{}),{name:t,ordinalMeta:null})}));r=z(o,(function(t,e){return{name:t,type:a[e%2].type}})),i=new lx(r,n)}else i=new lx(r=[{name:"value",type:"float"}],n);var s=z(n.get("data"),H(CB,e,t,n));t&&(s=B(s,H(kB,t)));var l=t?function(t,e,n,i){return wf(t.coord[Math.floor(i/2)][i%2],r[i])}:function(t,e,n,i){return wf(t.value,r[i])};return i.initData(s,null,l),i.hasItemOption=!0,i}(r,t,e);e.setData(u),u.each((function(e){var n=z(PB,(function(n){return LB(u,e,n,t,i)})),o=r.getAxis("x").scale,s=r.getAxis("y").scale,l=o.getExtent(),h=s.getExtent(),c=[o.parse(u.get("x0",e)),o.parse(u.get("x1",e))],p=[s.parse(u.get("y0",e)),s.parse(u.get("y1",e))];jr(c),jr(p);var d=!!(l[0]>c[1]||l[1]p[1]||h[1]=0},e.prototype.getOrient=function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},e.type="legend.plain",e.dependencies=["series"],e.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:"#ccc",inactiveBorderColor:"#ccc",inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:"#ccc",inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:"#333"},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:"#666",borderWidth:1,borderColor:"#666"},emphasis:{selectorLabel:{show:!0,color:"#eee",backgroundColor:"#666"}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1}},e}(Rp),NB=H,EB=E,zB=zr,VB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.newlineDisabled=!1,n}return n(e,t),e.prototype.init=function(){this.group.add(this._contentGroup=new zB),this.group.add(this._selectorGroup=new zB),this._isFirstRender=!0},e.prototype.getContentGroup=function(){return this._contentGroup},e.prototype.getSelectorGroup=function(){return this._selectorGroup},e.prototype.render=function(t,e,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),t.get("show",!0)){var r=t.get("align"),o=t.get("orient");r&&"auto"!==r||(r="right"===t.get("left")&&"vertical"===o?"right":"left");var a=t.get("selector",!0),s=t.get("selectorPosition",!0);!a||s&&"auto"!==s||(s="horizontal"===o?"end":"start"),this.renderInner(r,t,e,n,a,o,s);var l=t.getBoxLayoutParams(),u={width:n.getWidth(),height:n.getHeight()},h=t.get("padding"),c=Cp(l,u,h),p=this.layoutInner(t,r,c,i,a,s),d=Cp(k({width:p.width,height:p.height},l),u,h);this.group.x=d.x-p.x,this.group.y=d.y-p.y,this.group.markRedraw(),this.group.add(this._backgroundEl=dz(p,t))}},e.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},e.prototype.renderInner=function(t,e,n,i,r,o,a){var s=this.getContentGroup(),l=yt(),u=e.get("selectedMode"),h=[];n.eachRawSeries((function(t){!t.get("legendHoverLink")&&h.push(t.id)})),EB(e.getData(),(function(r,o){var a=r.get("name");if(!this.newlineDisabled&&(""===a||"\n"===a)){var c=new zB;return c.newline=!0,void s.add(c)}var p=n.getSeriesByName(a)[0];if(!l.get(a)){if(p){var d=p.getData(),f=d.getVisual("legendLineStyle")||{},g=d.getVisual("legendIcon"),y=d.getVisual("style");this._createItem(p,a,o,r,e,t,f,y,g,u,i).on("click",NB(BB,a,null,i,h)).on("mouseover",NB(GB,p.name,null,i,h)).on("mouseout",NB(WB,p.name,null,i,h)),l.set(a,!0)}else n.eachRawSeries((function(n){if(!l.get(a)&&n.legendVisualProvider){var s=n.legendVisualProvider;if(!s.containName(a))return;var c=s.indexOfName(a),p=s.getItemVisual(c,"style"),d=s.getItemVisual(c,"legendIcon"),f=qn(p.fill);f&&0===f[3]&&(f[3]=.2,p=A(A({},p),{fill:ri(f,"rgba")})),this._createItem(n,a,o,r,e,t,{},p,d,u,i).on("click",NB(BB,null,a,i,h)).on("mouseover",NB(GB,null,a,i,h)).on("mouseout",NB(WB,null,a,i,h)),l.set(a,!0)}}),this);0}}),this),r&&this._createSelector(r,e,i,o,a)},e.prototype._createSelector=function(t,e,n,i,r){var o=this.getSelectorGroup();EB(t,(function(t){var i=t.type,r=new Fs({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:"all"===i?"legendAllSelect":"legendInverseSelect"})}});o.add(r),tc(r,{normal:e.getModel("selectorLabel"),emphasis:e.getModel(["emphasis","selectorLabel"])},{defaultText:t.title}),Hl(r)}))},e.prototype._createItem=function(t,e,n,i,r,o,a,s,l,u,h){var c=t.visualDrawType,p=r.get("itemWidth"),d=r.get("itemHeight"),f=r.isSelected(e),g=i.get("symbolRotate"),y=i.get("symbolKeepAspect"),v=i.get("icon"),m=function(t,e,n,i,r,o,a){function s(t,e){"auto"===t.lineWidth&&(t.lineWidth=e.lineWidth>0?2:0),EB(t,(function(n,i){"inherit"===t[i]&&(t[i]=e[i])}))}var l=e.getModel("itemStyle"),u=l.getItemStyle(),h=0===t.lastIndexOf("empty",0)?"fill":"stroke",c=l.getShallow("decal");u.decal=c&&"inherit"!==c?gv(c,a):i.decal,"inherit"===u.fill&&(u.fill=i[r]);"inherit"===u.stroke&&(u.stroke=i[h]);"inherit"===u.opacity&&(u.opacity=("fill"===r?i:n).opacity);s(u,i);var p=e.getModel("lineStyle"),d=p.getLineStyle();if(s(d,n),"auto"===u.fill&&(u.fill=i.fill),"auto"===u.stroke&&(u.stroke=i.fill),"auto"===d.stroke&&(d.stroke=i.fill),!o){var f=e.get("inactiveBorderWidth"),g=u[h];u.lineWidth="auto"===f?i.lineWidth>0&&g?2:0:u.lineWidth,u.fill=e.get("inactiveColor"),u.stroke=e.get("inactiveBorderColor"),d.stroke=p.get("inactiveColor"),d.lineWidth=p.get("inactiveWidth")}return{itemStyle:u,lineStyle:d}}(l=v||l||"roundRect",i,a,s,c,f,h),x=new zB,_=i.getModel("textStyle");if(!X(t.getLegendIcon)||v&&"inherit"!==v){var b="inherit"===v&&t.getData().getVisual("symbol")?"inherit"===g?t.getData().getVisual("symbolRotate"):g:0;x.add(function(t){var e=t.icon||"roundRect",n=Wy(e,0,0,t.itemWidth,t.itemHeight,t.itemStyle.fill,t.symbolKeepAspect);n.setStyle(t.itemStyle),n.rotation=(t.iconRotate||0)*Math.PI/180,n.setOrigin([t.itemWidth/2,t.itemHeight/2]),e.indexOf("empty")>-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2);return n}({itemWidth:p,itemHeight:d,icon:l,iconRotate:b,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:y}))}else x.add(t.getLegendIcon({itemWidth:p,itemHeight:d,icon:l,iconRotate:g,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:y}));var w="left"===o?p+5:-5,S=o,M=r.get("formatter"),I=e;U(M)&&M?I=M.replace("{name}",null!=e?e:""):X(M)&&(I=M(e));var T=f?_.getTextColor():i.get("inactiveColor");x.add(new Fs({style:nc(_,{text:I,x:w,y:d/2,fill:T,align:S,verticalAlign:"middle"},{inheritColor:T})}));var C=new zs({shape:x.getBoundingRect(),invisible:!0}),D=i.getModel("tooltip");return D.get("show")&&Zh({el:C,componentModel:r,itemName:e,itemTooltipOption:D.option}),x.add(C),x.eachChild((function(t){t.silent=!0})),C.silent=!u,this.getContentGroup().add(x),Hl(x),x.__legendDataIndex=n,x},e.prototype.layoutInner=function(t,e,n,i,r,o){var a=this.getContentGroup(),s=this.getSelectorGroup();Tp(t.get("orient"),a,t.get("itemGap"),n.width,n.height);var l=a.getBoundingRect(),u=[-l.x,-l.y];if(s.markRedraw(),a.markRedraw(),r){Tp("horizontal",s,t.get("selectorItemGap",!0));var h=s.getBoundingRect(),c=[-h.x,-h.y],p=t.get("selectorButtonGap",!0),d=t.getOrient().index,f=0===d?"width":"height",g=0===d?"height":"width",y=0===d?"y":"x";"end"===o?c[d]+=l[f]+p:u[d]+=h[f]+p,c[1-d]+=l[g]/2-h[g]/2,s.x=c[0],s.y=c[1],a.x=u[0],a.y=u[1];var v={x:0,y:0};return v[f]=l[f]+p+h[f],v[g]=Math.max(l[g],h[g]),v[y]=Math.min(0,h[y]+c[1-d]),v}return a.x=u[0],a.y=u[1],this.group.getBoundingRect()},e.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},e.type="legend.plain",e}(Tg);function BB(t,e,n,i){WB(t,e,n,i),n.dispatchAction({type:"legendToggleSelect",name:null!=t?t:e}),GB(t,e,n,i)}function FB(t){for(var e,n=t.getZr().storage.getDisplayList(),i=0,r=n.length;in[r],f=[-c.x,-c.y];e||(f[i]=l[s]);var g=[0,0],y=[-p.x,-p.y],v=rt(t.get("pageButtonGap",!0),t.get("itemGap",!0));d&&("end"===t.get("pageButtonPosition",!0)?y[i]+=n[r]-p[r]:g[i]+=p[r]+v);y[1-i]+=c[o]/2-p[o]/2,l.setPosition(f),u.setPosition(g),h.setPosition(y);var m={x:0,y:0};if(m[r]=d?n[r]:c[r],m[o]=Math.max(c[o],p[o]),m[a]=Math.min(0,p[a]+y[1-i]),u.__rectSize=n[r],d){var x={x:0,y:0};x[r]=Math.max(n[r]-p[r]-v,0),x[o]=m[o],u.setClipPath(new zs({shape:x})),u.__rectSize=x[r]}else h.eachChild((function(t){t.attr({invisible:!0,silent:!0})}));var _=this._getPageInfo(t);return null!=_.pageIndex&&fh(l,{x:_.contentPosition[0],y:_.contentPosition[1]},d?t:null),this._updatePageInfoView(t,_),m},e.prototype._pageGo=function(t,e,n){var i=this._getPageInfo(e)[t];null!=i&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:e.id})},e.prototype._updatePageInfoView=function(t,e){var n=this._controllerGroup;E(["pagePrev","pageNext"],(function(i){var r=null!=e[i+"DataIndex"],o=n.childOfName(i);o&&(o.setStyle("fill",r?t.get("pageIconColor",!0):t.get("pageIconInactiveColor",!0)),o.cursor=r?"pointer":"default")}));var i=n.childOfName("pageText"),r=t.get("pageFormatter"),o=e.pageIndex,a=null!=o?o+1:0,s=e.pageCount;i&&r&&i.setStyle("text",U(r)?r.replace("{current}",null==a?"":a+"").replace("{total}",null==s?"":s+""):r({current:a,total:s}))},e.prototype._getPageInfo=function(t){var e=t.get("scrollDataIndex",!0),n=this.getContentGroup(),i=this._containerGroup.__rectSize,r=t.getOrient().index,o=qB[r],a=KB[r],s=this._findTargetItemIndex(e),l=n.children(),u=l[s],h=l.length,c=h?1:0,p={contentPosition:[n.x,n.y],pageCount:c,pageIndex:c-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!u)return p;var d=m(u);p.contentPosition[r]=-d.s;for(var f=s+1,g=d,y=d,v=null;f<=h;++f)(!(v=m(l[f]))&&y.e>g.s+i||v&&!x(v,g.s))&&(g=y.i>g.i?y:v)&&(null==p.pageNextDataIndex&&(p.pageNextDataIndex=g.i),++p.pageCount),y=v;for(f=s-1,g=d,y=d,v=null;f>=-1;--f)(v=m(l[f]))&&x(y,v.s)||!(g.i=e&&t.s<=e+i}},e.prototype._findTargetItemIndex=function(t){return this._showController?(this.getContentGroup().eachChild((function(i,r){var o=i.__legendDataIndex;null==n&&null!=o&&(n=r),o===t&&(e=r)})),null!=e?e:n):0;var e,n},e.type="legend.scroll",e}(VB);function JB(t){Nm(XB),t.registerComponentModel(UB),t.registerComponentView($B),function(t){t.registerAction("legendScroll","legendscroll",(function(t,e){var n=t.scrollDataIndex;null!=n&&e.eachComponent({mainType:"legend",subType:"scroll",query:t},(function(t){t.setScrollDataIndex(n)}))}))}(t)}var QB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.inside",e.defaultOption=Cc(KE.defaultOption,{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),e}(KE),tF=Oo();function eF(t,e,n){tF(t).coordSysRecordMap.each((function(t){var i=t.dataZoomInfoMap.get(e.uid);i&&(i.getRange=n)}))}function nF(t,e){if(e){t.removeKey(e.model.uid);var n=e.controller;n&&n.dispose()}}function iF(t,e){t.isDisposed()||t.dispatchAction({type:"dataZoom",animation:{easing:"cubicOut",duration:100},batch:e})}function rF(t,e,n,i){return t.coordinateSystem.containPoint([n,i])}function oF(t){t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,(function(t,e){var n=tF(e),i=n.coordSysRecordMap||(n.coordSysRecordMap=yt());i.each((function(t){t.dataZoomInfoMap=null})),t.eachComponent({mainType:"dataZoom",subType:"inside"},(function(t){E(jE(t).infoList,(function(n){var r=n.model.uid,o=i.get(r)||i.set(r,function(t,e){var n={model:e,containsPoint:H(rF,e),dispatchAction:H(iF,t),dataZoomInfoMap:null,controller:null},i=n.controller=new UI(t.getZr());return E(["pan","zoom","scrollMove"],(function(t){i.on(t,(function(e){var i=[];n.dataZoomInfoMap.each((function(r){if(e.isAvailableBehavior(r.model.option)){var o=(r.getRange||{})[t],a=o&&o(r.dzReferCoordSysInfo,n.model.mainType,n.controller,e);!r.model.get("disabled",!0)&&a&&i.push({dataZoomId:r.model.id,start:a[0],end:a[1]})}})),i.length&&n.dispatchAction(i)}))})),n}(e,n.model));(o.dataZoomInfoMap||(o.dataZoomInfoMap=yt())).set(t.uid,{dzReferCoordSysInfo:n,model:t,getRange:null})}))})),i.each((function(t){var e,n=t.controller,r=t.dataZoomInfoMap;if(r){var o=r.keys()[0];null!=o&&(e=r.get(o))}if(e){var a=function(t){var e,n="type_",i={type_true:2,type_move:1,type_false:0,type_undefined:-1},r=!0;return t.each((function(t){var o=t.model,a=!o.get("disabled",!0)&&(!o.get("zoomLock",!0)||"move");i[n+a]>i[n+e]&&(e=a),r=r&&o.get("preventDefaultMouseMove",!0)})),{controlType:e,opt:{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!0,preventDefaultMouseMove:!!r}}}(r);n.enable(a.controlType,a.opt),n.setPointerChecker(t.containsPoint),Fg(t,"dispatchAction",e.model.get("throttle",!0),"fixRate")}else nF(i,t)}))}))}var aF=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="dataZoom.inside",e}return n(e,t),e.prototype.render=function(e,n,i){t.prototype.render.apply(this,arguments),e.noTarget()?this._clear():(this.range=e.getPercentRange(),eF(i,e,{pan:W(sF.pan,this),zoom:W(sF.zoom,this),scrollMove:W(sF.scrollMove,this)}))},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){!function(t,e){for(var n=tF(t).coordSysRecordMap,i=n.keys(),r=0;r0?s.pixelStart+s.pixelLength-s.pixel:s.pixel-s.pixelStart)/s.pixelLength*(o[1]-o[0])+o[0],u=Math.max(1/i.scale,0);o[0]=(o[0]-l)*u+l,o[1]=(o[1]-l)*u+l;var h=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();return Ck(0,o,[0,100],0,h.minSpan,h.maxSpan),this.range=o,r[0]!==o[0]||r[1]!==o[1]?o:void 0}},pan:lF((function(t,e,n,i,r,o){var a=uF[i]([o.oldX,o.oldY],[o.newX,o.newY],e,r,n);return a.signal*(t[1]-t[0])*a.pixel/a.pixelLength})),scrollMove:lF((function(t,e,n,i,r,o){return uF[i]([0,0],[o.scrollDelta,o.scrollDelta],e,r,n).signal*(t[1]-t[0])*o.scrollDelta}))};function lF(t){return function(e,n,i,r){var o=this.range,a=o.slice(),s=e.axisModels[0];if(s)return Ck(t(a,s,e,n,i,r),a,[0,100],"all"),this.range=a,o[0]!==a[0]||o[1]!==a[1]?a:void 0}}var uF={grid:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem.getRect();return t=t||[0,0],"x"===o.dim?(a.pixel=e[0]-t[0],a.pixelLength=s.width,a.pixelStart=s.x,a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=s.height,a.pixelStart=s.y,a.signal=o.inverse?-1:1),a},polar:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),u=s.getAngleAxis().getExtent();return t=t?s.pointToCoord(t):[0,0],e=s.pointToCoord(e),"radiusAxis"===n.mainType?(a.pixel=e[0]-t[0],a.pixelLength=l[1]-l[0],a.pixelStart=l[0],a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=u[1]-u[0],a.pixelStart=u[0],a.signal=o.inverse?-1:1),a},singleAxis:function(t,e,n,i,r){var o=n.axis,a=r.model.coordinateSystem.getRect(),s={};return t=t||[0,0],"horizontal"===o.orient?(s.pixel=e[0]-t[0],s.pixelLength=a.width,s.pixelStart=a.x,s.signal=o.inverse?1:-1):(s.pixel=e[1]-t[1],s.pixelLength=a.height,s.pixelStart=a.y,s.signal=o.inverse?-1:1),s}};function hF(t){az(t),t.registerComponentModel(QB),t.registerComponentView(aF),oF(t)}var cF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.slider",e.layoutMode="box",e.defaultOption=Cc(KE.defaultOption,{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,borderColor:"#d2dbee",borderRadius:3,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#d2dbee",width:.5},areaStyle:{color:"#d2dbee",opacity:.2}},selectedDataBackground:{lineStyle:{color:"#8fb0f7",width:.5},areaStyle:{color:"#8fb0f7",opacity:.2}},fillerColor:"rgba(135,175,274,0.2)",handleIcon:"path://M-9.35,34.56V42m0-40V9.5m-2,0h4a2,2,0,0,1,2,2v21a2,2,0,0,1-2,2h-4a2,2,0,0,1-2-2v-21A2,2,0,0,1-11.35,9.5Z",handleSize:"100%",handleStyle:{color:"#fff",borderColor:"#ACB8D1"},moveHandleSize:7,moveHandleIcon:"path://M-320.9-50L-320.9-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-348-41-339-50-320.9-50z M-212.3-50L-212.3-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-239.4-41-230.4-50-212.3-50z M-103.7-50L-103.7-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-130.9-41-121.8-50-103.7-50z",moveHandleStyle:{color:"#D2DBEE",opacity:.7},showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#6E7079"},brushSelect:!0,brushStyle:{color:"rgba(135,175,274,0.15)"},emphasis:{handleStyle:{borderColor:"#8FB0F7"},moveHandleStyle:{color:"#8FB0F7"}}}),e}(KE),pF=zs,dF="horizontal",fF="vertical",gF=["line","bar","candlestick","scatter"],yF={easing:"cubicOut",duration:100,delay:0},vF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._displayables={},n}return n(e,t),e.prototype.init=function(t,e){this.api=e,this._onBrush=W(this._onBrush,this),this._onBrushEnd=W(this._onBrushEnd,this)},e.prototype.render=function(e,n,i,r){if(t.prototype.render.apply(this,arguments),Fg(this,"_dispatchZoomAction",e.get("throttle"),"fixRate"),this._orient=e.getOrient(),!1!==e.get("show")){if(e.noTarget())return this._clear(),void this.group.removeAll();r&&"dataZoom"===r.type&&r.from===this.uid||this._buildView(),this._updateView()}else this.group.removeAll()},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){Gg(this,"_dispatchZoomAction");var t=this.api.getZr();t.off("mousemove",this._onBrush),t.off("mouseup",this._onBrushEnd)},e.prototype._buildView=function(){var t=this.group;t.removeAll(),this._brushing=!1,this._displayables.brushRect=null,this._resetLocation(),this._resetInterval();var e=this._displayables.sliderGroup=new zr;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),t.add(e),this._positionGroup()},e.prototype._resetLocation=function(){var t=this.dataZoomModel,e=this.api,n=t.get("brushSelect")?7:0,i=this._findCoordRect(),r={width:e.getWidth(),height:e.getHeight()},o=this._orient===dF?{right:r.width-i.x-i.width,top:r.height-30-7-n,width:i.width,height:30}:{right:7,top:i.y,width:30,height:i.height},a=Lp(t.option);E(["right","top","width","height"],(function(t){"ph"===a[t]&&(a[t]=o[t])}));var s=Cp(a,r);this._location={x:s.x,y:s.y},this._size=[s.width,s.height],this._orient===fF&&this._size.reverse()},e.prototype._positionGroup=function(){var t=this.group,e=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),r=i&&i.get("inverse"),o=this._displayables.sliderGroup,a=(this._dataShadowInfo||{}).otherAxisInverse;o.attr(n!==dF||r?n===dF&&r?{scaleY:a?1:-1,scaleX:-1}:n!==fF||r?{scaleY:a?-1:1,scaleX:-1,rotation:Math.PI/2}:{scaleY:a?-1:1,scaleX:1,rotation:Math.PI/2}:{scaleY:a?1:-1,scaleX:1});var s=t.getBoundingRect([o]);t.x=e.x-s.x,t.y=e.y-s.y,t.markRedraw()},e.prototype._getViewExtent=function(){return[0,this._size[0]]},e.prototype._renderBackground=function(){var t=this.dataZoomModel,e=this._size,n=this._displayables.sliderGroup,i=t.get("brushSelect");n.add(new pF({silent:!0,shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:t.get("backgroundColor")},z2:-40}));var r=new pF({shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:"transparent"},z2:0,onclick:W(this._onClickPanel,this)}),o=this.api.getZr();i?(r.on("mousedown",this._onBrushStart,this),r.cursor="crosshair",o.on("mousemove",this._onBrush),o.on("mouseup",this._onBrushEnd)):(o.off("mousemove",this._onBrush),o.off("mouseup",this._onBrushEnd)),n.add(r)},e.prototype._renderDataShadow=function(){var t=this._dataShadowInfo=this._prepareDataShadowInfo();if(this._displayables.dataShadowSegs=[],t){var e=this._size,n=this._shadowSize||[],i=t.series,r=i.getRawData(),o=i.getShadowDim&&i.getShadowDim(),a=o&&r.getDimensionInfo(o)?i.getShadowDim():t.otherDim;if(null!=a){var s=this._shadowPolygonPts,l=this._shadowPolylinePts;if(r!==this._shadowData||a!==this._shadowDim||e[0]!==n[0]||e[1]!==n[1]){var u=r.getDataExtent(a),h=.3*(u[1]-u[0]);u=[u[0]-h,u[1]+h];var c,p=[0,e[1]],d=[0,e[0]],f=[[e[0],0],[0,0]],g=[],y=d[1]/(r.count()-1),v=0,m=Math.round(r.count()/e[0]);r.each([a],(function(t,e){if(m>0&&e%m)v+=y;else{var n=null==t||isNaN(t)||""===t,i=n?0:Xr(t,u,p,!0);n&&!c&&e?(f.push([f[f.length-1][0],0]),g.push([g[g.length-1][0],0])):!n&&c&&(f.push([v,0]),g.push([v,0])),f.push([v,i]),g.push([v,i]),v+=y,c=n}})),s=this._shadowPolygonPts=f,l=this._shadowPolylinePts=g}this._shadowData=r,this._shadowDim=a,this._shadowSize=[e[0],e[1]];for(var x=this.dataZoomModel,_=0;_<3;_++){var b=w(1===_);this._displayables.sliderGroup.add(b),this._displayables.dataShadowSegs.push(b)}}}function w(t){var e=x.getModel(t?"selectedDataBackground":"dataBackground"),n=new zr,i=new Wu({shape:{points:s},segmentIgnoreThreshold:1,style:e.getModel("areaStyle").getAreaStyle(),silent:!0,z2:-20}),r=new Yu({shape:{points:l},segmentIgnoreThreshold:1,style:e.getModel("lineStyle").getLineStyle(),silent:!0,z2:-19});return n.add(i),n.add(r),n}},e.prototype._prepareDataShadowInfo=function(){var t=this.dataZoomModel,e=t.get("showDataShadow");if(!1!==e){var n,i=this.ecModel;return t.eachTargetAxis((function(r,o){E(t.getAxisProxy(r,o).getTargetSeriesModels(),(function(t){if(!(n||!0!==e&&P(gF,t.get("type"))<0)){var a,s=i.getComponent(UE(r),o).axis,l=function(t){var e={x:"y",y:"x",radius:"angle",angle:"radius"};return e[t]}(r),u=t.coordinateSystem;null!=l&&u.getOtherAxis&&(a=u.getOtherAxis(s).inverse),l=t.getData().mapDimension(l),n={thisAxis:s,series:t,thisDim:r,otherDim:l,otherAxisInverse:a}}}),this)}),this),n}},e.prototype._renderHandle=function(){var t=this.group,e=this._displayables,n=e.handles=[null,null],i=e.handleLabels=[null,null],r=this._displayables.sliderGroup,o=this._size,a=this.dataZoomModel,s=this.api,l=a.get("borderRadius")||0,u=a.get("brushSelect"),h=e.filler=new pF({silent:u,style:{fill:a.get("fillerColor")},textConfig:{position:"inside"}});r.add(h),r.add(new pF({silent:!0,subPixelOptimize:!0,shape:{x:0,y:0,width:o[0],height:o[1],r:l},style:{stroke:a.get("dataBackgroundColor")||a.get("borderColor"),lineWidth:1,fill:"rgba(0,0,0,0)"}})),E([0,1],(function(e){var o=a.get("handleIcon");!By[o]&&o.indexOf("path://")<0&&o.indexOf("image://")<0&&(o="path://"+o);var s=Wy(o,-1,0,2,2,null,!0);s.attr({cursor:mF(this._orient),draggable:!0,drift:W(this._onDragMove,this,e),ondragend:W(this._onDragEnd,this),onmouseover:W(this._showDataInfo,this,!0),onmouseout:W(this._showDataInfo,this,!1),z2:5});var l=s.getBoundingRect(),u=a.get("handleSize");this._handleHeight=Ur(u,this._size[1]),this._handleWidth=l.width/l.height*this._handleHeight,s.setStyle(a.getModel("handleStyle").getItemStyle()),s.style.strokeNoScale=!0,s.rectHover=!0,s.ensureState("emphasis").style=a.getModel(["emphasis","handleStyle"]).getItemStyle(),Hl(s);var h=a.get("handleColor");null!=h&&(s.style.fill=h),r.add(n[e]=s);var c=a.getModel("textStyle");t.add(i[e]=new Fs({silent:!0,invisible:!0,style:nc(c,{x:0,y:0,text:"",verticalAlign:"middle",align:"center",fill:c.getTextColor(),font:c.getFont()}),z2:10}))}),this);var c=h;if(u){var p=Ur(a.get("moveHandleSize"),o[1]),d=e.moveHandle=new zs({style:a.getModel("moveHandleStyle").getItemStyle(),silent:!0,shape:{r:[0,0,2,2],y:o[1]-.5,height:p}}),f=.8*p,g=e.moveHandleIcon=Wy(a.get("moveHandleIcon"),-f/2,-f/2,f,f,"#fff",!0);g.silent=!0,g.y=o[1]+p/2-.5,d.ensureState("emphasis").style=a.getModel(["emphasis","moveHandleStyle"]).getItemStyle();var y=Math.min(o[1]/2,Math.max(p,10));(c=e.moveZone=new zs({invisible:!0,shape:{y:o[1]-y,height:p+y}})).on("mouseover",(function(){s.enterEmphasis(d)})).on("mouseout",(function(){s.leaveEmphasis(d)})),r.add(d),r.add(g),r.add(c)}c.attr({draggable:!0,cursor:mF(this._orient),drift:W(this._onDragMove,this,"all"),ondragstart:W(this._showDataInfo,this,!0),ondragend:W(this._onDragEnd,this),onmouseover:W(this._showDataInfo,this,!0),onmouseout:W(this._showDataInfo,this,!1)})},e.prototype._resetInterval=function(){var t=this._range=this.dataZoomModel.getPercentRange(),e=this._getViewExtent();this._handleEnds=[Xr(t[0],[0,100],e,!0),Xr(t[1],[0,100],e,!0)]},e.prototype._updateInterval=function(t,e){var n=this.dataZoomModel,i=this._handleEnds,r=this._getViewExtent(),o=n.findRepresentativeAxisProxy().getMinMaxSpan(),a=[0,100];Ck(e,i,r,n.get("zoomLock")?"all":t,null!=o.minSpan?Xr(o.minSpan,a,r,!0):null,null!=o.maxSpan?Xr(o.maxSpan,a,r,!0):null);var s=this._range,l=this._range=jr([Xr(i[0],r,a,!0),Xr(i[1],r,a,!0)]);return!s||s[0]!==l[0]||s[1]!==l[1]},e.prototype._updateView=function(t){var e=this._displayables,n=this._handleEnds,i=jr(n.slice()),r=this._size;E([0,1],(function(t){var i=e.handles[t],o=this._handleHeight;i.attr({scaleX:o/2,scaleY:o/2,x:n[t]+(t?-1:1),y:r[1]/2-o/2})}),this),e.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:r[1]});var o={x:i[0],width:i[1]-i[0]};e.moveHandle&&(e.moveHandle.setShape(o),e.moveZone.setShape(o),e.moveZone.getBoundingRect(),e.moveHandleIcon&&e.moveHandleIcon.attr("x",o.x+o.width/2));for(var a=e.dataShadowSegs,s=[0,i[0],i[1],r[0]],l=0;le[0]||n[1]<0||n[1]>e[1])){var i=this._handleEnds,r=(i[0]+i[1])/2,o=this._updateInterval("all",n[0]-r);this._updateView(),o&&this._dispatchZoomAction(!1)}},e.prototype._onBrushStart=function(t){var e=t.offsetX,n=t.offsetY;this._brushStart=new De(e,n),this._brushing=!0,this._brushStartTime=+new Date},e.prototype._onBrushEnd=function(t){if(this._brushing){var e=this._displayables.brushRect;if(this._brushing=!1,e){e.attr("ignore",!0);var n=e.shape;if(!(+new Date-this._brushStartTime<200&&Math.abs(n.width)<5)){var i=this._getViewExtent(),r=[0,100];this._range=jr([Xr(n.x,i,r,!0),Xr(n.x+n.width,i,r,!0)]),this._handleEnds=[n.x,n.x+n.width],this._updateView(),this._dispatchZoomAction(!1)}}}},e.prototype._onBrush=function(t){this._brushing&&(de(t.event),this._updateBrushRect(t.offsetX,t.offsetY))},e.prototype._updateBrushRect=function(t,e){var n=this._displayables,i=this.dataZoomModel,r=n.brushRect;r||(r=n.brushRect=new pF({silent:!0,style:i.getModel("brushStyle").getItemStyle()}),n.sliderGroup.add(r)),r.attr("ignore",!1);var o=this._brushStart,a=this._displayables.sliderGroup,s=a.transformCoordToLocal(t,e),l=a.transformCoordToLocal(o.x,o.y),u=this._size;s[0]=Math.max(Math.min(u[0],s[0]),0),r.setShape({x:l[0],y:0,width:s[0]-l[0],height:u[1]})},e.prototype._dispatchZoomAction=function(t){var e=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,animation:t?yF:null,start:e[0],end:e[1]})},e.prototype._findCoordRect=function(){var t,e=jE(this.dataZoomModel).infoList;if(!t&&e.length){var n=e[0].model.coordinateSystem;t=n.getRect&&n.getRect()}if(!t){var i=this.api.getWidth(),r=this.api.getHeight();t={x:.2*i,y:.2*r,width:.6*i,height:.6*r}}return t},e.type="dataZoom.slider",e}(QE);function mF(t){return"vertical"===t?"ns-resize":"ew-resize"}function xF(t){t.registerComponentModel(cF),t.registerComponentView(vF),az(t)}var _F=function(t,e,n){var i=T((bF[t]||{})[e]);return n&&Y(i)?i[i.length-1]:i},bF={color:{active:["#006edd","#e0ffff"],inactive:["rgba(0,0,0,0)"]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},wF=_D.mapVisual,SF=_D.eachVisual,MF=Y,IF=E,TF=jr,CF=Xr,DF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.stateList=["inRange","outOfRange"],n.replacableOptionKeys=["inRange","outOfRange","target","controller","color"],n.layoutMode={type:"box",ignoreSize:!0},n.dataBound=[-1/0,1/0],n.targetVisuals={},n.controllerVisuals={},n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n)},e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&wV(n,t,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},e.prototype.resetVisual=function(t){var e=this.stateList;t=W(t,this),this.controllerVisuals=bV(this.option.controller,e,t),this.targetVisuals=bV(this.option.target,e,t)},e.prototype.getItemSymbol=function(){return null},e.prototype.getTargetSeriesIndices=function(){var t=this.option.seriesIndex,e=[];return null==t||"all"===t?this.ecModel.eachSeries((function(t,n){e.push(n)})):e=bo(t),e},e.prototype.eachTargetSeries=function(t,e){E(this.getTargetSeriesIndices(),(function(n){var i=this.ecModel.getSeriesByIndex(n);i&&t.call(e,i)}),this)},e.prototype.isTargetSeries=function(t){var e=!1;return this.eachTargetSeries((function(n){n===t&&(e=!0)})),e},e.prototype.formatValueText=function(t,e,n){var i,r=this.option,o=r.precision,a=this.dataBound,s=r.formatter;n=n||["<",">"],Y(t)&&(t=t.slice(),i=!0);var l=e?t:i?[u(t[0]),u(t[1])]:u(t);return U(s)?s.replace("{value}",i?l[0]:l).replace("{value2}",i?l[1]:l):X(s)?i?s(t[0],t[1]):s(t):i?t[0]===a[0]?n[0]+" "+l[1]:t[1]===a[1]?n[1]+" "+l[0]:l[0]+" - "+l[1]:l;function u(t){return t===a[0]?"min":t===a[1]?"max":(+t).toFixed(Math.min(o,20))}},e.prototype.resetExtent=function(){var t=this.option,e=TF([t.min,t.max]);this._dataExtent=e},e.prototype.getDataDimensionIndex=function(t){var e=this.option.dimension;if(null!=e)return t.getDimensionIndex(e);for(var n=t.dimensions,i=n.length-1;i>=0;i--){var r=n[i],o=t.getDimensionInfo(r);if(!o.isCalculationCoord)return o.storeDimIndex}},e.prototype.getExtent=function(){return this._dataExtent.slice()},e.prototype.completeVisualOption=function(){var t=this.ecModel,e=this.option,n={inRange:e.inRange,outOfRange:e.outOfRange},i=e.target||(e.target={}),r=e.controller||(e.controller={});C(i,n),C(r,n);var o=this.isCategory();function a(n){MF(e.color)&&!n.inRange&&(n.inRange={color:e.color.slice().reverse()}),n.inRange=n.inRange||{color:t.get("gradientColor")}}a.call(this,i),a.call(this,r),function(t,e,n){var i=t[e],r=t[n];i&&!r&&(r=t[n]={},IF(i,(function(t,e){if(_D.isValidType(e)){var n=_F(e,"inactive",o);null!=n&&(r[e]=n,"color"!==e||r.hasOwnProperty("opacity")||r.hasOwnProperty("colorAlpha")||(r.opacity=[0,0]))}})))}.call(this,i,"inRange","outOfRange"),function(t){var e=(t.inRange||{}).symbol||(t.outOfRange||{}).symbol,n=(t.inRange||{}).symbolSize||(t.outOfRange||{}).symbolSize,i=this.get("inactiveColor"),r=this.getItemSymbol()||"roundRect";IF(this.stateList,(function(a){var s=this.itemSize,l=t[a];l||(l=t[a]={color:o?i:[i]}),null==l.symbol&&(l.symbol=e&&T(e)||(o?r:[r])),null==l.symbolSize&&(l.symbolSize=n&&T(n)||(o?s[0]:[s[0],s[0]])),l.symbol=wF(l.symbol,(function(t){return"none"===t?r:t}));var u=l.symbolSize;if(null!=u){var h=-1/0;SF(u,(function(t){t>h&&(h=t)})),l.symbolSize=wF(u,(function(t){return CF(t,[0,h],[0,s[0]],!0)}))}}),this)}.call(this,r)},e.prototype.resetItemSize=function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},e.prototype.isCategory=function(){return!!this.option.categories},e.prototype.setSelected=function(t){},e.prototype.getSelected=function(){return null},e.prototype.getValueState=function(t){return null},e.prototype.getVisualMeta=function(t){return null},e.type="visualMap",e.dependencies=["series"],e.defaultOption={show:!0,z:4,seriesIndex:"all",min:0,max:200,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",contentColor:"#5793f3",inactiveColor:"#aaa",borderWidth:0,padding:5,textGap:10,precision:0,textStyle:{color:"#333"}},e}(Rp),AF=[20,140],kF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent(),this.resetVisual((function(t){t.mappingMethod="linear",t.dataExtent=this.getExtent()})),this._resetRange()},e.prototype.resetItemSize=function(){t.prototype.resetItemSize.apply(this,arguments);var e=this.itemSize;(null==e[0]||isNaN(e[0]))&&(e[0]=AF[0]),(null==e[1]||isNaN(e[1]))&&(e[1]=AF[1])},e.prototype._resetRange=function(){var t=this.getExtent(),e=this.option.range;!e||e.auto?(t.auto=1,this.option.range=t):Y(e)&&(e[0]>e[1]&&e.reverse(),e[0]=Math.max(e[0],t[0]),e[1]=Math.min(e[1],t[1]))},e.prototype.completeVisualOption=function(){t.prototype.completeVisualOption.apply(this,arguments),E(this.stateList,(function(t){var e=this.option.controller[t].symbolSize;e&&e[0]!==e[1]&&(e[0]=e[1]/3)}),this)},e.prototype.setSelected=function(t){this.option.range=t.slice(),this._resetRange()},e.prototype.getSelected=function(){var t=this.getExtent(),e=jr((this.get("range")||[]).slice());return e[0]>t[1]&&(e[0]=t[1]),e[1]>t[1]&&(e[1]=t[1]),e[0]=n[1]||t<=e[1])?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[];return this.eachTargetSeries((function(n){var i=[],r=n.getData();r.each(this.getDataDimensionIndex(r),(function(e,n){t[0]<=e&&e<=t[1]&&i.push(n)}),this),e.push({seriesId:n.id,dataIndex:i})}),this),e},e.prototype.getVisualMeta=function(t){var e=LF(this,"outOfRange",this.getExtent()),n=LF(this,"inRange",this.option.range.slice()),i=[];function r(e,n){i.push({value:e,color:t(e,n)})}for(var o=0,a=0,s=n.length,l=e.length;at[1])break;n.push({color:this.getControllerVisual(o,"color",e),offset:r/100})}return n.push({color:this.getControllerVisual(t[1],"color",e),offset:1}),n},e.prototype._createBarPoints=function(t,e){var n=this.visualMapModel.itemSize;return[[n[0]-e[0],t[0]],[n[0],t[0]],[n[0],t[1]],[n[0]-e[1],t[1]]]},e.prototype._createBarGroup=function(t){var e=this._orient,n=this.visualMapModel.get("inverse");return new zr("horizontal"!==e||n?"horizontal"===e&&n?{scaleX:"bottom"===t?-1:1,rotation:-Math.PI/2}:"vertical"!==e||n?{scaleX:"left"===t?1:-1}:{scaleX:"left"===t?1:-1,scaleY:-1}:{scaleX:"bottom"===t?1:-1,rotation:Math.PI/2})},e.prototype._updateHandle=function(t,e){if(this._useHandle){var n=this._shapes,i=this.visualMapModel,r=n.handleThumbs,o=n.handleLabels,a=i.itemSize,s=i.getExtent();zF([0,1],(function(l){var u=r[l];u.setStyle("fill",e.handlesColor[l]),u.y=t[l];var h=EF(t[l],[0,a[1]],s,!0),c=this.getControllerVisual(h,"symbolSize");u.scaleX=u.scaleY=c/a[0],u.x=a[0]-c/2;var p=zh(n.handleLabelPoints[l],Eh(u,this.group));o[l].setStyle({x:p[0],y:p[1],text:i.formatValueText(this._dataInterval[l]),verticalAlign:"middle",align:"vertical"===this._orient?this._applyTransform("left",n.mainGroup):"center"})}),this)}},e.prototype._showIndicator=function(t,e,n,i){var r=this.visualMapModel,o=r.getExtent(),a=r.itemSize,s=[0,a[1]],l=this._shapes,u=l.indicator;if(u){u.attr("invisible",!1);var h=this.getControllerVisual(t,"color",{convertOpacityToAlpha:!0}),c=this.getControllerVisual(t,"symbolSize"),p=EF(t,o,s,!0),d=a[0]-c/2,f={x:u.x,y:u.y};u.y=p,u.x=d;var g=zh(l.indicatorLabelPoint,Eh(u,this.group)),y=l.indicatorLabel;y.attr("invisible",!1);var v=this._applyTransform("left",l.mainGroup),m="horizontal"===this._orient;y.setStyle({text:(n||"")+r.formatValueText(e),verticalAlign:m?v:"middle",align:m?"center":v});var x={x:d,y:p,style:{fill:h}},_={style:{x:g[0],y:g[1]}};if(r.ecModel.isAnimationEnabled()&&!this._firstShowIndicator){var b={duration:100,easing:"cubicInOut",additive:!0};u.x=f.x,u.y=f.y,u.animateTo(x,b),y.animateTo(_,b)}else u.attr(x),y.attr(_);this._firstShowIndicator=!1;var w=this._shapes.handleLabels;if(w)for(var S=0;Sr[1]&&(u[1]=1/0),e&&(u[0]===-1/0?this._showIndicator(l,u[1],"< ",a):u[1]===1/0?this._showIndicator(l,u[0],"> ",a):this._showIndicator(l,l,"≈ ",a));var h=this._hoverLinkDataIndices,c=[];(e||WF(n))&&(c=this._hoverLinkDataIndices=n.findTargetDataIndices(u));var p=function(t,e){var n={},i={};return r(t||[],n),r(e||[],i,n),[o(n),o(i)];function r(t,e,n){for(var i=0,r=t.length;i=0&&(r.dimension=o,i.push(r))}})),t.getData().setVisual("visualMeta",i)}}];function ZF(t,e,n,i){for(var r=e.targetVisuals[i],o=_D.prepareVisualTypes(r),a={color:Ty(t.getData(),"color")},s=0,l=o.length;s0:t.splitNumber>0)&&!t.calculable?"piecewise":"continuous"})),t.registerAction(YF,XF),E(UF,(function(e){t.registerVisual(t.PRIORITY.VISUAL.COMPONENT,e)})),t.registerPreprocessor(qF))}function QF(t){t.registerComponentModel(kF),t.registerComponentView(FF),JF(t)}var tG=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._pieceList=[],n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent();var i=this._mode=this._determineMode();this._pieceList=[],eG[this._mode].call(this,this._pieceList),this._resetSelected(e,n);var r=this.option.categories;this.resetVisual((function(t,e){"categories"===i?(t.mappingMethod="category",t.categories=T(r)):(t.dataExtent=this.getExtent(),t.mappingMethod="piecewise",t.pieceList=z(this._pieceList,(function(t){return t=T(t),"inRange"!==e&&(t.visual=null),t})))}))},e.prototype.completeVisualOption=function(){var e=this.option,n={},i=_D.listVisualTypes(),r=this.isCategory();function o(t,e,n){return t&&t[e]&&t[e].hasOwnProperty(n)}E(e.pieces,(function(t){E(i,(function(e){t.hasOwnProperty(e)&&(n[e]=1)}))})),E(n,(function(t,n){var i=!1;E(this.stateList,(function(t){i=i||o(e,t,n)||o(e.target,t,n)}),this),!i&&E(this.stateList,(function(t){(e[t]||(e[t]={}))[n]=_F(n,"inRange"===t?"active":"inactive",r)}))}),this),t.prototype.completeVisualOption.apply(this,arguments)},e.prototype._resetSelected=function(t,e){var n=this.option,i=this._pieceList,r=(e?n:t).selected||{};if(n.selected=r,E(i,(function(t,e){var n=this.getSelectedMapKey(t);r.hasOwnProperty(n)||(r[n]=!0)}),this),"single"===n.selectedMode){var o=!1;E(i,(function(t,e){var n=this.getSelectedMapKey(t);r[n]&&(o?r[n]=!1:o=!0)}),this)}},e.prototype.getItemSymbol=function(){return this.get("itemSymbol")},e.prototype.getSelectedMapKey=function(t){return"categories"===this._mode?t.value+"":t.index+""},e.prototype.getPieceList=function(){return this._pieceList},e.prototype._determineMode=function(){var t=this.option;return t.pieces&&t.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},e.prototype.setSelected=function(t){this.option.selected=T(t)},e.prototype.getValueState=function(t){var e=_D.findPieceIndex(t,this._pieceList);return null!=e&&this.option.selected[this.getSelectedMapKey(this._pieceList[e])]?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[],n=this._pieceList;return this.eachTargetSeries((function(i){var r=[],o=i.getData();o.each(this.getDataDimensionIndex(o),(function(e,i){_D.findPieceIndex(e,n)===t&&r.push(i)}),this),e.push({seriesId:i.id,dataIndex:r})}),this),e},e.prototype.getRepresentValue=function(t){var e;if(this.isCategory())e=t.value;else if(null!=t.value)e=t.value;else{var n=t.interval||[];e=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return e},e.prototype.getVisualMeta=function(t){if(!this.isCategory()){var e=[],n=["",""],i=this,r=this._pieceList.slice();if(r.length){var o=r[0].interval[0];o!==-1/0&&r.unshift({interval:[-1/0,o]}),(o=r[r.length-1].interval[1])!==1/0&&r.push({interval:[o,1/0]})}else r.push({interval:[-1/0,1/0]});var a=-1/0;return E(r,(function(t){var e=t.interval;e&&(e[0]>a&&s([a,e[0]],"outOfRange"),s(e.slice()),a=e[1])}),this),{stops:e,outerColors:n}}function s(r,o){var a=i.getRepresentValue({interval:r});o||(o=i.getValueState(a));var s=t(a,o);r[0]===-1/0?n[0]=s:r[1]===1/0?n[1]=s:e.push({value:r[0],color:s},{value:r[1],color:s})}},e.type="visualMap.piecewise",e.defaultOption=Cc(DF.defaultOption,{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieces:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0}),e}(DF),eG={splitNumber:function(t){var e=this.option,n=Math.min(e.precision,20),i=this.getExtent(),r=e.splitNumber;r=Math.max(parseInt(r,10),1),e.splitNumber=r;for(var o=(i[1]-i[0])/r;+o.toFixed(n)!==o&&n<5;)n++;e.precision=n,o=+o.toFixed(n),e.minOpen&&t.push({interval:[-1/0,i[0]],close:[0,0]});for(var a=0,s=i[0];a","≥"][e[0]]];t.text=t.text||this.formatValueText(null!=t.value?t.value:t.interval,!1,n)}),this)}};function nG(t,e){var n=t.inverse;("vertical"===t.orient?!n:n)&&e.reverse()}var iG=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.doRender=function(){var t=this.group;t.removeAll();var e=this.visualMapModel,n=e.get("textGap"),i=e.textStyleModel,r=i.getFont(),o=i.getTextColor(),a=this._getItemAlign(),s=e.itemSize,l=this._getViewData(),u=l.endsText,h=it(e.get("showLabel",!0),!u);u&&this._renderEndsText(t,u[0],s,h,a),E(l.viewPieceList,(function(i){var l=i.piece,u=new zr;u.onclick=W(this._onItemClick,this,l),this._enableHoverLink(u,i.indexInModelPieceList);var c=e.getRepresentValue(l);if(this._createItemSymbol(u,c,[0,0,s[0],s[1]]),h){var p=this.visualMapModel.getValueState(c);u.add(new Fs({style:{x:"right"===a?-n:s[0]+n,y:s[1]/2,text:l.text,verticalAlign:"middle",align:a,font:r,fill:o,opacity:"outOfRange"===p?.5:1}}))}t.add(u)}),this),u&&this._renderEndsText(t,u[1],s,h,a),Tp(e.get("orient"),t,e.get("itemGap")),this.renderBackground(t),this.positionGroup(t)},e.prototype._enableHoverLink=function(t,e){var n=this;t.on("mouseover",(function(){return i("highlight")})).on("mouseout",(function(){return i("downplay")}));var i=function(t){var i=n.visualMapModel;i.option.hoverLink&&n.api.dispatchAction({type:t,batch:NF(i.findTargetDataIndices(e),i)})}},e.prototype._getItemAlign=function(){var t=this.visualMapModel,e=t.option;if("vertical"===e.orient)return RF(t,this.api,t.itemSize);var n=e.align;return n&&"auto"!==n||(n="left"),n},e.prototype._renderEndsText=function(t,e,n,i,r){if(e){var o=new zr,a=this.visualMapModel.textStyleModel;o.add(new Fs({style:nc(a,{x:i?"right"===r?n[0]:0:n[0]/2,y:n[1]/2,verticalAlign:"middle",align:i?r:"center",text:e})})),t.add(o)}},e.prototype._getViewData=function(){var t=this.visualMapModel,e=z(t.getPieceList(),(function(t,e){return{piece:t,indexInModelPieceList:e}})),n=t.get("text"),i=t.get("orient"),r=t.get("inverse");return("horizontal"===i?r:!r)?e.reverse():n&&(n=n.slice().reverse()),{viewPieceList:e,endsText:n}},e.prototype._createItemSymbol=function(t,e,n){t.add(Wy(this.getControllerVisual(e,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(e,"color")))},e.prototype._onItemClick=function(t){var e=this.visualMapModel,n=e.option,i=n.selectedMode;if(i){var r=T(n.selected),o=e.getSelectedMapKey(t);"single"===i||!0===i?(r[o]=!0,E(r,(function(t,e){r[e]=e===o}))):r[o]=!r[o],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:r})}},e.type="visualMap.piecewise",e}(PF);function rG(t){t.registerComponentModel(tG),t.registerComponentView(iG),JF(t)}var oG={label:{enabled:!0},decal:{show:!1}},aG=Oo(),sG={};function lG(t,e){var n=t.getModel("aria");if(n.get("enabled")){var i=T(oG);C(i.label,t.getLocaleModel().get("aria"),!1),C(n.option,i,!1),function(){if(n.getModel("decal").get("show")){var e=yt();t.eachSeries((function(t){if(!t.isColorBySeries()){var n=e.get(t.type);n||(n={},e.set(t.type,n)),aG(t).scope=n}})),t.eachRawSeries((function(e){if(!t.isSeriesFiltered(e))if(X(e.enableAriaDecal))e.enableAriaDecal();else{var n=e.getData();if(e.isColorBySeries()){var i=ud(e.ecModel,e.name,sG,t.getSeriesCount()),r=n.getVisual("decal");n.setVisual("decal",u(r,i))}else{var o=e.getRawData(),a={},s=aG(e).scope;n.each((function(t){var e=n.getRawIndex(t);a[e]=t}));var l=o.count();o.each((function(t){var i=a[t],r=o.getName(t)||t+"",h=ud(e.ecModel,r,s,l),c=n.getItemVisual(i,"decal");n.setItemVisual(i,"decal",u(c,h))}))}}function u(t,e){var n=t?A(A({},e),t):e;return n.dirty=!0,n}}))}}(),function(){var i=t.getLocaleModel().get("aria"),o=n.getModel("label");if(o.option=k(o.option,i),!o.get("enabled"))return;var a=e.getZr().dom;if(o.get("description"))return void a.setAttribute("aria-label",o.get("description"));var s,l=t.getSeriesCount(),u=o.get(["data","maxCount"])||10,h=o.get(["series","maxCount"])||10,c=Math.min(l,h);if(l<1)return;var p=function(){var e=t.get("title");e&&e.length&&(e=e[0]);return e&&e.text}();s=p?r(o.get(["general","withTitle"]),{title:p}):o.get(["general","withoutTitle"]);var d=[];s+=r(l>1?o.get(["series","multiple","prefix"]):o.get(["series","single","prefix"]),{seriesCount:l}),t.eachSeries((function(e,n){if(n1?o.get(["series","multiple",a]):o.get(["series","single",a]),{seriesId:e.seriesIndex,seriesName:e.get("name"),seriesType:(x=e.subType,t.getLocaleModel().get(["series","typeNames"])[x]||"自定义图")});var s=e.getData();if(s.count()>u)i+=r(o.get(["data","partialData"]),{displayCnt:u});else i+=o.get(["data","allData"]);for(var h=o.get(["data","separator","middle"]),p=o.get(["data","separator","end"]),f=[],g=0;g":"gt",">=":"gte","=":"eq","!=":"ne","<>":"ne"},cG=function(){function t(t){if(null==(this._condVal=U(t)?new RegExp(t):et(t)?t:null)){var e="";0,vo(e)}}return t.prototype.evaluate=function(t){var e=typeof t;return U(e)?this._condVal.test(t):!!j(e)&&this._condVal.test(t+"")},t}(),pG=function(){function t(){}return t.prototype.evaluate=function(){return this.value},t}(),dG=function(){function t(){}return t.prototype.evaluate=function(){for(var t=this.children,e=0;e2&&l.push(e),e=[t,n]}function f(t,n,i,r){TG(t,i)&&TG(n,r)||e.push(t,n,i,r,i,r)}function g(t,n,i,r,o,a){var s=Math.abs(n-t),l=4*Math.tan(s/4)/3,u=nM:C2&&l.push(e),l}function DG(t,e,n,i,r,o,a,s,l,u){if(TG(t,n)&&TG(e,i)&&TG(r,a)&&TG(o,s))l.push(a,s);else{var h=2/u,c=h*h,p=a-t,d=s-e,f=Math.sqrt(p*p+d*d);p/=f,d/=f;var g=n-t,y=i-e,v=r-a,m=o-s,x=g*g+y*y,_=v*v+m*m;if(x=0&&_-w*w=0)l.push(a,s);else{var S=[],M=[];wn(t,n,r,a,.5,S),wn(e,i,o,s,.5,M),DG(S[0],M[0],S[1],M[1],S[2],M[2],S[3],M[3],l,u),DG(S[4],M[4],S[5],M[5],S[6],M[6],S[7],M[7],l,u)}}}}function AG(t,e,n){var i=t[e],r=t[1-e],o=Math.abs(i/r),a=Math.ceil(Math.sqrt(o*n)),s=Math.floor(n/a);0===s&&(s=1,a=n);for(var l=[],u=0;u0)for(u=0;uMath.abs(u),c=AG([l,u],h?0:1,e),p=(h?s:u)/c.length,d=0;d1?null:new De(d*l+t,d*u+e)}function OG(t,e,n){var i=new De;De.sub(i,n,e),i.normalize();var r=new De;return De.sub(r,t,e),r.dot(i)}function RG(t,e){var n=t[t.length-1];n&&n[0]===e[0]&&n[1]===e[1]||t.push(e)}function NG(t){var e=t.points,n=[],i=[];Ra(e,n,i);var r=new ze(n[0],n[1],i[0]-n[0],i[1]-n[1]),o=r.width,a=r.height,s=r.x,l=r.y,u=new De,h=new De;return o>a?(u.x=h.x=s+o/2,u.y=l,h.y=l+a):(u.y=h.y=l+a/2,u.x=s,h.x=s+o),function(t,e,n){for(var i=t.length,r=[],o=0;or,a=AG([i,r],o?0:1,e),s=o?"width":"height",l=o?"height":"width",u=o?"x":"y",h=o?"y":"x",c=t[s]/a.length,p=0;p0)for(var b=i/n,w=-i/2;w<=i/2;w+=b){var S=Math.sin(w),M=Math.cos(w),I=0;for(x=0;x0;l/=2){var u=0,h=0;(t&l)>0&&(u=1),(e&l)>0&&(h=1),s+=l*l*(3*u^h),0===h&&(1===u&&(t=l-1-t,e=l-1-e),a=t,t=e,e=a)}return s}function JG(t){var e=1/0,n=1/0,i=-1/0,r=-1/0,o=z(t,(function(t){var o=t.getBoundingRect(),a=t.getComputedTransform(),s=o.x+o.width/2+(a?a[4]:0),l=o.y+o.height/2+(a?a[5]:0);return e=Math.min(s,e),n=Math.min(l,n),i=Math.max(s,i),r=Math.max(l,r),[s,l]}));return z(o,(function(o,a){return{cp:o,z:$G(o[0],o[1],e,n,i,r),path:t[a]}})).sort((function(t,e){return t.z-e.z})).map((function(t){return t.path}))}function QG(t){return VG(t.path,t.count)}function tW(t){return Y(t[0])}function eW(t,e){for(var n=[],i=t.length,r=0;r=0;r--)if(!n[r].many.length){var l=n[s].many;if(l.length<=1){if(!s)return n;s=0}o=l.length;var u=Math.ceil(o/2);n[r].many=l.slice(u,o),n[s].many=l.slice(0,u),s++}return n}var nW={clone:function(t){for(var e=[],n=1-Math.pow(1-t.path.style.opacity,1/t.count),i=0;i0){var s,l,u=i.getModel("universalTransition").get("delay"),h=Object.assign({setToFinal:!0},a);tW(t)&&(s=t,l=e),tW(e)&&(s=e,l=t);for(var c=s?s===t:t.length>e.length,p=s?eW(l,s):eW(c?e:t,[c?t:e]),d=0,f=0;f1e4))for(var i=n.getIndices(),r=function(t){for(var e=t.dimensions,n=0;n0&&i.group.traverse((function(t){t instanceof Is&&!t.animators.length&&t.animateFrom({style:{opacity:0}},r)}))}))}function pW(t){var e=t.getModel("universalTransition").get("seriesKey");return e||t.id}function dW(t){return Y(t)?t.sort().join(","):t}function fW(t){if(t.hostModel)return t.hostModel.getModel("universalTransition").get("divideShape")}function gW(t,e){for(var n=0;n=0&&r.push({dataGroupId:e.oldDataGroupIds[n],data:e.oldData[n],divide:fW(e.oldData[n]),dim:t.dimension})})),E(bo(t.to),(function(t){var i=gW(n.updatedSeries,t);if(i>=0){var r=n.updatedSeries[i].getData();o.push({dataGroupId:e.oldDataGroupIds[i],data:r,divide:fW(r),dim:t.dimension})}})),r.length>0&&o.length>0&&cW(r,o,i)}(t,i,n,e)}));else{var o=function(t,e){var n=yt(),i=yt(),r=yt();return E(t.oldSeries,(function(e,n){var o=t.oldDataGroupIds[n],a=t.oldData[n],s=pW(e),l=dW(s);i.set(l,{dataGroupId:o,data:a}),Y(s)&&E(s,(function(t){r.set(t,{key:l,dataGroupId:o,data:a})}))})),E(e.updatedSeries,(function(t){if(t.isUniversalTransitionEnabled()&&t.isAnimationEnabled()){var e=t.get("dataGroupId"),o=t.getData(),a=pW(t),s=dW(a),l=i.get(s);if(l)n.set(s,{oldSeries:[{dataGroupId:l.dataGroupId,divide:fW(l.data),data:l.data}],newSeries:[{dataGroupId:e,divide:fW(o),data:o}]});else if(Y(a)){var u=[];E(a,(function(t){var e=i.get(t);e.data&&u.push({dataGroupId:e.dataGroupId,divide:fW(e.data),data:e.data})})),u.length&&n.set(s,{oldSeries:u,newSeries:[{dataGroupId:e,data:o,divide:fW(o)}]})}else{var h=r.get(a);if(h){var c=n.get(h.key);c||(c={oldSeries:[{dataGroupId:h.dataGroupId,data:h.data,divide:fW(h.data)}],newSeries:[]},n.set(h.key,c)),c.newSeries.push({dataGroupId:e,data:o,divide:fW(o)})}}}})),n}(i,n);E(o.keys(),(function(t){var n=o.get(t);cW(n.oldSeries,n.newSeries,e)}))}E(n.updatedSeries,(function(t){t[vg]&&(t[vg]=!1)}))}for(var a=t.getSeries(),s=i.oldSeries=[],l=i.oldDataGroupIds=[],u=i.oldData=[],h=0;h + + + + + GitHub仓库Star数量对比 + + + + +
+
+ 正在加载数据... +
+ +
+
+
+
+

正在加载数据...

+
+
+
+ + + + + + + + diff --git a/sa-token-doc/a/star-guide/index.html b/sa-token-doc/a/star-guide/index.html new file mode 100644 index 000000000..f43d01e57 --- /dev/null +++ b/sa-token-doc/a/star-guide/index.html @@ -0,0 +1,100 @@ + + + + + 点击 Star 支持我们 + + + +
+ img 1 + img 2 + + +
+ + + + + diff --git a/sa-token-doc/a/star-guide/star-guide.js b/sa-token-doc/a/star-guide/star-guide.js new file mode 100644 index 000000000..331875e23 --- /dev/null +++ b/sa-token-doc/a/star-guide/star-guide.js @@ -0,0 +1,78 @@ + +// 判断当前是否已弹出 +function isShowStarGuide() { + // 非PC端不检查 + if(document.body.offsetWidth < 800) { + console.log('small screen ... isShowStarGuide '); + return; + } + + // 检查成功后,多少天不再检查 + const alertAllowDisparity = 1000 * 60 * 60 * 24 * 30; // 30天 + // const allowDisparity = 1000 * 10; + + // 判断是否近期已经判断过了 + const SAVE_KEY = 'isShowStarGuide'; + try{ + const showAlert = localStorage[SAVE_KEY]; + if(showAlert) { + // 记录 star 的时间,和当前时间的差距 + const disparity = new Date().getTime() - parseInt(showAlert); + + // 差距小于一月,不再检测,大于一月,再检测一下 + if(disparity < alertAllowDisparity) { + console.log('checked ... wj '); + return; + } + } + }catch(e){ + console.error(e); + } + + // 本次打开页面的内存内已经弹出了的话,也不再弹了 + if(window.isYtcXsjfkasjda3232) { + return; + } + window.isYtcXsjfkasjda3232 = true; + + // 弹出弹框,邀请填写 + const tipStr = ` +
+
+ +
+

+ Sa-Token 采用 Apache-2.0 开源协议,承诺框架本身与在线文档永久免费开放。 +

+

+ 如果 Sa-Token 帮助到了你,希望你可以为项目点个 star ⭐,这对我们非常重要,感谢支持! +

+
+ `; + + const index = layer.confirm(tipStr, { + title: '支持项目', + btn: ['确定'], + area: '570px', + offset: '10%', + cancel: function() { + localStorage[SAVE_KEY] = new Date().getTime(); + } + }, + // 点击确定 + function(index) { + layer.close(index); + localStorage[SAVE_KEY] = new Date().getTime(); + open('https://gitee.com/dromara/sa-token'); + // open('https://github.com/dromara/sa-token'); + // open('https://atomgit.com/dromara/sa-token'); + + }, + // 点击取消 + function(){ + localStorage[SAVE_KEY] = new Date().getTime(); + } + ); +} +isShowStarGuide(); diff --git a/sa-token-doc/api/sa-session.md b/sa-token-doc/api/sa-session.md new file mode 100644 index 000000000..1817f53a7 --- /dev/null +++ b/sa-token-doc/api/sa-session.md @@ -0,0 +1,89 @@ +# SaSession-会话对象 + +SaSession-会话对象,专业数据缓存组件。 + +--- + +### 1、常量 +``` java +SaSession.USER= "USER"; // 在 Session 上存储用户对象时建议使用的key +SaSession.ROLE_LIST = "ROLE_LIST"; // 在 Session 上存储角色时建议使用的key +SaSession.PERMISSION_LIST = "PERMISSION_LIST"; // 在 Session 上存储权限时建议使用的key +``` + + +### 2、构建相关 +``` java +session.getId(); // 获取此 Session 的 id +session.setId(id); // 写入此 Session 的 id +session.getCreateTime(); // 返回当前会话创建时间(时间戳) +session.setCreateTime(createTime); // 写入此 Session 的创建时间(时间戳) +session.getType(); // 获取此 Session 的类型 +session.setType(type); // 写入此 Session 的类型 +session.getLoginType(); // 获取此 Session 的账号类型 +session.setLoginType(loginType); // 写入此 Session 的账号类型 +session.getLoginId(); // 获取此 Session 的账号 id +session.setLoginId(loginId); // 写入此 Session 的账号 id +session.getToken(); // 获取此 Session 的 token 值 +session.setToken(token); // 写入此 Session 的 token 值 +``` + + +### 3、SaTerminalInfo 相关 +``` java +session.setTerminalList(terminalList); // 写入登录终端信息列表 +session.getTerminalList(); // 获取登录终端信息列表 +session.terminalListCopy(); // 获取 登录终端信息列表 (拷贝副本) +session.getTerminalListByDeviceType(deviceType); // 获取 登录终端信息列表 (拷贝副本),根据 deviceType 筛选 +session.getTokenValueListByDeviceType(deviceType); // 获取指定设备类型的 token 值列表 +session.getTerminal(tokenValue); // 查找一个终端信息,根据 tokenValue +session.addTerminal(terminal); // 添加一个终端信息 +session.removeTerminal(tokenValue); // 移除一个终端信息 +session.forEachTerminalList((session, terminal) -> {}); // 遍历登录终端信息列表 +session.getHistoryTerminalCount(); // 获取历史终端数量 +session.setHistoryTerminalCount(count); // 设置历史终端数量 +session.isTrustDeviceId("xxxxxxxxxxxxxxxxxxxxxxxx"); // 判断指定设备 id 是否为可信任设备 +``` + + +### 4、一些操作 +``` java +session.update(); // 更新Session(从持久库更新刷新一下) +session.logout(); // 注销Session (从持久库删除) +session.logoutByTerminalCountToZero(); // 当 Session 上的 SaTerminalInfo 数量为零时,注销会话 +session.timeout(); // 获取此Session的剩余存活时间 (单位: 秒) +session.updateTimeout(timeout); // 修改此Session的剩余存活时间 +session.updateMinTimeout(minTimeout); // 修改此Session的最小剩余存活时间 (只有在 Session 的过期时间低于指定的 minTimeout 时才会进行修改) +session.updateMaxTimeout(maxTimeout); // 修改此Session的最大剩余存活时间 (只有在 Session 的过期时间高于指定的 maxTimeout 时才会进行修改) +``` + + +### 5、存取值 +``` java +session.get(key); // 取值 +session.get(key, defaultValue); // 取值 (指定默认值) +session.get(key, () -> {}); // 取值 (如果值为 null,则执行 fun 函数获取值,并把函数返回值写入缓存) +session.getString(key); // 取值 (转String类型) +session.getInt(key); // 取值 (转int类型) +session.getLong(key); // 取值 (转long类型) +session.getDouble(key); // 取值 (转double类型) +session.getFloat(key); // 取值 (转float类型) +session.getModel(key, clazz); // 取值 (指定转换类型) +session.getModel(key, clazz, defaultValue); // 取值 (指定转换类型, 并指定值为Null时返回的默认值) +session.has(key); // 是否含有某个key +session.set(key, value); // 写值 +session.setByNull(key, value); // 写值 (只有在此 key 原本无值的情况下才会写入) +session.delete(key); // 删值 +session.keys(); // 返回当前Session的所有key +session.clear(); // 清空所有值 +session.getDataMap(); // 获取数据挂载集合(如果更新map里的值,请调用session.update()方法避免产生脏数据 ) +session.setDataMap(dataMap); // 写入数据挂载集合 +session.refreshDataMap(dataMap); // 写入数据集合 (不改变底层对象,只将此dataMap所有数据进行替换) +``` + + + + + + + diff --git a/sa-token-doc/api/sa-strategy.md b/sa-token-doc/api/sa-strategy.md new file mode 100644 index 000000000..951686a57 --- /dev/null +++ b/sa-token-doc/api/sa-strategy.md @@ -0,0 +1,96 @@ +# SaStrategy-全局策略 + +SaStrategy-全局策略,核心逻辑的代理封装。 + +--- + +### 核心策略 + +``` java +/** + * 创建 Token 的策略 + *

参数 [账号id, 账号类型] + */ +public BiFunction createToken = (loginId, loginType) -> { + // 默认,还是uuid + return "xxxxx-xxxxx-xxxxx-xxxxx"; +}; + +/** + * 创建 Session 的策略 + *

参数 [SessionId] + */ +public Function createSession = (sessionId) -> { + return new SaSession(sessionId); +}; + +/** + * 反序列化 SaSession 时默认指定的类型 + */ +public Class sessionClassType = SaSession.class; + +/** + * 判断:集合中是否包含指定元素(模糊匹配) + *

参数 [集合, 元素] + */ +public BiFunction, String, Boolean> hasElement = (list, element) -> { + return false; +}; + + +/** + * 生成唯一式 token 的算法 + *

参数:元素名称, 最大尝试次数, 创建 token 函数, 检查 token 函数

+ */ +public SaGenerateUniqueTokenFunction generateUniqueToken = (elementName, maxTryTimes, createTokenFunction, checkTokenFunction) -> { + // ... + return "xxxxxx"; +}; + +/** + * 是否自动续期,每次续期前都会执行,可以加入动态判断逻辑 + *

参数 当前 stpLogic 实例对象 + *

返回 true 自动续期 false 不自动续期 + */ +public Function autoRenew = (stpLogic) -> { + return stpLogic.getConfigOrGlobal().getAutoRenew(); +}; + +/** + * 创建 StpLogic 的算法 + *

参数:账号体系标识

+ *

返回:创建好的 StpLogic 对象

+ */ +public SaCreateStpLogicFunction createStpLogic = (loginType) -> { + return new StpLogic(loginType); +}; + +/** + * 路由匹配策略 + *

参数:pattern, path

+ *

返回:是否匹配

+ */ +public SaRouteMatchFunction routeMatcher = (pattern, path) -> { + return true; +}; + +/** + * CORS 策略处理函数 + *

参数:请求包装对象, 响应包装对象, 数据读写对象

+ */ +public SaCorsHandleFunction corsHandle = (req, res, sto) -> { + +}; +``` + + +### 重写策略(set 连缀风格) + +``` java +SaStrategy.instance.setCreateToken(createToken); // 重写创建 Token 的策略 +SaStrategy.instance.setCreateSession(createSession); // 重写创建 Session 的策略 +SaStrategy.instance.setHasElement(hasElement); // 重写集合模糊匹配策略 +SaStrategy.instance.setGenerateUniqueToken(generateUniqueToken); // 重写生成唯一 token 的策略 +SaStrategy.instance.setCreateStpLogic(createStpLogic); // 重写创建 StpLogic 的策略 +SaStrategy.instance.setAutoRenew(autoRenew); // 重写是否自动续期策略 +``` diff --git a/sa-token-doc/api/sa-token-dao.md b/sa-token-doc/api/sa-token-dao.md new file mode 100644 index 000000000..7f573cb7d --- /dev/null +++ b/sa-token-doc/api/sa-token-dao.md @@ -0,0 +1,62 @@ +# SaTokenDao-数据持久接口 + +SaTokenDao 是数据持久层接口,负责所有会话数据的底层写入和读取。 + +--- + +### 1、常量 +``` java +SaTokenDao.NEVER_EXPIRE = -1; // 常量,表示一个key永不过期 (在一个key被标注为永远不过期时返回此值) +SaTokenDao.NOT_VALUE_EXPIRE = -2; // 常量,表示系统中不存在这个缓存 (在对不存在的key获取剩余存活时间时返回此值) +``` + + +### 2、字符串读写 +``` java +dao.get(key); // 获取Value,如无返空 +dao.set(key, value, timeout); // 写入Value,并设定存活时间 (单位: 秒) +dao.update(key, value); // 更新Value (过期时间不变) +dao.delete(key); // 删除Value +dao.getTimeout(key); // 获取Value的剩余存活时间 (单位: 秒) +dao.updateTimeout(key, timeout); // 修改Value的剩余存活时间 (单位: 秒) +``` + + +### 3、对象读写 +``` java +dao.getObject(key); // 获取Object,如无返空 +dao.getObject(key, classType); // 获取Object,并指定转换类型 +dao.setObject(key, value, timeout); // 写入Object,并设定存活时间 (单位: 秒) +dao.updateObject(key, value); // 更新Object (过期时间不变) +dao.deleteObject(key); // 删除Object +dao.getObjectTimeout(key); // 获取Object的剩余存活时间 (单位: 秒) +dao.updateObjectTimeout(key, timeout); // 修改Object的剩余存活时间 (单位: 秒) +``` + + +### 4、Session读写 +``` java +dao.getSession(sessionId); // 获取Session,如无返空 +dao.setSession(session, timeout); // 写入Session,并设定存活时间 (单位: 秒) +dao.updateSession(session); // 更新Session (过期时间不变) +dao.deleteSession(sessionId); // 删除Session +dao.getSessionTimeout(sessionId); // 获取Session的剩余存活时间 (单位: 秒) +dao.updateSessionTimeout(sessionId, timeout); // 修改Session的剩余存活时间 (单位: 秒) +``` + + +### 5、会话管理 +``` java +dao.searchData(prefix, keyword, start, size, sortType); // 搜索数据 +``` + + + + + + + + + + + diff --git a/sa-token-doc/api/stp-util.md b/sa-token-doc/api/stp-util.md new file mode 100644 index 000000000..496715177 --- /dev/null +++ b/sa-token-doc/api/stp-util.md @@ -0,0 +1,235 @@ +# StpUtil - 鉴权工具类 + +StpUtil 是 Sa-Token 整体功能的核心,大多数功能均由此工具类提供。 + +--- + +### 1、常规操作 +``` java +StpUtil.getStpLogic(); // 获取底层 StpLogic 对象。 +StpUtil.setStpLogic(newStpLogic); // 安全的重置底层 StpLogic 引用。 +StpUtil.getLoginType(); // 获取账号类型 (例如:login、user、admin、teacher、student等等)。 +StpUtil.getTokenName(); // 获取 Token 的名称 +StpUtil.getTokenValue(); // 获取本次请求前端提交的 Token。 +StpUtil.getTokenValueNotCut(); // 获取本次请求前端提交的 Token (不裁剪前缀) 。 +StpUtil.setTokenValue(tokenValue); // 在当前会话中写入 Token 值。 +StpUtil.setTokenValue(tokenValue, timeout); // 在当前会话中写入 Token 值,并指定 Cookie 有效期。 +StpUtil.setTokenValue(tokenValue, loginParameter); // 在当前会话中写入 Token 值,并指定登录参数。 +StpUtil.setTokenValueToStorage(tokenValue); // 将 Token 写入当前请求的 Storage 存储器。 +StpUtil.getTokenInfo(); // 获取当前 Token 的详细参数。 +StpUtil.createSaLoginParameter(); // 根据当前配置对象创建一个 SaLoginParameter 对象 +``` + + +### 2、登录相关 +``` java +StpUtil.login(10001); // 会话登录 +StpUtil.login(10001, "APP"); // 会话登录,并指定设备类型 +StpUtil.login(10001, true); // 会话登录,并指定是否 [记住我] +StpUtil.login(10001, 86400); // 会话登录,并指定此次 token 有效期(单位:秒) +StpUtil.login(10001, loginParameter); // 会话登录,并指定所有登录参数Model +StpUtil.createLoginSession(10001); // 创建指定账号id的登录会话,此方法不会将 Token 注入到上下文 +StpUtil.createLoginSession(10001, loginParameter); // 创建指定账号id的登录会话,此方法不会将 Token 注入到上下文 +StpUtil.getOrCreateLoginSession(10001); // 获取指定账号的登录会话,若不存在则创建并返回 +``` + +更多 SaLoginParameter 登录参数的用法与示例,请参考 [登录参数示例章节](/up/login-parameter)。 + + +### 3、注销相关 +``` java +StpUtil.logout(); // 会话注销 +StpUtil.logout(logoutParameter); // 会话注销,根据注销参数 +StpUtil.logout(10001); // 会话注销,根据账号id +StpUtil.logout(10001, "PC"); // 会话注销,根据账号id 和 设备类型 +StpUtil.logout(10001, logoutParameter); // 会话注销,根据账号id 和 注销参数 +StpUtil.logoutByTokenValue(token); // 指定 Token 强制注销 +StpUtil.logoutByTokenValue(token, logoutParameter); // 指定 Token 强制注销,带注销参数 +StpUtil.kickout(10001); // 踢人下线,根据账号id +StpUtil.kickout(10001, "PC"); // 踢人下线,根据账号id 和 设备类型 +StpUtil.kickout(10001, logoutParameter); // 踢人下线,根据账号id 和 注销参数 +StpUtil.kickoutByTokenValue(token); // 踢人下线,根据token +StpUtil.kickoutByTokenValue(token, logoutParameter); // 踢人下线,根据token,带注销参数 +StpUtil.replacedByTokenValue(token); // 顶人下线,根据token +StpUtil.replacedByTokenValue(token, logoutParameter); // 顶人下线,根据token,带注销参数 +StpUtil.replaced(10001); // 顶人下线,根据账号id +StpUtil.replaced(10001, "PC"); // 顶人下线,根据账号id 和 设备类型 +StpUtil.replaced(10001, logoutParameter); // 顶人下线,根据账号id 和 注销参数 +``` + +更多 SaLogoutParameter 注销参数的用法与示例,请参考 [注销参数示例章节](/up/login-parameter?id=_2、注销参数)。 + + +### 4、会话查询 +``` java +StpUtil.isLogin(); // 当前会话是否已经登录 +StpUtil.isLogin(10001); // 判断指定账号是否已登录 +StpUtil.checkLogin(); // 检验当前会话是否已经登录,如未登录,则抛出异常 +StpUtil.getLoginId(); // 获取当前会话账号id, 如果未登录,则抛出异常 +StpUtil.getLoginId(defaultValue); // 获取当前会话账号id, 如果未登录,则返回默认值 +StpUtil.getLoginIdDefaultNull(); // 获取当前会话账号id, 如果未登录,则返回null +StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转换为String类型 +StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转换为int类型 +StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转换为long类型 +StpUtil.getLoginIdByToken(token); // 获取指定 Token 对应的账号 id,若 token 无效/被踢/被顶/被冻结则返回 null +StpUtil.getLoginIdByTokenNotThinkFreeze(token); // 获取指定Token对应的账号id(不考虑冻结状态),如果未登录,则返回 null +StpUtil.getExtra(key); // 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) +StpUtil.getExtra(token, key); // 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) +``` + + +### 5、Session 相关 +``` java +// Account-Session 相关 +StpUtil.getSession(); // 获取当前会话的Session,如果Session尚未创建,则新建并返回 +StpUtil.getSession(true); // 获取当前会话的Session, 如果Session尚未创建,isCreate=是否新建并返回 +StpUtil.getSessionByLoginId(10001); // 获取指定账号id的Session,如果Session尚未创建,则新建并返回 +StpUtil.getSessionByLoginId(10001, true); // 获取指定账号id的Session, 如果Session尚未创建,isCreate=是否新建并返回 + +// Token-Session 相关 +StpUtil.getTokenSession(); // 获取当前 Token-Session,如果 Session 尚未创建,则新建并返回 +StpUtil.getTokenSessionByToken(token); // 获取指定Token-Session,如果Session尚未创建,则新建并返回 +StpUtil.getAnonTokenSession(); // 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) + +// 其它 +StpUtil.getSessionBySessionId("xxxx-xxxx-xxxx"); // 获取指定 sessionId 的 Session,若不存在则返回 null +StpUtil.isTrustDeviceId(123456, "xxxxxxxxxxxxxxxxxxxxxxxx"); // 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 +``` + + +### 6、Token有效期相关 +``` java +// Token 最低活跃频率 +StpUtil.getTokenActiveTimeout(); // 获取当前 token 距离被冻结还剩多少时间 (单位: 秒) +StpUtil.getTokenLastActiveTime(); // 获取当前 token 最后活跃时间 +StpUtil.checkActiveTimeout(); // 检查当前token 是否已经被冻结,如果是则抛出异常 +StpUtil.updateLastActiveToNow(); // 续签当前token:(将 [最后操作时间] 更新为当前时间戳) + +// Token 有效期 +StpUtil.getTokenTimeout(); // 获取当前登录者的 token 剩余有效时间 (单位: 秒) +StpUtil.getTokenTimeout(token); // 获取指定 token 的剩余有效时间 (单位: 秒) +StpUtil.getSessionTimeout(); // 获取当前登录者的 Account-Session 剩余有效时间 (单位: 秒) +StpUtil.getTokenSessionTimeout(); // 获取当前 Token-Session 剩余有效时间 (单位: 秒) +StpUtil.renewTimeout(timeout); // 对当前 Token 的 timeout 值进行续期 +StpUtil.renewTimeout(token, timeout); // 对指定 Token 的 timeout 值进行续期 +``` + + +### 7、角色认证 +``` java +StpUtil.getRoleList(); // 获取:当前账号的角色集合 +StpUtil.getRoleList(10001); // 获取:指定账号的角色集合 +StpUtil.hasRole(role); // 判断:当前账号是否拥有指定角色, 返回true或false +StpUtil.hasRole(loginId, role); // 判断:指定账号是否含有指定角色标识, 返回true或false +StpUtil.hasRoleAnd(...roleArray); // 判断:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] +StpUtil.hasRoleOr(...roleArray); // 判断:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] +StpUtil.checkRole(role); // 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException +StpUtil.checkRoleAnd(...roleArray); // 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] +StpUtil.checkRoleOr(...roleArray); // 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] +``` + + +### 8、权限认证 +``` java +StpUtil.getPermissionList(); // 获取:当前账号的权限集合 +StpUtil.getPermissionList(10001); // 获取:指定账号的权限集合 +StpUtil.hasPermission(permission); // 判断:当前账号是否拥有指定权限, 返回true或false +StpUtil.hasPermission(loginId, permission); // 判断:指定账号是否含有指定权限标识, 返回true或false +StpUtil.hasPermissionAnd(...permissionArray); // 判断:当前账号是否含有指定权限标识 [指定多个,必须全部验证通过] +StpUtil.hasPermissionOr(...permissionArray); // 判断:当前账号是否含有指定权限标识 [指定多个,只要其一验证通过即可] +StpUtil.checkPermission(permission); // 校验:当前账号是否含有指定权限标识, 如果验证未通过,则抛出异常: NotPermissionException +StpUtil.checkPermissionAnd(...permissionArray); // 校验:当前账号是否含有指定权限标识 [指定多个,必须全部验证通过] +StpUtil.checkPermissionOr(...permissionArray); // 校验:当前账号是否含有指定权限标识 [指定多个,只要其一验证通过即可] +``` + + +### 9、id 反查 Token +``` java +StpUtil.getTokenValueByLoginId(10001); // 获取指定账号id的tokenValue +StpUtil.getTokenValueByLoginId(10001, "PC"); // 获取指定账号id指定设备类型端的tokenValue +StpUtil.getTokenValueListByLoginId(10001); // 获取指定账号id的tokenValue集合 +StpUtil.getTokenValueListByLoginId(10001, "APP"); // 获取指定账号id指定设备类型端的tokenValue 集合 +StpUtil.getTerminalListByLoginId(10001); // 获取指定账号 id 已登录设备信息集合 +StpUtil.getTerminalListByLoginId(10001, "PC"); // 获取指定账号 id 指定设备类型端的已登录设备信息集合 +StpUtil.forEachTerminalList(10001, (session, terminal) -> {}); // 遍历指定账号的已登录设备列表 +StpUtil.getTerminalInfo(); // 获取当前会话的终端信息 +StpUtil.getTerminalInfoByToken(token); // 获取指定 token 的终端信息 +StpUtil.getLoginDeviceId(); // 返回当前会话的登录设备 id +StpUtil.getLoginDeviceIdByToken(token); // 返回指定 token 的登录设备 id +StpUtil.getLoginDeviceType(); // 返回当前会话的登录设备类型 +StpUtil.getLoginDeviceTypeByToken(token); // 返回指定 token 的登录设备类型 +``` + + +### 10、会话管理 +``` java +StpUtil.searchTokenValue(keyword, start, size, sortType); // 根据条件查询Token +StpUtil.searchSessionId(keyword, start, size, sortType); // 根据条件查询SessionId +StpUtil.searchTokenSessionId(keyword, start, size, sortType); // 根据条件查询Token专属Session的Id +``` +详细可参考:[会话治理](/up/search-session) + + +### 11、账号封禁 +``` java +StpUtil.disable(10001, 1200); // 封禁:指定账号 指定时间(单位s) +StpUtil.isDisable(10001); // 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) +StpUtil.checkDisable(10001); // 校验:指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException` +StpUtil.getDisableTime(10001); // 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) +StpUtil.untieDisable(loginId); // 解封:指定账号 +``` + + +### 12、分类封禁 (version >= 1.31.0) +``` java +StpUtil.disable(10001, "<业务标识>", 86400); // 封禁:指定账号的指定服务 指定时间(单位s) +StpUtil.isDisable(10001, "<业务标识>"); // 判断:指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁) +StpUtil.checkDisable(10001, "<业务标识>"); // 校验:指定账号的指定服务 是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`(支持传入多个业务标识) +StpUtil.getDisableTime(10001, "<业务标识>"); // 获取:指定账号的指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) +StpUtil.untieDisable(loginId, "<业务标识>"); // 解封:指定账号的指定服务(支持传入多个业务标识) +``` + + +### 13、阶梯封禁 (version >= 1.31.0) +``` java +StpUtil.disableLevel(10001, "comment", 3, 10000); // 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间 +StpUtil.disableLevel(10001, 3, 10000); // 阶梯封禁(无业务标识,使用默认服务),参数:封禁账号、封禁级别、封禁时间 +StpUtil.getDisableLevel(10001, "comment"); // 获取:指定账号的指定服务 封禁的级别 (如果此账号未被封禁则返回 -2) +StpUtil.getDisableLevel(10001); // 获取:指定账号的封禁等级(无业务标识,未封禁返回 -2) +StpUtil.isDisableLevel(10001, "comment", 3); // 判断:指定账号的指定服务 是否已被封禁到指定级别,返回 true 或 false +StpUtil.isDisableLevel(10001, 3); // 判断:指定账号是否被封禁到指定级别(无业务标识) +StpUtil.checkDisableLevel(10001, "comment", 2); // 校验:指定账号的指定服务 是否已被封禁到指定级别(例如 comment服务 已被3级封禁,这里校验是否达到2级),如果已达到此级别,则抛出异常 +StpUtil.checkDisableLevel(10001, 2); // 校验:指定账号是否被封禁到指定级别(无业务标识) +``` + + +### 14、身份切换 +``` java +StpUtil.switchTo(10044); // 临时切换身份为指定账号id +StpUtil.endSwitch(); // 结束临时切换身份 +StpUtil.isSwitch(); // 当前是否正处于[身份临时切换]中 +StpUtil.switchTo(10044, () -> {}); // 在一个代码段里方法内,临时切换身份为指定账号id +``` + + +### 15、二级认证 +``` java +StpUtil.openSafe(safeTime); // 在当前会话 开启二级认证 +StpUtil.isSafe(); // 当前会话 是否处于二级认证时间内 +StpUtil.checkSafe(); // 检查当前会话是否已通过二级认证,如未通过则抛出异常 +StpUtil.getSafeTime(); // 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) +StpUtil.closeSafe(); // 在当前会话 结束二级认证 +``` + + +### 16、带有业务标识的二级认证 +``` java +StpUtil.openSafe("<业务标识>", safeTime); // 在当前会话 指定业务标识开启二级认证 +StpUtil.isSafe("<业务标识>"); // 当前会话 指定业务标识是否处于二级认证时间内 +StpUtil.isSafe(tokenValue, "<业务标识>"); // 判断指定 token 的指定业务是否处于二级认证时间内 +StpUtil.checkSafe("<业务标识>"); // 检查当前会话,指定业务标识是否已通过二级认证,如未通过则抛出异常 +StpUtil.getSafeTime("<业务标识>"); // 获取当前会话的指定业务标识二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) +StpUtil.closeSafe("<业务标识>"); // 在当前会话 结束指定业务标识二级认证 +``` + + diff --git a/sa-token-doc/arch/data-structure.md b/sa-token-doc/arch/data-structure.md new file mode 100644 index 000000000..757df22d2 --- /dev/null +++ b/sa-token-doc/arch/data-structure.md @@ -0,0 +1,477 @@ +# 数据结构 + + +## 1、登录会话 + +### 1.1、token -> loginId 映射 + +``` js +// ttl = 此 token 的 timeout 有效期 +{tokenName}:{loginType}:token:{tokenValue} --> {loginId} +``` + +
+详细 + +示例: +``` js +satoken:login:token:47ab0105-2be1-400c-b517-82f81a0cfcf8 --> 10001 +``` + +异常 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 +// ttl = 对应 token 的 timeout 有效期值 +{tokenName}:{loginType}:last-active:{tokenValue} --> {13位时间戳} +``` + +
+详细 + +示例: +``` js +satoken:login:last-active:06d1f12b-614e-4c00-8d8e-c07fef5f4aa9 --> 1722334954193 +``` + +value 格式分两种: +``` +1722334954193 // 单值时:此 token 最后访问日期 +1722334954193, 1200 // 双值时:此 token 最后访问日期,此 token 指定的动态 active-timeout 值 +``` + +注意:判断一个 token 是否 active-timeout 过期,与 ttl 无关,而是利 value 值计算: +``` js +当前时间 - token 最后访问时间 > active-timeout (true=token 已冻结,false=token 未冻结 ) +``` + +
+ + + +### 1.3、SaSession + +``` js +{tokenName}:{loginType}:session:{loginId} --> {SaSession 对象} // Account-Session +{tokenName}:{loginType}:token-session:{loginId} --> {SaSession 对象} // Token-Session +{tokenName}:custom:session:{sessionId} --> {SaSession 对象} // 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 挂载数据 详情 + // 更多值 ... + }, + "terminalList": [ // 已登录终端信息列表(Account-Session才会有值) + "java.util.Vector", + [ + { + "@class": "cn.dev33.satoken.session.SaTerminalInfo", + "index": 1, + "tokenValue": "2551663f-bb98-47d7-9af3-e2e6a28dadce", // 客户端 token 值 + "deviceType": "DEF", // 登录设备类型 + "deviceId": "xxxxxxxxx", // 登录设备id + "extraData": { + // 扩展信息列表 (手动自定义值) + "@class": "java.util.LinkedHashMap", + "deviceSimpleTitle": "XiaoMi 15 Ultra", + "loginAddress": "浙江省杭州市西湖区", + "loginIp": "127.0.0.1", + "loginTime": "2025-03-08 15:00:02" + }, + "createTime": 1741406340845 // 登录时间 + } + ] + ] +} +``` + +
+ + +### 1.4、二级认证 +``` js +{tokenName}:{loginType}:safe:{service}:{tokenValue} --> SAFE_AUTH_SAVE_VALUE +``` +value 为常亮值:`SAFE_AUTH_SAVE_VALUE` + + +### 1.5、账号服务封禁 +``` js +{tokenName}:{loginType}:disable:{service}:{loginId} --> {level} +``` +value 为封禁等级,int类型 + + +### 1.6、其它 +SaApplication 全局变量 +``` js +{tokenName}:var:{变量名} +``` + +本次请求新创建 token,在 SaStorage 存储 key +``` js +JUST_CREATED_ --> {token} +``` + +本次请求新创建 token,在 SaStorage 存储 key (无前缀方式) +``` js +JUST_CREATED_NOT_PREFIX_ --> {token} +``` + +临时身份切换,使用的key +``` js +SWITCH_TO_SAVE_KEY_{loginType} --> {loginId} +``` + + +## 2、SSO 单点登录 + +### 2.1、ticket -> loginId 映射 +``` js +// ttl = 此 ticket 有效期,下同理 +{tokenName}:ticket:{ticket} --> {loginId} +``` + +### 2.2、ticket -> client 映射 +``` js +{tokenName}:ticket-client:{ticket} --> {client} +``` + +### 2.3、loginId -> ticket 映射(client + loginId 反查 ticket) +``` js +{tokenName}:ticket-index:{client}:{loginId} --> {ticket} +``` + + + +## 3、OAuth2 统一认证 + +### 3.1、Code 授权码 +``` js +{tokenName}:oauth2:code:{code} --> {CodeModel 对象} +``` + +
+详细 + +value 示例: + +``` 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} --> {code 值} +``` + + + +### 3.2、Access-Token 资源令牌 +``` js +{tokenName}:oauth2:access-token:{accessToken} --> {AccessTokenModel 对象} +``` + +
+详细 + +value 示例: + +``` js +{ + "@class": "cn.dev33.satoken.oauth2.data.model.AccessTokenModel", // java class 信息 + "accessToken": "Pu3t55dJIgvkmVoHz50FqaVQOJ6Flggjr2eHTiS74Ooai8e3nNyYPq78K80P", // 资源令牌值 + "refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m", // 刷新令牌值 + "expiresTime": 1738280553695, // 资源令牌到期时间 + "refreshExpiresTime": 1740865353760, // 刷新令牌到期时间 + "clientId": "1001", // 对应的应用id + "loginId": "10001", // 对应的loginId + "scopes": [ // 所具有的权限列表 + "java.util.ArrayList", + [ + "userinfo", + "userid", + "openid", + "unionid", + "oidc" + ] + ], + "tokenType": "Bearer", // tokenType + "grantType": "authorization_code", // 授权方式 + "extraData": { // 扩展数据 + "@class": "java.util.LinkedHashMap", + "userid": "10001", + "openid": "ded91dc189a437dd1bac2274be167d50", + "unionid": "11d48faa74c4e5f19355ccc53c1c5c7a", + "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g" + }, + "createTime": 1738273353694, // 创建时间 + "expiresIn": 7199 // 资源令牌剩余有效时间,单位秒 + "refreshExpiresIn": 2592000, // 刷新令牌剩余有效时间,单位秒 +} + +``` + +
+ +clientId + loginId 反查 Access-Token +``` js +{tokenName}:oauth2:access-token-index:{clientId}:{loginId} --> {access_token 值} +``` + + +### 3.3、Refresh-Token 资源令牌 +``` js +{tokenName}:oauth2:refresh-token:{refreshToken} --> {RefreshTokenModel 对象} +``` + +
+详细 + +value 示例: + +``` js +{ + "@class": "cn.dev33.satoken.oauth2.data.model.RefreshTokenModel", // java class 信息 + "refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m", // 刷新令牌值 + "expiresTime": 1740865353760, // 刷新令牌到期时间 + "clientId": "1001", // 对应的应用id + "loginId": "10001", // 对应的loginId + "scopes": [ // 所具有的权限列表 + "java.util.ArrayList", + [ + "userinfo", + "userid", + "openid", + "unionid", + "oidc" + ] + ], + "extraData": { // 扩展数据 + "@class": "java.util.LinkedHashMap", + "userid": "10001", + "openid": "ded91dc189a437dd1bac2274be167d50", + "unionid": "11d48faa74c4e5f19355ccc53c1c5c7a", + "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g" + }, + "createTime": 1738273353760, // 创建时间 + "expiresIn": 2591999 // 刷新令牌剩余有效时间,单位秒 +} +``` + +
+ +clientId + loginId 反查 Refresh-Token +``` js +{tokenName}:oauth2:refresh-token-index:{clientId}:{loginId} --> {refresh_token 值} +``` + + +### 3.4、Client-Token 应用令牌 +``` js +{tokenName}:oauth2:client-token:{clientToken} --> {ClientTokenModel 对象} +``` + +
+详细 + +value 示例: + +``` js +{ + "@class": "cn.dev33.satoken.oauth2.data.model.ClientTokenModel", // java class 信息 + "clientToken": "lIpS3fKEACKMFauEWVpR7Zmzh7SoFetPVuB9aDzISnqzHKu8R3OwpWFy5nLv", // 应用令牌值 + "expiresTime": 1738280930646, // 应用令牌到期时间 + "clientId": "1001", // 对应的应用id + "scopes": [ // 所具有的权限列表 + "java.util.ArrayList", + [ + "userinfo", + "userid", + "openid", + "unionid", + "oidc" + ] + ], + "tokenType": "Bearer", // tokenType + "grantType": "client_credentials", // 授权类型 + "extraData": { // 扩展数据 + "@class": "java.util.LinkedHashMap" + }, + "createTime": 1738273730646, // 创建时间 + "expiresIn": 7199 // 应用令牌剩余有效时间,单位秒 +} +``` + +
+ +clientId 反查 Client-Token +``` js +{tokenName}:oauth2:client-token-index:{clientId} --> {client_token 值} +``` + +Lower-Client-Token 次级应用令牌索引 +``` js +{tokenName}:oauth2:lower-client-token-index:{clientId} --> {client_token 值} +``` + +### 3.5、用户授权记录 +``` js +{tokenName}:oauth2:grant-scope:{clientId}:{loginId} --> {scope列表} +``` +值为 scope 列表,多个用逗号隔开,例如:`userinfo,openid,userid`。 + + + + +## 4、插件 + +### 4.1、临时 token 会话 + +temp-token -> value + +``` js +// namespace 默认值为 "temp-token" +{tokenName}:{namespace}:{temp-token} --> {value} +``` + +value 反查 temp-token + +``` js +{tokenName}:raw-session:{namespace}:{value} --> {Raw#SaSession 对象} +``` + +- 在 SaSession 以 `__HD_TEMP_TOKEN_MAP` 为 key 存储 temp-token 索引列表。值类型为 Map。 +- 其中:Map 的 key = temp-token 值,Map 的 value = 此 temp-token 到期时间戳。 + + + +### 4.2、 Same-Token + +Same-Token +``` js +{tokenName}:var:same-token --> {same-token 值} +``` + +Past-Same-Token +``` js +{tokenName}:var:past-same-token --> {same-token 值} +``` + + +### 4.3、Sign 签名 + +随机字符串 +``` js +// nonce 值 默认为 32位随机字符 +{tokenName}:sign:nonce:{nonce} --> {nonce 值} +``` + +### 4.4、API Key + +``` js +// namespace 默认值为 "apikey" (全小写), ttl = 此 API Key 剩余有效期 +{tokenName}:{namespace}:{apikey} --> {ApiKeyModel 对象} +``` + +
+详细 + +key 示例: + +``` js +satoken:apikey:AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R +``` + +value 示例: +``` js +{ + "@class": "cn.dev33.satoken.apikey.model.ApiKeyModel", // java class 信息 + "title": "test", // API Key 名称 + "intro": null, // 用途介绍 + "apiKey": "AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R", // API Key 值 + "loginId": "10001", // 所属用户 id + "createTime": 1766509019137, // 创建时间戳 + "expiresTime": 1769101019136, // 到期时间戳 + "isValid": true, // 是否有效 + "scopes": [ // 含有权限 + "java.util.ArrayList", + [ + "userinfo", + "user-update" + ] + ], + "extraData": null // 扩展数据:Map 类型 +} +``` + +
+ + +value 反查 API Key + +``` js +{tokenName}:raw-session:{namespace}:{value} --> {Raw#SaSession 对象} +``` + +- 在 SaSession 以 `__HD_API_KEY_LIST` 为 key 存储 API Key 索引列表。值类型为 List (API Key 列表)。 + + + + + + + + diff --git a/sa-token-doc/arch/dir-intro.md b/sa-token-doc/arch/dir-intro.md new file mode 100644 index 000000000..923050244 --- /dev/null +++ b/sa-token-doc/arch/dir-intro.md @@ -0,0 +1,190 @@ +# 仓库目录介绍 + +--- + +### 1、仓库根目录介绍: + +``` js +── sa-token + ├── sa-token-core // [核心] Sa-Token 核心模块 + ├── sa-token-dependencies // [依赖] Sa-Token 依赖版本信息 + ├── sa-token-special-dependencies // [依赖] Sa-Token 特殊依赖(SpringBoot2/3/4 版本隔离) + ├── sa-token-bom // [核心] Sa-Token bom 包 + ├── sa-token-starter // [整合] Sa-Token 与其它框架整合 + ├── sa-token-plugin // [插件] Sa-Token 插件合集 + ├── sa-token-demo // [示例] Sa-Token 示例合集 + ├── sa-token-test // [测试] Sa-Token 单元测试合集 + ├── sa-token-doc // [文档] Sa-Token 开发文档 + ├── MEMO // [备忘] 内部备忘录、开发记录 + ├── pom.xml // [依赖] 顶级pom文件 + ├── LICENSE // 开源协议 + ├── mvn clean.bat // 一键 mvn clean 核心包+所有示例包 + ├── mvn test.bat // 一键单元测试 + ├── preview-doc.bat // 一键预览开发文档 + ├── README.md // 仓库自述文件 +``` + + +### 2、所有目录详细介绍: + +``` js +── sa-token + ├── sa-token-core // [核心] Sa-Token 核心模块 + ├── sa-token-dependencies // [依赖] Sa-Token 依赖版本信息 + ├── sa-token-special-dependencies // [依赖] Sa-Token 特殊依赖(SpringBoot2/3/4 版本隔离) + ├── sa-token-bom // [核心] Sa-Token bom 包 + ├── sa-token-starter // [整合] Sa-Token 与其它框架整合 + ├── sa-token-servlet // [整合] Sa-Token 整合 Servlet 容器实现类包 + ├── sa-token-jakarta-servlet // [整合] Sa-Token 整合 Jakarta-Servlet 容器实现类包 + ├── sa-token-spring-boot-webmvc-reactor-v2v3v4-common // [整合] Sa-Token SpringBoot WebMvc+Reactor 公共包 (2/3/4) + ├── sa-token-spring-boot-reactor-v3v4-common // [整合] Sa-Token SpringBoot Reactor 公共包 (3/4) + ├── sa-token-spring-boot-starter // [整合] Sa-Token 整合 SpringBoot2 快速集成 + ├── sa-token-spring-boot-webmvc-v3v4-common // [整合] Sa-Token SpringBoot WebMvc 公共包 (3/4) + ├── sa-token-spring-boot3-starter // [整合] Sa-Token 整合 SpringBoot3 快速集成 + ├── sa-token-spring-boot4-starter // [整合] Sa-Token 整合 SpringBoot4 快速集成 + ├── sa-token-reactor-spring-boot-starter // [整合] Sa-Token 整合 SpringBoot2 Reactor 响应式编程 快速集成 + ├── sa-token-reactor-spring-boot3-starter // [整合] Sa-Token 整合 SpringBoot3 Reactor 响应式编程 快速集成 + ├── sa-token-reactor-spring-boot4-starter // [整合] Sa-Token 整合 SpringBoot4 Reactor 响应式编程 快速集成 + ├── sa-token-solon-plugin // [整合] Sa-Token 整合 Solon 快速集成 + ├── sa-token-jfinal-plugin // [整合] Sa-Token 整合 JFinal 快速集成 + ├── sa-token-jboot-plugin // [整合] Sa-Token 整合 jboot 快速集成 + ├── sa-token-loveqq-boot-starter // [整合] Sa-Token 整合 LoveQQ-Boot 快速集成 + ├── sa-token-plugin // [插件] Sa-Token 插件合集 + ├── sa-token-jackson // [插件] Sa-Token 整合 Jackson (json序列化插件) + ├── sa-token-jackson3 // [插件] Sa-Token 整合 Jackson3 (json序列化插件) + ├── sa-token-fastjson // [插件] Sa-Token 整合 Fastjson (json序列化插件) + ├── sa-token-fastjson2 // [插件] Sa-Token 整合 Fastjson2 (json序列化插件) + ├── sa-token-snack3 // [插件] Sa-Token 整合 Snack3 (json序列化插件) + ├── sa-token-snack4 // [插件] Sa-Token 整合 Snack4 (json序列化插件) + ├── sa-token-hutool-timed-cache // [插件] Sa-Token 整合 Hutool 缓存组件 Timed-Cache(基于内存) (数据缓存插件) + ├── sa-token-caffeine // [插件] Sa-Token 整合 Caffeine 缓存组件(基于内存) (数据缓存插件) + ├── sa-token-thymeleaf // [插件] Sa-Token 整合 Thymeleaf (自定义标签方言) + ├── sa-token-freemarker // [插件] Sa-Token 整合 Freemarker (自定义标签方言) + ├── sa-token-dubbo // [插件] Sa-Token 整合 Dubbo (RPC 调用鉴权、状态传递) + ├── sa-token-dubbo3 // [插件] Sa-Token 整合 Dubbo3 (RPC 调用鉴权、状态传递) + ├── sa-token-temp-jwt // [插件] Sa-Token 整合 jjwt (临时 Token) + ├── sa-token-jwt // [插件] Sa-Token 整合 jjwt (JWT 登录认证) + ├── sa-token-sso // [插件] Sa-Token 实现 SSO 单点登录 + ├── sa-token-oauth2 // [插件] Sa-Token 实现 OAuth2.0 认证 + ├── sa-token-apikey // [插件] Sa-Token 实现 API Key 认证 + ├── sa-token-sign // [插件] Sa-Token 实现 API 参数签名 + ├── sa-token-redisson // [插件] Sa-Token 整合 Redisson (数据缓存插件) + ├── sa-token-redisx // [插件] Sa-Token 整合 Redisx (数据缓存插件) + ├── sa-token-serializer-features // [插件] Sa-Token 序列化实现扩展 + ├── sa-token-redis-template // [插件] Sa-Token 整合 RedisTemplate (数据缓存插件) + ├── sa-token-redis-template-jdk-serializer // [插件] Sa-Token 整合 RedisTemplate - 使用 jdk 序列化算法 (数据缓存插件) + ├── sa-token-redis-jackson // [插件] Sa-Token 整合 RedisTemplate - 使用 Jackson 序列化算法 (数据缓存插件) + ├── sa-token-alone-redis // [插件] Sa-Token 独立 Redis 插件,实现 [ 权限缓存与业务缓存分离 ] + ├── sa-token-alone-redisson // [插件] Sa-Token 独立 Redisson 插件,实现 [ 权限缓存与业务缓存分离 ] + ├── sa-token-spring-aop // [插件] Sa-Token 整合 SpringAOP 注解鉴权 + ├── sa-token-spring-el // [插件] Sa-Token 实现 SpringEL 表达式注解鉴权 + ├── sa-token-grpc // [插件] Sa-Token 整合 gRPC (RPC 调用鉴权、状态传递) + ├── sa-token-quick-login // [插件] Sa-Token 快速注入登录页插件 + ├── sa-token-redisson-spring-boot-starter // [插件] Sa-Token 整合 Redisson - SpringBoot 自动配置包 (数据缓存插件) + ├── sa-token-forest // [插件] Sa-Token 整合 Forest,http 请求处理器 + ├── sa-token-okhttps // [插件] Sa-Token 整合 OkHttps,http 请求处理器 + ├── sa-token-demo // [示例] Sa-Token 示例合集 + ├── sa-token-demo-alone-redis // [示例] Sa-Token 集成 alone-redis 模块 + ├── sa-token-demo-alone-redis-cluster // [示例] Sa-Token 集成 alone-redis 模块、集群模式 + ├── sa-token-demo-alone-redisson // [示例] Sa-Token 集成 alone-redisson 模块 + ├── sa-token-demo-alone-redisson-cluster // [示例] Sa-Token 集成 alone-redisson 模块、集群模式 + ├── sa-token-demo-alone-redisson-sb3 // [示例] Sa-Token 集成 alone-redisson 模块(Spring Boot 3) + ├── sa-token-demo-alone-redisson-sb4 // [示例] Sa-Token 集成 alone-redisson 模块(Spring Boot 4) + ├── sa-token-demo-apikey // [示例] Sa-Token API Key 模块示例 + ├── sa-token-demo-async // [示例] Sa-Token 异步场景示例 + ├── sa-token-demo-beetl // [示例] Sa-Token 集成 beetl 示例 + ├── sa-token-demo-bom-import // [示例] Sa-Token bom 包导入示例 + ├── sa-token-demo-case // [示例] Sa-Token 各模块示例 + ├── sa-token-demo-device-lock // [示例] Sa-Token 设备锁登录示例 - 后端 + ├── sa-token-demo-device-lock-h5 // [示例] Sa-Token 设备锁登录示例 - 前端 + ├── sa-token-demo-dubbo // [示例] Sa-Token 集成 dubbo + ├── sa-token-demo-dubbo-consumer // [示例] Sa-Token 集成 dubbo 鉴权,消费端(调用端) + ├── sa-token-demo-dubbo-provider // [示例] Sa-Token 集成 dubbo 鉴权,生产端(被调用端) + ├── sa-token-demo-dubbo3-consumer // [示例] Sa-Token 集成 dubbo3 鉴权,消费端(调用端) + ├── sa-token-demo-dubbo3-provider // [示例] Sa-Token 集成 dubbo3 鉴权,生产端(被调用端) + ├── sa-token-demo-freemarker // [示例] Sa-Token 集成 Freemarker 标签方言 + ├── sa-token-demo-first-run // [示例] 源码首次运行引导,强制引入 apikey、sso、oauth2、sign、jwt 等可选模块 + ├── sa-token-demo-bug-reproduce // [示例] Issue/Bug 复现沙盒(按需改代码验证) + ├── sa-token-demo-grpc // [示例] Sa-Token 集成 grpc 鉴权 + ├── client // [示例] Sa-Token 集成 grpc 鉴权,client 端 + ├── server // [示例] Sa-Token 集成 grpc 鉴权,server 端 + ├── sa-token-demo-hutool-timed-cache // [示例] Sa-Token 集成 hutool timed-cache + ├── sa-token-demo-caffeine // [示例] Sa-Token 集成 Caffeine + ├── sa-token-demo-jwt // [示例] Sa-Token 集成 jwt 登录认证 + ├── sa-token-demo-oauth2 // [示例] Sa-Token 集成 OAuth2.0 + ├── sa-token-demo-oauth2-client // [示例] Sa-Token 集成 OAuth2.0 (客户端) + ├── sa-token-demo-oauth2-client-h5 // [示例] Sa-Token OAuth2 前端测试页 + ├── sa-token-demo-oauth2-server // [示例] Sa-Token 集成 OAuth2.0 (服务端) + ├── sa-token-demo-oauth2-server-h5 // [示例] Sa-Token 集成 OAuth2.0 (服务端 - 前后台分离示例) + ├── sa-token-demo-quick-login // [示例] Sa-Token 集成 quick-login 模块 + ├── sa-token-demo-quick-login-sb3 // [示例] Sa-Token 集成 quick-login 模块 (SpringBoot3) + ├── sa-token-demo-remember-me // [示例] Sa-Token 实现 [ 记住我 ] 模式 + ├── page_project // [示例] Sa-Token 实现 [ 记住我 ] 模式、前端页面 + ├── sa-token-demo-remember-me-server // [示例] Sa-Token 实现 [ 记住我 ] 模式、后端接口 + ├── sa-token-demo-solon // [示例] Sa-Token 集成 Solon + ├── sa-token-demo-solon-reisson // [示例] Sa-Token 集成 Solon、Reisson + ├── sa-token-demo-springboot // [示例] Sa-Token 整合 SpringBoot + ├── sa-token-demo-springboot3-redis // [示例] Sa-Token 整合 SpringBoot3 整合 Redis + ├── sa-token-demo-springboot4-redis // [示例] Sa-Token 整合 SpringBoot4 整合 Redis + ├── sa-token-demo-springboot-low-version // [示例] Sa-Token 整合 SpringBoot2 低版本 + ├── sa-token-demo-springboot-redis // [示例] Sa-Token 整合 SpringBoot 整合 Redis + ├── sa-token-demo-springboot-redisson // [示例] Sa-Token 整合 SpringBoot 整合 redisson + ├── sa-token-demo-sse // [示例] 在 SSE 中使用 Sa-Token + ├── sa-token-demo-ssm // [示例] 在 SSM 中使用 Sa-Token + ├── sa-token-demo-sso // [示例] Sa-Token 集成 SSO 单点登录 + ├── sa-token-demo-sso-server // [示例] Sa-Token 集成 SSO单点登录-Server认证中心 + ├── sa-token-demo-sso1-client // [示例] Sa-Token 集成 SSO单点登录-模式一 应用端 (同域、同Redis) + ├── sa-token-demo-sso2-client // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端 (跨域、同Redis) + ├── sa-token-demo-sso3-client // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (跨域、跨Redis) + ├── sa-token-demo-sso3-client-nosdk // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (不使用sdk,纯手动对接) + ├── sa-token-demo-sso3-client-resdk // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (ReSdk 模式,重写部分方法对接任意技术栈) + ├── sa-token-demo-sso3-client-anon // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (匿名应用接入示例) + ├── sa-token-demo-sso-server-h5 // [示例] Sa-Token 集成 SSO单点登录-Server认证中心 (前后端分离) + ├── sa-token-demo-sso-client-h5 // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-原生h5 版本) + ├── sa-token-demo-sso-server-vue2 // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-Vue2 版本) + ├── sa-token-demo-sso-client-vue3 // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-Vue3 版本) + ├── sa-token-demo-sso-for-solon // [示例] Sa-Token 集成 SSO 单点登录(Solon 版) + ├── sa-token-demo-sso-server-solon // [示例] Sa-Token 集成 SSO单点登录-Server认证中心 + ├── sa-token-demo-sso1-client-solon // [示例] Sa-Token 集成 SSO单点登录-模式一 应用端 (同域、同Redis) + ├── sa-token-demo-sso2-client-solon // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端 (跨域、同Redis) + ├── sa-token-demo-sso3-client-solon // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (跨域、跨Redis) + ├── sa-token-demo-test // [示例] Sa-Token 整合测试项目 + ├── sa-token-demo-thymeleaf // [示例] Sa-Token 集成 Thymeleaf 标签方言 + ├── sa-token-demo-webflux // [示例] Sa-Token 整合 WebFlux + ├── sa-token-demo-webflux-springboot3 // [示例] Sa-Token 整合 WebFlux (SpringBoot3) + ├── sa-token-demo-webflux-springboot4 // [示例] Sa-Token 整合 WebFlux (SpringBoot4) + ├── sa-token-demo-websocket // [示例] Sa-Token 集成 Web-Socket 鉴权示例 + ├── sa-token-demo-websocket-spring // [示例] Sa-Token 集成 Web-Socket(Spring封装版) 鉴权示例 + ├── sa-token-demo-loveqq-boot // [示例] Sa-Token 集成 LoveQQ-Boot + ├── pom.xml // 示例 pom 文件,用于帮助在 idea 中一键导入所有 demo + ├── sa-token-test // [测试] Sa-Token 单元测试合集 + ├── sa-token-easy-test // [测试] Sa-Token 简易测试 + ├── sa-token-springboot-test // [测试] Sa-Token SpringBoot 整合测试 + ├── sa-token-jwt-test // [测试] Sa-Token jwt 整合测试 + ├── sa-token-temp-jwt-test // [测试] Sa-Token temp-jwt 整合测试 + ├── sa-token-json-test // [测试] Sa-Token json 序列化测试 + ├── sa-token-jackson3-test // [测试] Sa-Token Jackson3 整合测试 + ├── sa-token-serializer-test // [测试] Sa-Token 序列化测试 + ├── sa-token-doc // [文档] Sa-Token 开发文档 + ├── MEMO // [备忘] 内部备忘录、开发记录 + ├── pom.xml // [依赖] 顶级pom文件 + ├── LICENSE // 开源协议 + ├── mvn clean.bat // 一键 mvn clean 核心包+所有示例包 + ├── mvn test.bat // 一键单元测试 + ├── preview-doc.bat // 一键预览开发文档 + ├── README.md // 仓库自述文件 +``` + + + +其它([sa-tokens](https://gitee.com/sa-tokens) 组织下相关仓库): + +- [Awesome-Sa-Token](https://gitee.com/sa-tokens/awesome-sa-token):集成 Sa-Token 的优秀开源案例收集。 +- [sa-token-rust](https://gitee.com/sa-tokens/sa-token-rust):Sa-Token 的 Rust 版本,轻量级 Rust 权限认证框架。 +- [sa-token-go](https://gitee.com/sa-tokens/sa-token-go):Sa-Token 的 Go 版本,轻量级 Go 权限认证框架。 +- [Sa-Token-Study](https://gitee.com/sa-tokens/Sa-Token-Study):Sa-Token 涉及技术点学习笔记与实战。 +- [Sa-Token-Login-Demos](https://gitee.com/sa-tokens/Sa-Token-Login-Demos):各种登录方式示例集合,一站式学习 Sa-Token 登录认证。 +- [sa-token-doc-big-file](https://gitee.com/sa-tokens/sa-token-doc-big-file):sa-token-doc 文档中的图片资源文件。 +- [sa-token-three-plugin](https://gitee.com/sa-tokens/sa-token-three-plugin):Sa-Token 第三方插件合集。 +- [sa-token-demo-cross](https://gitee.com/sa-tokens/sa-token-demo-cross):Sa-Token 处理跨域场景示例。 +- [auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test):Java 权限认证框架功能 测试 / 对比 / 迁移。 diff --git a/sa-token-doc/blog/common/blog.css b/sa-token-doc/blog/common/blog.css new file mode 100644 index 000000000..8f8369a08 --- /dev/null +++ b/sa-token-doc/blog/common/blog.css @@ -0,0 +1,1206 @@ +/* Sa-Token 博客页专用样式(不影响 doc 文档站) */ + +/* vue.css 默认 body:not(.ready){overflow:hidden},博客静态页无 docsify,必须解除 */ +body.blog-page { + overflow-x: hidden; + overflow-y: auto !important; + height: auto; + min-height: 100%; + scroll-behavior: smooth; + /* 博客专属蓝色主题(文档站仍为绿色) */ + --blog-theme: #0a4fb0; + --blog-theme-hover: #1a6fd4; + --blog-theme-soft: #f4f8fd; + --theme-color: #0a4fb0; + --a-color: #0a4fb0; + --a-hover-color: #1a6fd4; + --blog-context-width: 800px; + --blog-content-aside-gap: 20px; + --blog-page-gradient: linear-gradient(165deg, #eef4ff 0%, #f7f9fc 42%, #f3f6fa 68%, #e9eef5 100%); + --blog-page-bg: #e9eef5; + --blog-panel-border: #e2e8f0; + --blog-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: var(--blog-font); + background: var(--blog-page-gradient); + background-attachment: fixed; +} + +body.blog-page.blog-sidebar-open { + overflow: hidden; +} + +/* 博客顶栏固定白色,不受文档站调色盘 / localStorage 主题色影响 */ +.blog-page .doc-header { + background-color: #fff !important; + background: #fff !important; + box-shadow: 0 1px 3px rgba(26, 26, 26, 0.1); +} + +/* 下拉展开按钮(视频/案例等)不显示导航下划线;下拉项也不跟 vue.css 的 a:hover underline */ +.blog-page .doc-header .nav-right .zk-box > .wzi::after { + display: none; +} + +.blog-page .doc-header .nav-right .zk-box .zk-context a, +.blog-page .doc-header .nav-right .zk-box .zk-context a:hover { + text-decoration: none; +} + +/* 移动端顶栏:只保留 首页/文档/博客,避免按钮挤到下一行 */ +@media screen and (max-width: 800px) { + .blog-page .doc-header .nav-left .select-language-box, + .blog-page .doc-header .nav-right .zk-box, + .blog-page .doc-header .nav-right .github-corner { + display: none !important; + } + + /* 覆盖 doc.css 隐藏 logo 文字;有空位就显示 Sa-Token */ + .blog-page .doc-header .nav-left .logo-box .logo-text { + display: inline-block !important; + font-size: 20px; + } + + .blog-page .doc-header .nav-right { + padding-right: 8px; + margin-right: 0 !important; + } + + .blog-page .doc-header .nav-right > * { + margin: 0 6px; + } +} + +.blog-page { + background: transparent; +} + +/* 覆盖 vue.css / doc.css:宽屏对齐 doc.html — 左栏贴边、正文居中、右栏挂正文右侧 */ +.blog-page .blog-main-box { + min-height: calc(100vh - 60px); + padding-top: 60px; + padding-bottom: 32px; + text-align: left; + width: 100%; + box-sizing: border-box; + position: relative; + overflow: visible; + background: transparent; +} + +@media screen and (min-width: 1100px) { + .blog-page .blog-main-box { + display: block; + text-align: center; + padding-top: calc(60px + 16px); + } + + .blog-page .blog-main-box > .sidebar.blog-sidebar { + position: fixed; + left: 0; + top: 60px; + bottom: auto; + width: var(--doc-left-width, 300px); + max-height: calc(100vh - 60px); + overflow-x: hidden; + overflow-y: auto; + padding: 12px 0 48px; + margin-top: 0 !important; + /* doc.css 有 .sidebar{background-color:transparent!important},此处必须压过 */ + background: #fff !important; + background-color: #fff !important; + border: 0; + border-right: 1px solid var(--blog-panel-border); + box-shadow: none; + transform: none !important; + z-index: 10; + -webkit-overflow-scrolling: touch; + text-align: left; + } + + .blog-page .blog-main-box > .content.blog-content { + position: relative !important; + left: auto !important; + right: auto !important; + top: auto !important; + bottom: auto !important; + display: block; + width: var(--blog-context-width, 800px); + max-width: var(--blog-context-width, 800px); + margin: 0 auto; + padding-top: 0; + overflow: visible; + text-align: left; + background: #fff; + border: 1px solid var(--blog-panel-border); + box-shadow: none; + } + + .blog-page .blog-main-box > .content.blog-content #main .markdown-section { + width: 100%; + max-width: 100%; + margin: 0; + padding: 32px 40px 64px; + overflow: visible; + font-size: 15px; + line-height: 2; + color: #252933; + } + + .blog-page .blog-main-box > .doc-right-bj-box { + position: fixed; + left: calc(50% + (var(--blog-context-width, 800px) / 2) + var(--blog-content-aside-gap, 20px)); + top: calc(60px + 16px); + width: var(--doc-right-width, 300px); + max-height: calc(100vh - 60px - 16px); + overflow-x: hidden; + overflow-y: auto; + margin-top: 0; + padding: 0; + background: transparent; + box-shadow: none; + -webkit-overflow-scrolling: touch; + text-align: left; + } + + .blog-page .blog-main-box > .doc-right-bj-box > .blog-right-toc-panel { + background: #fff; + border: 1px solid var(--blog-panel-border); + box-shadow: none; + padding: 16px; + margin-bottom: 12px; + } + + /* 文章页章节目录:全宽贴边(须高于上一规则的选择器优先级) */ + .blog-page.blog-article-page .blog-main-box > .doc-right-bj-box > .blog-right-toc-panel { + display: flex; + flex-direction: column; + min-height: 0; + padding: 16px 0; + } + + .blog-page.blog-article-page .blog-main-box > .doc-right-bj-box > .blog-right-toc-panel .doc-right-bj-box-title { + padding: 0 16px 10px; + } + + .blog-page.blog-article-page .blog-main-box > .doc-right-bj-box > .blog-right-toc-panel > .blog-article-toc { + flex: 0 1 auto; + min-height: 0; + max-height: min(48vh, calc(100vh - 76px - 280px)); + margin: 14px 0 0; + padding: 0; + overflow-x: hidden; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #d0d5dd transparent; + -webkit-overflow-scrolling: touch; + } + + .blog-page .blog-right-toc-panel .doc-right-bj-box-title { + padding-bottom: 10px; + margin-bottom: 0; + border-bottom-color: var(--blog-panel-border); + } + + .blog-page .blog-right-toc-panel > .blog-index-aside, + .blog-page .blog-right-toc-panel > .blog-article-toc { + margin-top: 14px; + } + + .blog-page.blog-article-page .blog-main-box > .doc-right-bj-box { + overflow-x: hidden; + overflow-y: hidden; + -webkit-overflow-scrolling: auto; + } +} + +@media screen and (max-width: 1099px) { + .blog-page .blog-main-box { + display: flex; + flex-direction: column; + align-items: stretch; + } +} + +.blog-page .doc-right-more-item { + position: static !important; + width: auto; + margin-top: 12px; +} + +/* 右侧广告内层背景:与页面渐变拉开区分,不改布局 */ +.blog-page .doc-right-more-item .mad-bg-box { + background-color: #fff; +} + +/* 正文排版(参考掘金阅读体验:15px / 行高 2 / 段落间距 15px) */ +.blog-page #main .markdown-section { + font-family: var(--blog-font); + color: #252933; +} + +/* 文章页正文浅灰网格(掘金同款):四边渐隐;标题区无网格(标题在网格层之外) */ +.blog-page.blog-article-page #main .markdown-section { + position: relative; +} + +.blog-page.blog-article-page #main .markdown-section::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: + linear-gradient(90deg, rgba(60, 10, 30, 0.04) 3%, transparent 0), + linear-gradient(1turn, rgba(60, 10, 30, 0.04) 3%, transparent 0); + background-size: 20px 20px; + background-position: 50% center; + -webkit-mask-image: + linear-gradient(to right, transparent, #000 48px, #000 calc(100% - 48px), transparent), + linear-gradient(to bottom, transparent, #000 40px, #000 calc(100% - 64px), transparent); + -webkit-mask-composite: source-in; + mask-image: + linear-gradient(to right, transparent, #000 48px, #000 calc(100% - 48px), transparent), + linear-gradient(to bottom, transparent, #000 40px, #000 calc(100% - 64px), transparent); + mask-composite: intersect; +} + +.blog-page.blog-article-page #main .markdown-section > * { + position: relative; + z-index: 1; +} + +/* 标题 + 元信息铺白底,盖住网格(对齐掘金:标题在 .markdown-body 外) */ +.blog-page.blog-article-page #main .markdown-section > h1 { + background: #fff; + margin: -32px -40px 16px; + padding: 32px 40px 0; +} + +.blog-page.blog-article-page #main .markdown-section > .blog-article-meta { + background: #fff; + margin: 0 -40px 24px; + padding: 0 40px 8px; +} + +/* 上一篇 / 下一篇:同样铺白底,盖住网格 */ +.blog-page.blog-article-page #main .markdown-section > .blog-post-nav { + background: #fff; + margin: 48px -40px -64px; + padding: 24px 40px 64px; + border-top: 1px solid #eee; +} + +/* 压过 doc.css 链接单独指定「思源黑体」,避免正文内中英文基线不齐 */ +.blog-page #main *:not(h1, h2, h3, h4, h5, h6) a { + font-family: var(--blog-font); +} + +.blog-page #main .markdown-section h1, +.blog-page #main .markdown-section strong { + color: #252933; +} + +.blog-page #main .markdown-section h1 { + font-size: 32px; + line-height: 1.31; + margin: 0 0 16px; + font-weight: 700; +} + +/* 文章页标题装饰(白皮书风格下划线),首页列表不加 */ +.blog-page.blog-article-page #main .markdown-section h2, +.blog-page.blog-article-page #main .markdown-section h3, +.blog-page.blog-article-page #main .markdown-section h4 { + color: #252933; +} + +.blog-page.blog-article-page #main .markdown-section h2 { + font-size: 20px; + line-height: 1.4; + margin: 34px 0 20px; + padding-bottom: 12px; + /* 淡蓝下划线,与灰色网格区分 */ + border-bottom: 2px solid #c5d8f0; + font-weight: 700; + color: #252933; +} + +.blog-page.blog-article-page #main .markdown-section h3 { + font-size: 18px; + line-height: 1.4; + margin: 36px 0 16px; + padding-bottom: 8px; + border-bottom: 1px solid #d2e2f5; + font-weight: 700; + color: #252933; +} + +.blog-page.blog-article-page #main .markdown-section h3::before { + content: "▍"; + color: var(--blog-theme); + margin-right: 8px; +} + +.blog-page.blog-article-page #main .markdown-section h4 { + font-size: 16px; + line-height: 1.5; + margin: 20px 0 10px; + font-weight: 600; +} + +.blog-page #main .markdown-section p, +.blog-page #main .markdown-section ul, +.blog-page #main .markdown-section ol { + margin: 15px 0; + line-height: 2; +} + +.blog-page #main .markdown-section blockquote { + margin: 20px 0; + color: #515767; +} + +.blog-page.blog-article-page #main .markdown-section img { + margin: 20px 0; + cursor: zoom-in; +} + +.blog-page.blog-article-page #main .markdown-section img.medium-zoom-image--hidden { + cursor: zoom-in; +} + +/* 图片放大:遮罩须盖住顶栏(z-index:1000)、左侧目录(z-index:10) */ +.blog-page .medium-zoom-overlay { + z-index: 10050; +} + +.blog-page .medium-zoom-image.medium-zoom-image--opened { + z-index: 10051; +} + +/* 代码块:只压 vue.css 浅色主题;行高(1.5rem)、字号(.8rem) 与 doc.html 一致,勿覆盖 line-height / font-size */ +.blog-page .main-box .markdown-section pre[v-pre][data-lang] { + position: relative; + padding: 0 !important; + margin: 1.2em 0; + border-radius: 2px; + overflow-x: auto; + overflow-y: hidden; + /* 滚动容器自身背景不会随 scrollWidth 延伸,深色铺底留给 > code */ + background: #191919 !important; + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; +} + +.blog-page .main-box .markdown-section pre[v-pre][data-lang] > code { + /* inline-block + min-width:随最长行撑开,横向滚动时背景不露白 */ + display: inline-block; + min-width: calc(100% - 40px); + vertical-align: top; + box-sizing: border-box; + background-color: #191919 !important; + color: #fff !important; + border: 0 !important; + border-radius: 0 !important; + padding: 1.5em 1.3em !important; + margin: 0 0 0 40px !important; + max-width: none !important; + overflow: visible !important; + white-space: pre !important; + word-wrap: normal !important; +} + +.blog-page .main-box .markdown-section pre[v-pre][data-lang]:after { + color: #888; +} + +/* 字符串:勿跟 --theme-color 深蓝,黑底对比度不够 */ +.blog-page .main-box .markdown-section .token.string, +.blog-page .main-box .markdown-section .token.attr-value { + color: #42b983; +} + +/* 覆盖 vue.css:.sidebar ul { margin-left: 15px } */ +.blog-page .blog-sidebar .sidebar-nav ul { + margin: 0; + padding: 0; +} + +.blog-page .blog-sidebar > .sidebar-nav > ul { + padding-left: 1em; +} + +.blog-sidebar .sidebar-nav > ul > li > strong { + display: block; + font-family: var(--blog-font); + font-size: 13px; + color: #333; + margin: 16px 0 8px; + padding-left: 0; +} + +.blog-sidebar .sidebar-nav > ul > li:first-child > strong { + margin-top: 4px; +} + +.blog-page .blog-sidebar .sidebar-nav > ul > li > ul { + list-style: none; + padding-left: 1em; +} + +.blog-sidebar .sidebar-nav > ul > li > ul > li { + position: relative; + margin: 0; + padding: 0; +} + +.blog-sidebar .sidebar-nav > ul > li > ul > li > a { + display: block; + font-family: var(--blog-font); + font-size: 13px; + line-height: 1.5; + padding: 8px 8px 8px 0; + color: #86909c; + white-space: normal; + word-break: break-word; + overflow: visible; + text-overflow: unset; + text-decoration: none; +} + +.blog-sidebar .sidebar-nav > ul > li > ul > li > a:hover { + color: var(--blog-theme); +} + +.blog-sidebar .sidebar-nav > ul > li > ul > li.active-rep::after { + content: ''; + position: absolute; + right: 0; + top: 7px; + bottom: 7px; + width: 3px; + background: var(--blog-theme); +} + +.blog-sidebar .sidebar-nav > ul > li > ul > li.active-rep > a { + color: var(--blog-theme); + font-weight: 700; +} + +.blog-sidebar-end { + margin-top: 20px; + padding: 0 0.5em 72px; + text-align: center; + font-size: 12px; + line-height: 1.5; + color: #c0c4cc; + user-select: none; + white-space: nowrap; +} + +.blog-article-meta { + color: #888; + font-size: 14px; + margin-bottom: 24px; +} + +.blog-article-meta span + span::before { + content: ' · '; +} + +.blog-article-toc { + margin-bottom: 12px; + margin-top: 0; +} + +.blog-article-toc .blog-toc-list { + list-style: none; + padding: 0; + margin: 0; +} + +.blog-article-toc .blog-toc-list li { + position: relative; + margin: 0; + padding: 6px 6px 6px 12px; + line-height: 1.5; + border-radius: 0; + min-width: 0; + overflow: hidden; + transition: color 0.15s ease, background-color 0.15s ease; +} + +.blog-article-toc .blog-toc-list li::before { + content: ''; + position: absolute; + left: 0; + top: 5px; + bottom: 5px; + width: 2px; + background: transparent; +} + +.blog-article-toc .blog-toc-list li.active::before { + background: var(--blog-theme); +} + +.blog-article-toc .blog-toc-list li.toc-h3 { + padding-left: 1.5em; +} + +.blog-article-toc .blog-toc-list li.toc-h4 { + padding-left: 2.15em; +} + +.blog-article-toc .blog-toc-list a { + font-size: 12px; + color: #86909c; + text-decoration: none; + display: block; + padding: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.blog-article-toc .blog-toc-list a:hover { + color: var(--blog-theme); +} + +.blog-article-toc .blog-toc-list li.active a { + color: var(--blog-theme); + font-weight: 500; +} + +.blog-article-toc::-webkit-scrollbar { + width: 3px; +} + +.blog-article-toc::-webkit-scrollbar-track { + margin: 0; + background: transparent; +} + +.blog-article-toc::-webkit-scrollbar-thumb { + background: #d0d5dd; + border-radius: 0; +} + +.blog-article-toc::-webkit-scrollbar-thumb:hover { + background: #b8bcc4; +} + +/* 锚点跳转:留出固定导航栏高度(与 doc.html topMargin: 90 一致) */ +.blog-page #main .markdown-section h2[id], +.blog-page #main .markdown-section h3[id], +.blog-page #main .markdown-section h4[id] { + scroll-margin-top: 90px; + cursor: pointer; +} + +.blog-post-nav { + display: flex; + justify-content: space-between; + gap: 16px; + margin-top: 48px; + padding-top: 24px; + border-top: 1px solid #eee; +} + +.blog-page #main .blog-post-nav a { + flex: 1; + font-size: 14px; + color: var(--blog-theme); + text-decoration: none; + line-height: 1.5; + word-break: break-word; + border-bottom: 0; +} + +.blog-page #main .blog-post-nav a:hover { + color: var(--blog-theme-hover); + border-bottom: 0; + text-decoration: none; +} + +.blog-post-nav-next { text-align: right; } + +.blog-index-intro { + color: #666; + margin-bottom: 24px; +} + +/* 首页 Tab:参考掘金 推荐/最新/关注 */ +.blog-index-tabs { + display: flex; + align-items: center; + gap: 28px; + margin: 0 0 8px; + padding: 0 0 12px; + border-bottom: 1px solid #e5e6eb; +} + +.blog-index-tab { + appearance: none; + border: 0; + background: transparent; + padding: 0; + font-family: var(--blog-font) !important; + font-size: 16px; + font-weight: 500; + line-height: 1.4; + color: #8a919f; + cursor: pointer; + position: relative; + transition: color 0.2s ease; +} + +.blog-index-tab:hover { + color: #1d2129; +} + +.blog-index-tab.is-active { + color: #1d2129; + font-weight: 600; +} + +.blog-index-tab.is-active::after { + content: ""; + position: absolute; + left: 50%; + bottom: -13px; + width: 24px; + height: 3px; + margin-left: -12px; + background: var(--blog-theme); + border-radius: 2px; +} + +.blog-index-panels { + padding-top: 4px; +} + +.blog-index-panel { + display: none; +} + +.blog-index-panel.is-active { + display: block; +} + +.blog-index-list { + display: flex; + flex-direction: column; + gap: 0; +} + +.blog-index-card { + padding: 20px 0; + border-bottom: 1px solid #e5e6eb; +} + +.blog-index-card-body { + display: flex; + align-items: flex-start; +} + +.blog-index-card-text { + flex: 1; + min-width: 0; +} + +.blog-index-card-cover { + flex: 0 0 120px; + width: 120px; + margin-left: 16px; +} + +.blog-index-card-cover--empty { + flex: 0 0 0; + width: 0; + margin-left: 0; + overflow: hidden; +} + +.blog-index-cover { + display: block; + margin: 0; + padding: 0; + line-height: 0; + font-size: 0; + text-decoration: none; + border-bottom: 0; +} + +.blog-index-cover img { + display: block; + width: 120px; + height: 80px; + margin: 0; + object-fit: cover; + border: 1px solid var(--blog-panel-border); + background: #fff; +} + +/* 压过 doc.css #main *:not(h1-h6) a { border-bottom } */ +.blog-index-page #main a.blog-index-cover, +.blog-index-page #main a.blog-index-cover:hover { + color: inherit; + font-weight: 400 !important; + font-family: var(--blog-font) !important; + border-bottom: 0 !important; + text-decoration: none !important; + box-shadow: none; +} + +.blog-index-card:last-child { + border-bottom: 0; +} + +.blog-index-card h2 { + font-size: 19px; + line-height: 1.35; + letter-spacing: -0.01em; + margin: 0 0 12px; + font-weight: 700; + border-bottom: 0; + padding-bottom: 0; +} + +.blog-index-page #main .markdown-section .blog-index-card h2 { + margin: 0 0 8px !important; + padding: 0 !important; + border-bottom: 0 !important; + font-size: 19px; + line-height: 1.35; + font-weight: 700; +} + +/* 中文优先字体栈,避免英文用 Source Sans Pro、汉字用系统中文字体导致基线不齐 */ +.blog-index-page #main .blog-index-card h2 a { + color: #1d1d1f; + font-size: 19px; + font-weight: 700 !important; + line-height: 1.35; + letter-spacing: -0.01em; + font-family: var(--blog-font) !important; + text-decoration: none; + border-bottom: 0; + display: block; + white-space: normal; + word-break: break-word; + transition: color 0.2s ease; +} + +.blog-index-page #main .blog-index-card h2 a:hover { + color: var(--blog-theme); + border-bottom: 0; + text-decoration: none; +} + +.blog-index-meta { + display: none; +} + +/* 压过 .blog-page #main .markdown-section p 的 line-height:2 / margin,以及 vue.css 的 word-spacing */ +.blog-index-page #main .markdown-section .blog-index-intro { + font-family: var(--blog-font) !important; + font-size: 15px; + line-height: 1.6 !important; + color: #666; + margin: 0 0 24px !important; + word-spacing: normal; + letter-spacing: normal; +} + +.blog-index-page #main .markdown-section .blog-index-desc { + font-family: var(--blog-font) !important; + font-size: 13px; + font-weight: 400; + line-height: 1.5 !important; + color: #909399; + margin: 0 !important; + word-spacing: normal; + letter-spacing: normal; + -webkit-font-smoothing: antialiased; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.blog-index-page #main .markdown-section .blog-index-card--title-multiline .blog-index-desc { + -webkit-line-clamp: 1; +} + +/* 社区 Tab:平台 + 标题 + 日期,外链新窗口打开 */ +.blog-community-list { + display: flex; + flex-direction: column; +} + +.blog-community-card { + display: grid; + grid-template-columns: 72px minmax(0, 1fr) 96px; + align-items: start; + gap: 12px; + padding: 16px 0; + border-bottom: 1px solid #e5e6eb; +} + +.blog-community-card:last-child { + border-bottom: 0; +} + +.blog-community-platform { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + font-size: 11px; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0.02em; + text-align: center; + padding: 4px 6px; + border-radius: 2px; + white-space: normal; + word-break: break-all; + box-sizing: border-box; +} + +/* 常见平台配色;未识别走 --default */ +.blog-community-platform--csdn { + color: #e33e33; + background: #fff1f0; +} + +.blog-community-platform--juejin { + color: #1e80ff; + background: #eaf2ff; +} + +.blog-community-platform--weixin { + color: #07a84d; + background: #e8f8ef; +} + +.blog-community-platform--51cto { + color: #00a854; + background: #e6f7ef; +} + +.blog-community-platform--cnblogs { + color: #0585d6; + background: #e8f4fc; +} + +.blog-community-platform--zhihu { + color: #056de8; + background: #ebf3ff; +} + +.blog-community-platform--baijiahao { + color: #fa541c; + background: #fff2e8; +} + +.blog-community-platform--php { + color: #6b5b95; + background: #f3f0f8; +} + +.blog-community-platform--default { + color: #597ef7; + background: #f0f5ff; +} + +.blog-index-page #main .blog-community-title { + font-family: var(--blog-font) !important; + font-size: 15px; + font-weight: 700 !important; + line-height: 1.5; + color: #1d2129; + text-decoration: none; + border-bottom: 0; + display: block; + white-space: normal; + word-break: break-word; + transition: color 0.2s ease; +} + +.blog-index-page #main .blog-community-title:hover { + color: var(--blog-theme); + border-bottom: 0; + text-decoration: none; +} + +.blog-community-date { + font-size: 13px; + line-height: 1.4; + color: #86909c; + text-align: right; + white-space: nowrap; + padding-top: 2px; +} + +.blog-community-empty { + margin: 24px 0 0; + font-size: 14px; + color: #86909c; + text-align: center; +} + +/* 推荐 Tab:标题 + 日期(无平台标签) */ +.blog-recommended-list { + display: flex; + flex-direction: column; +} + +.blog-recommended-card { + display: grid; + grid-template-columns: minmax(0, 1fr) 96px; + align-items: start; + gap: 12px; + padding: 16px 0; + border-bottom: 1px solid #e5e6eb; +} + +.blog-recommended-card:last-child { + border-bottom: 0; +} + +.blog-index-page #main .blog-recommended-title { + font-family: var(--blog-font) !important; + font-size: 15px; + font-weight: 700 !important; + line-height: 1.5; + color: #1d2129; + text-decoration: none; + border-bottom: 0; + display: block; + white-space: normal; + word-break: break-word; + transition: color 0.2s ease; +} + +.blog-index-page #main .blog-recommended-title:hover { + color: var(--blog-theme); + border-bottom: 0; + text-decoration: none; +} + +.blog-recommended-date { + font-size: 13px; + line-height: 1.4; + color: #86909c; + text-align: right; + white-space: nowrap; + padding-top: 2px; +} + +.blog-recommended-empty { + margin: 24px 0 0; + font-size: 14px; + color: #86909c; + text-align: center; +} + +@media screen and (max-width: 640px) { + .blog-community-card { + grid-template-columns: 64px minmax(0, 1fr); + grid-template-rows: auto auto; + } + + .blog-community-date { + grid-column: 2; + text-align: left; + font-size: 12px; + } + + .blog-recommended-card { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto auto; + } + + .blog-recommended-date { + text-align: left; + font-size: 12px; + } +} + +.blog-index-aside { + font-size: 12px; + color: #909399; + line-height: 1.6; + margin-bottom: 16px; + margin-top: 0; +} + +.blog-index-aside p { + margin: 0 0 8px; +} + +.blog-foot-box { + position: relative; + z-index: 20; + background-color: #000; + color: #ddd; + padding: 2em 0; + line-height: 28px; + margin-top: 0; +} + +.blog-foot-box a { + color: #ddd; +} + +.blog-sidebar-toggle { + display: none; + position: fixed; + left: 12px; + bottom: 16px; + z-index: 1001; + width: 44px; + height: 44px; + border: 1px solid #ddd; + border-radius: 4px; + background: #fff; + font-size: 20px; + cursor: pointer; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.blog-sidebar-mask { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.4); + z-index: 999; +} + +@media screen and (max-width: 1100px) { + .blog-page .blog-main-box > .doc-right-bj-box { + display: none; + } + + .blog-page .blog-main-box > .sidebar.blog-sidebar { + display: block !important; + position: fixed; + left: -320px !important; + top: 60px; + width: 300px; + max-width: 85vw; + height: calc(100vh - 60px); + z-index: 1000; + transition: left 0.25s ease-out, transform 0.25s ease-out; + transform: translateX(0) !important; + background: #fff !important; + background-color: #fff !important; + box-shadow: 2px 0 12px rgba(0,0,0,0.1); + overflow-x: hidden; + overflow-y: auto; + margin-top: 0 !important; + padding: 12px 0 48px; + } + + body.blog-page.blog-sidebar-open .blog-main-box > .sidebar.blog-sidebar { + left: 0 !important; + transform: translateX(0) !important; + } + + body.blog-page.blog-sidebar-open .blog-sidebar-mask { + display: block; + } + + .blog-sidebar-toggle { + display: block; + } + + .blog-page .blog-main-box > .content.blog-content { + width: 100%; + max-width: 100%; + margin: 0; + background: #fff; + min-height: calc(100vh - 60px); + } + + .blog-page .blog-main-box > .content.blog-content #main .markdown-section { + width: 100%; + padding: 24px 20px 48px; + font-size: 15px; + line-height: 2; + } + + .blog-page.blog-article-page #main .markdown-section > h1 { + margin: -24px -20px 16px; + padding: 24px 20px 0; + } + + .blog-page.blog-article-page #main .markdown-section > .blog-article-meta { + margin: 0 -20px 24px; + padding: 0 20px 8px; + } + + .blog-page.blog-article-page #main .markdown-section > .blog-post-nav { + margin: 48px -20px -48px; + padding: 24px 20px 48px; + } + + .blog-page.blog-article-page #main .markdown-section::before { + -webkit-mask-image: + linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent), + linear-gradient(to bottom, transparent, #000 28px, #000 calc(100% - 48px), transparent); + mask-image: + linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent), + linear-gradient(to bottom, transparent, #000 28px, #000 calc(100% - 48px), transparent); + } +} + +@media screen and (max-width: 1600px) { + body.blog-page { + --doc-left-width: 200px; + } +} + +@media screen and (min-width: 1100px) { + .blog-page .blog-main-box > .doc-right-bj-box { + display: block; + } +} + +/* ========== 博客蓝色主题:覆盖 doc.css / vue.css 中的绿色强调色 ========== */ +.blog-page .github-corner svg { + fill: var(--blog-theme); +} + +.blog-page .main-box .sidebar li a:hover { + color: var(--blog-theme); +} + +.blog-page .sidebar .sidebar-nav > ul > li > ul > li.active-rep > a { + color: var(--blog-theme); +} + +.blog-page .help-btn { + border-color: var(--blog-theme); + color: var(--blog-theme); +} + +.blog-page .main-box details { + border-color: var(--blog-theme); + background-color: var(--blog-theme-soft); +} + +.blog-page .main-box details summary { + background-color: var(--blog-theme-soft); + color: var(--blog-theme); +} + +.blog-page .markdown-section a { + color: var(--blog-theme); +} + +.blog-page .markdown-section blockquote { + border-left-color: var(--blog-theme); +} diff --git a/sa-token-doc/blog/common/blog.js b/sa-token-doc/blog/common/blog.js new file mode 100644 index 000000000..80d115307 --- /dev/null +++ b/sa-token-doc/blog/common/blog.js @@ -0,0 +1,368 @@ +(function () { + var SCROLL_OFFSET = 90; + var PRISM_LANG = { + xml: 'markup', + html: 'markup', + cmd: 'bash', + url: 'plain' + }; + + function normalizePrismLang(lang) { + return PRISM_LANG[lang] || lang; + } + + function initCodeHighlight() { + document.querySelectorAll('pre[v-pre][data-lang] > code').forEach(function (code) { + var lang = code.parentElement.getAttribute('data-lang'); + if (!lang) return; + code.classList.add('lang-' + lang); + code.classList.add('language-' + normalizePrismLang(lang)); + }); + + if (window.Prism && typeof window.Prism.highlightAll === 'function') { + window.Prism.highlightAll(); + } + + document.querySelectorAll('pre[v-pre][data-lang]').forEach(function (pre) { + var lang = pre.getAttribute('data-lang'); + var code = pre.querySelector('code'); + if (!lang || !code) return; + code.classList.add('lang-' + lang); + pre.classList.remove('language-' + normalizePrismLang(lang)); + }); + } + + function initSidebarDrawer() { + var toggle = document.querySelector('.blog-sidebar-toggle'); + var mask = document.querySelector('.blog-sidebar-mask'); + if (!toggle || !mask) return; + + toggle.addEventListener('click', function () { + document.body.classList.toggle('blog-sidebar-open'); + }); + mask.addEventListener('click', function () { + document.body.classList.remove('blog-sidebar-open'); + }); + } + + function getScrollTopFor(el) { + var top = el.getBoundingClientRect().top + (window.pageYOffset || document.documentElement.scrollTop); + return Math.max(0, top - SCROLL_OFFSET); + } + + function supportsSmoothScroll() { + return 'scrollBehavior' in document.documentElement.style; + } + + function animateScrollTo(top, duration) { + var start = window.pageYOffset || document.documentElement.scrollTop; + var change = top - start; + if (Math.abs(change) < 1) return; + + var startTime = performance.now(); + duration = duration || 450; + + function step(now) { + var elapsed = now - startTime; + var progress = Math.min(elapsed / duration, 1); + var ease = progress < 0.5 + ? 4 * progress * progress * progress + : 1 - Math.pow(-2 * progress + 2, 3) / 2; + window.scrollTo(0, start + change * ease); + if (progress < 1) requestAnimationFrame(step); + } + + requestAnimationFrame(step); + } + + function scrollToSection(id, smooth) { + if (!id) return false; + var el = document.getElementById(id); + if (!el) return false; + var top = getScrollTopFor(el); + + if (!smooth) { + window.scrollTo(0, top); + return true; + } + + if (supportsSmoothScroll()) { + window.scrollTo({ top: top, behavior: 'smooth' }); + } else { + animateScrollTo(top); + } + return true; + } + + function hashForId(id) { + return '#' + encodeURIComponent(id); + } + + function hashToId() { + var raw = (location.hash || '').replace(/^#/, ''); + if (!raw) return ''; + try { + return decodeURIComponent(raw); + } catch (e) { + return raw; + } + } + + function setUrlHash(id) { + var hash = hashForId(id); + if (location.hash === hash) return; + history.pushState(null, '', location.pathname + location.search + hash); + } + + function navigateToSection(id, smooth) { + if (!scrollToSection(id, smooth)) return; + setUrlHash(id); + } + + function initTocHighlight() { + var tocLinks = document.querySelectorAll('.blog-article-toc .blog-toc-list a'); + if (!tocLinks.length) return; + + var headings = []; + tocLinks.forEach(function (a) { + var id = (a.getAttribute('href') || '').replace(/^#/, ''); + try { + id = decodeURIComponent(id); + } catch (e) {} + var el = document.getElementById(id); + if (el) headings.push({ el: el, link: a, li: a.parentElement }); + }); + if (!headings.length) return; + + function updateActiveByScroll() { + var current = headings[0]; + for (var i = 0; i < headings.length; i++) { + if (headings[i].el.getBoundingClientRect().top - SCROLL_OFFSET <= 1) { + current = headings[i]; + } + } + headings.forEach(function (h) { + h.li.classList.remove('active'); + }); + current.li.classList.add('active'); + } + + window.addEventListener('scroll', updateActiveByScroll, { passive: true }); + updateActiveByScroll(); + + tocLinks.forEach(function (a) { + if (!a.getAttribute('title')) a.setAttribute('title', a.textContent.trim()); + a.addEventListener('click', function (e) { + e.preventDefault(); + var id = (a.getAttribute('href') || '').replace(/^#/, ''); + try { + id = decodeURIComponent(id); + } catch (err) {} + navigateToSection(id, true); + document.body.classList.remove('blog-sidebar-open'); + }); + }); + } + + function initHeadingClicks() { + var content = document.querySelector('#main .markdown-section'); + if (!content) return; + + content.addEventListener('click', function (e) { + var heading = e.target.closest('h2[id], h3[id], h4[id]'); + if (!heading || !content.contains(heading)) return; + if (e.target.closest('a')) return; + e.preventDefault(); + navigateToSection(heading.id, true); + }); + } + + function initHashNavigation() { + function applyHash(smooth) { + var id = hashToId(); + if (!id) return; + scrollToSection(id, smooth); + } + + window.addEventListener('hashchange', function () { + applyHash(true); + }); + + window.addEventListener('popstate', function () { + applyHash(true); + }); + + if (location.hash) { + if ('scrollRestoration' in history) { + history.scrollRestoration = 'manual'; + } + window.scrollTo(0, 0); + requestAnimationFrame(function () { + applyHash(false); + }); + } + } + + function scrollActiveSidebarItem() { + var active = document.querySelector('.blog-sidebar .active-rep'); + var sidebar = document.querySelector('.blog-sidebar'); + if (!active || !sidebar) return; + try { + var offset = active.offsetTop - sidebar.clientHeight / 3; + sidebar.scrollTop = Math.max(0, offset); + } catch (e) {} + } + + function initAdClose() { + if (!window.jQuery || $(window).width() < 800) return; + + var allowJg = 86400000; + try { + var closeAdTime = localStorage.closeAdTimeRight; + if (closeAdTime && (Date.now() - parseInt(closeAdTime, 10)) < allowJg) { + $('.ad-box').remove(); + return; + } + } catch (e) {} + + $('.ad-close').click(function () { + layer.confirm('关闭后,一天内不再展现此信息', function () { + $('.ad-box').fadeOut(1000); + layer.msg('关闭成功'); + localStorage.closeAdTimeRight = Date.now(); + }); + }); + } + + function initIndexDescClamp() { + if (!document.body.classList.contains('blog-index-page')) return; + + function isMultilineTitle(el) { + var range = document.createRange(); + range.selectNodeContents(el); + return range.getClientRects().length > 1; + } + + function update() { + document.querySelectorAll('.blog-index-card').forEach(function (card) { + var title = card.querySelector('.blog-index-card-text h2 a'); + if (!title) return; + card.classList.toggle('blog-index-card--title-multiline', isMultilineTitle(title)); + }); + } + + update(); + var timer; + window.addEventListener('resize', function () { + clearTimeout(timer); + timer = setTimeout(update, 150); + }); + } + + function initBlogIndexTabs() { + var tabBar = document.querySelector('.blog-index-tabs'); + if (!tabBar) return; + + var buttons = tabBar.querySelectorAll('.blog-index-tab'); + var panels = document.querySelectorAll('.blog-index-panel'); + + function tabFromHash() { + var hash = location.hash; + if (hash === '#community') return 'community'; + if (hash === '#recommended') return 'recommended'; + return 'official'; + } + + function setTabHash(tab) { + var nextHash = tab === 'official' ? '' : '#' + tab; + if (location.hash === nextHash) return; + if (nextHash) { + history.replaceState(null, '', nextHash); + } else { + history.replaceState(null, '', location.pathname + location.search); + } + } + + function activateTab(tab, updateHash) { + if (updateHash !== false) setTabHash(tab); + buttons.forEach(function (item) { + var active = item.getAttribute('data-tab') === tab; + item.classList.toggle('is-active', active); + item.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + panels.forEach(function (panel) { + panel.classList.toggle('is-active', panel.getAttribute('data-panel') === tab); + }); + } + + buttons.forEach(function (btn) { + btn.addEventListener('click', function () { + activateTab(btn.getAttribute('data-tab')); + }); + }); + + window.addEventListener('hashchange', function () { + activateTab(tabFromHash(), false); + }); + + activateTab(tabFromHash(), false); + } + + function initImageZoom() { + if (!document.body.classList.contains('blog-article-page')) return; + if (typeof window.mediumZoom !== 'function') return; + + var matches = Element.prototype.matches || Element.prototype.msMatchesSelector; + var imgs = [].slice.call( + document.querySelectorAll('#main .markdown-section img:not(.emoji):not([data-no-zoom])') + ).filter(function (img) { + return matches.call(img, 'a img') === false; + }); + if (!imgs.length) return; + + window.mediumZoom(imgs, { + margin: 24, + background: 'rgba(0, 0, 0, 0.85)' + }); + } + + function initSidebarEnd() { + var nav = document.querySelector('.blog-sidebar .sidebar-nav'); + if (!nav || nav.querySelector('.blog-sidebar-end')) return; + var end = document.createElement('div'); + end.className = 'blog-sidebar-end'; + end.textContent = '--------- 到底线了 ---------'; + nav.appendChild(end); + } + + /** 正文链接新窗口打开(锚点、上一篇/下一篇除外) */ + function initContentLinks() { + if (!document.body.classList.contains('blog-article-page')) return; + var section = document.querySelector('#main .markdown-section'); + if (!section) return; + + section.querySelectorAll('a[href]').forEach(function (a) { + if (a.closest('.blog-post-nav')) return; + var href = a.getAttribute('href') || ''; + if (!href || href.charAt(0) === '#' || href.indexOf('javascript:') === 0) return; + a.setAttribute('target', '_blank'); + a.setAttribute('rel', 'noopener noreferrer'); + }); + } + + document.addEventListener('DOMContentLoaded', function () { + initSidebarDrawer(); + initSidebarEnd(); + initBlogIndexTabs(); + initIndexDescClamp(); + initTocHighlight(); + initHeadingClicks(); + initHashNavigation(); + scrollActiveSidebarItem(); + initCodeHighlight(); + initImageZoom(); + initContentLinks(); + initAdClose(); + if (typeof initSaTranslate === 'function') initSaTranslate(); + }); +})(); diff --git a/sa-token-doc/blog/common/prism-bash.min.js b/sa-token-doc/blog/common/prism-bash.min.js new file mode 100644 index 000000000..f1659f1e3 --- /dev/null +++ b/sa-token-doc/blog/common/prism-bash.min.js @@ -0,0 +1 @@ +!function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",a={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},n={bash:a,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:a}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:n},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:n.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},a.inside=e.languages.bash;for(var s=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=n.variable[1].inside,i=0;i]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; \ No newline at end of file diff --git a/sa-token-doc/blog/common/prism-core.min.js b/sa-token-doc/blog/common/prism-core.min.js new file mode 100644 index 000000000..2e3ac2eef --- /dev/null +++ b/sa-token-doc/blog/common/prism-core.min.js @@ -0,0 +1 @@ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); \ No newline at end of file diff --git a/sa-token-doc/blog/common/prism-css.min.js b/sa-token-doc/blog/common/prism-css.min.js new file mode 100644 index 000000000..3a9b628eb --- /dev/null +++ b/sa-token-doc/blog/common/prism-css.min.js @@ -0,0 +1 @@ +!function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); \ No newline at end of file diff --git a/sa-token-doc/blog/common/prism-javascript.min.js b/sa-token-doc/blog/common/prism-javascript.min.js new file mode 100644 index 000000000..22292c41f --- /dev/null +++ b/sa-token-doc/blog/common/prism-javascript.min.js @@ -0,0 +1 @@ +Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; \ No newline at end of file diff --git a/sa-token-doc/blog/common/prism-markup.min.js b/sa-token-doc/blog/common/prism-markup.min.js new file mode 100644 index 000000000..d39ab7faf --- /dev/null +++ b/sa-token-doc/blog/common/prism-markup.min.js @@ -0,0 +1 @@ +Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; \ No newline at end of file diff --git a/sa-token-doc/blog/essays/bie-ren-rao-guo-wo-de-wang-guan-zhi-jie-diao-yong-zi-yuan-fu-wu-zen-me-ban-shi-y.html b/sa-token-doc/blog/essays/bie-ren-rao-guo-wo-de-wang-guan-zhi-jie-diao-yong-zi-yuan-fu-wu-zen-me-ban-shi-y.html new file mode 100644 index 000000000..ba943d6a9 --- /dev/null +++ b/sa-token-doc/blog/essays/bie-ren-rao-guo-wo-de-wang-guan-zhi-jie-diao-yong-zi-yuan-fu-wu-zen-me-ban-shi-y.html @@ -0,0 +1,623 @@ + + + + + 别人绕过我的网关直接调用资源服务怎么办?使用 Sa-Token 解决:网关转发鉴权、RPC调用鉴权 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

别人绕过我的网关直接调用资源服务怎么办?使用 Sa-Token 解决:网关转发鉴权、RPC调用鉴权

+ +

一、需求场景

+

在微服务项目中,我们的子服务一般不能通过外网直接访问,必须通过网关转发才是一个合法的请求,这种子服务与外网的隔离一般分为两种:

+
    +
  1. 物理隔离:子服务部署在指定的内网环境中,只有网关对外网开放。
  2. +
  3. 逻辑隔离:子服务与网关同时暴露在外网,但是子服务会有一个权限拦截层保证只接受网关发送来的请求,绕过网关直接访问子服务会被提示:无效请求。
  4. +
+

这种鉴权需求牵扯到两个环节:网关转发鉴权服务内部调用鉴权

+

Sa-Token提供两种解决方案:
+1. 使用 OAuth2.0 模式的凭证式,将 Client-Token 用作各个服务的身份凭证进行权限校验。
+2. 使用 Same-Token 模块提供的身份校验能力,完成服务间的权限认证。

+

本篇主要讲解方案二 Same-Token 模块的整合步骤,其鉴权流程与 OAuth2.0 类似,不过使用方式上更加简洁。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

二、网关转发鉴权

+

1、引入依赖

+

在网关处引入的依赖为(此处以 SpringCloud Gateway 为例):

+
<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:https://sa-token.com -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
+
+<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-dao-redis-jackson</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.commons</groupId>
+    <artifactId>commons-pool2</artifactId>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
+

Gradle 方式:

+
// Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.com
+implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:1.34.0'
+
+// Sa-Token 整合 Redis (使用 jackson 序列化方式)
+implementation 'cn.dev33:sa-token-dao-redis-jackson:1.34.0'
+implementation 'org.apache.commons:commons-pool2'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

在子服务引入的依赖为:

+
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.com -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
+
+<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-dao-redis-jackson</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.commons</groupId>
+    <artifactId>commons-pool2</artifactId>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
+

Gradle 方式:

+
// Sa-Token 权限认证,在线文档:https://sa-token.com
+implementation 'cn.dev33:sa-token-spring-boot-starter:1.34.0'
+
+// Sa-Token 整合 Redis (使用 jackson 序列化方式)
+implementation 'cn.dev33:sa-token-dao-redis-jackson:1.34.0'
+implementation 'org.apache.commons:commons-pool2'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

2、网关处添加Same-Token

+

为网关添加全局过滤器:

+
/**
+ * 全局过滤器,为请求添加 Same-Token 
+ */
+@Component
+public class ForwardAuthFilter implements GlobalFilter {
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        ServerHttpRequest newRequest = exchange
+                .getRequest()
+                .mutate()
+                // 为请求追加 Same-Token 参数 
+                .header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
+                .build();
+        ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
+        return chain.filter(newExchange);
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
+

此过滤器会为 Request 请求头追加 Same-Token 参数,这个参数会被转发到子服务

+

3、在子服务里校验参数

+

在子服务添加过滤器校验参数

+
/**
+ * Sa-Token 权限认证 配置类 
+ */
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    // 注册 Sa-Token 全局过滤器 
+    @Bean
+    public SaServletFilter getSaServletFilter() {
+        return new SaServletFilter()
+                .addInclude("/**")
+                .addExclude("/favicon.ico")
+                .setAuth(obj -> {
+                    // 校验 Same-Token 身份凭证   —— 以下两句代码可简化为:SaSameUtil.checkCurrentRequestToken(); 
+                    String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN);
+                    SaSameUtil.checkToken(token);
+                })
+                .setError(e -> {
+                    return SaResult.error(e.getMessage());
+                })
+                ;
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
+

启动网关与子服务,访问测试:

+
+

如果通过网关转发,可以正常访问,直接访问子服务会提示:无效Same-Token:xxx

+
+

三、RPC调用鉴权

+

有时候我们需要在一个服务调用另一个服务的接口,这也是需要添加Same-Token作为身份凭证的

+

在服务里添加 Same-Token 流程与网关类似,我们以RPC框架 Feign 为例:

+
1、首先在调用方添加 FeignInterceptor
+
/**
+ * feign拦截器, 在feign请求发出之前,加入一些操作 
+ */
+@Component
+public class FeignInterceptor implements RequestInterceptor {
+    // 为 Feign 的 RCP调用 添加请求头Same-Token 
+    @Override
+    public void apply(RequestTemplate requestTemplate) {
+        requestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+
2、在调用接口里使用此 Interceptor
+
/**
+ * 服务调用 
+ */
+@FeignClient(
+        name = "sp-home",               // 服务名称 
+        configuration = FeignInterceptor.class,     // 请求拦截器 (关键代码)
+        fallbackFactory = SpCfgInterfaceFallback.class  // 服务降级处理 
+        )   
+public interface SpCfgInterface {
+
+    // 获取server端指定配置信息 
+    @RequestMapping("/SpConfig/getConfig")
+    public String getConfig(@RequestParam("key")String key);
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

被调用方的代码无需更改(按照网关转发鉴权处的代码注册全局过滤器),保持启动测试即可

+

四、Same-Token 模块详解

+

Same-Token —— 专门解决同源系统互相调用时的身份认证校验,它的作用不仅局限于微服务调用场景。

+

基本使用流程为:服务调用方获取Token,提交到请求中,被调用方取出Token进行校验:Token一致则校验通过,否则拒绝服务。

+

首先我们预览一下此模块的相关API:

+
// 获取当前Same-Token
+SaSameUtil.getToken();
+
+// 判断一个Same-Token是否有效
+SaSameUtil.isValid(token);
+
+// 校验一个Same-Token是否有效 (如果无效则抛出异常)
+SaSameUtil.checkToken(token);
+
+// 校验当前Request提供的Same-Token是否有效 (如果无效则抛出异常)
+SaSameUtil.checkCurrentRequestToken();
+
+// 刷新一次Same-Token (注意集群环境中不要多个服务重复调用) 
+SaSameUtil.refreshToken();
+
+// 在 Request 上储存 Same-Token 时建议使用的key
+SaSameUtil.SAME_TOKEN;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
+
1、疑问:这个Token保存在什么地方?有没有泄露的风险?Token为永久有效还是临时有效?
+

Same-Token 默认随 Sa-Token 数据一起保存在Redis中,理论上不会存在泄露的风险,每个Token默认有效期只有一天

+
2、如何主动刷新Same-Token,例如:五分钟、两小时刷新一次?
+

Same-Token 刷新间隔越短,其安全性越高,每个Token的默认有效期为一天,在一天后再次获取会自动产生一个新的Token

+
+

需要注意的一点是:Same-Token默认的自刷新机制,并不能做到高并发可用,多个服务一起触发Token刷新可能会造成毫秒级的短暂服务失效,其只能适用于 项目开发阶段 或 低并发业务场景

+
+

因此在微服务架构下,我们需要有专门的机制主动刷新Same-Token,保证其高可用

+

例如,我们可以专门起一个服务,使用定时任务来刷新Same-Token

+
/**
+ * Same-Token,定时刷新
+ */
+@Configuration
+public class SaSameTokenRefreshTask {
+    // 从 0 分钟开始 每隔 5 分钟执行一次 Same-Token  
+    @Scheduled(cron = "0 0/5 * * * ? ")
+    public void refreshToken(){
+        SaSameUtil.refreshToken();
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

以上的cron表达式刷新间隔可以配置为五分钟十分钟两小时,只要低于Same-Token的有效期(默认为一天)即可。

+
3、如果网关携带token转发的请求在落到子服务的节点上时,恰好刷新了token,导致鉴权未通过怎么办?
+

Same-Token 模块在每次刷新 Token 时,旧 Token 会被作为次级 Token 存储起来,
+只要网关携带的 Token 符合新旧 Token 其一即可通过认证,直至下一次刷新,新 Token 再次作为次级 Token 将此替换掉。

+

五、在 Dubbo 中集成 Same-Token

+

Same-Token 不仅可以为 Feign 提供调用鉴权操作,在 dubbo 的 RPC 调用时也可以使用 Same-Token 进行鉴权,提供这个能力的正是 sa-token-context-dubbo 插件。

+

1、先说说要解决的问题

+

在 Dubbo 的整个调用链中,代码被分为 Consumer 端和 Provider 端,为方便理解我们可以称其为 [调用端][被调用端]

+

RPC 模式的调用,可以让我们像调用本地方法一样完成服务通信,然而这种便利下却隐藏着两个问题:

+
    +
  • 上下文环境的丢失。
  • +
  • 上下文参数的丢失。
  • +
+

这种问题作用在 Sa-Token 框架上就是,在 [ 被调用端 ] 调用 Sa-Token 相关API会抛出异常:无效上下文

+

所以本插件的目的也就是解决上述两个问题:

+
    +
  • 在 [ 被调用端 ] 提供以 Dubbo 为基础的上下文环境
  • +
  • 在 RPC 调用时将 Token 传递至 [ 被调用端 ],同时在调用结束时将 Token 回传至 [ 调用端 ]。
  • +
+

2、引入插件

+

在项目已经引入 Dubbo 的基础上,继续添加依赖(Consumer 端和 Provider 端都需要引入):

+
<!-- Sa-Token 整合 Dubbo -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-context-dubbo</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

Gradle 方式

+
// Sa-Token 整合 Dubbo
+implementation 'cn.dev33:sa-token-context-dubbo:1.34.0'
  • 1
  • 2
+

然后我们就可以愉快的做到以下事情:

+
    +
  1. 在 [ 被调用端 ] 安全的调用 Sa-Token 相关 API。
  2. +
  3. 在 [ 调用端 ] 登录的会话,其登录状态可以自动传递到 [ 被调用端 ] 。
  4. +
  5. 在 [ 被调用端 ] 登录的会话,其登录状态也会自动回传到 [ 调用端 ] 。
  6. +
+

但是我们仍具有以下限制:

+
    +
  1. [ 调用端 ] 与 [ 被调用端 ] 的 SaStorage 数据无法互通。
  2. +
  3. [ 被调用端 ] 执行的 SaResponse.setHeader()setStatus() 等代码无效。
  4. +
+

应该合理避开以上 API 的使用。

+

3、dubbo 的 RPC调用鉴权

+

下面我们演示一下在 Dubbo 中如何集成 Same-Token 模块。其实思路和 Feign 模式一致,在 [ 调用端 ] 追加 Same-Token 参数,在 [ 被调用端 ] 校验这个 Same-Token 参数:

+
    +
  • 校验通过:调用成功。
  • +
  • 校验不通过:调用失败,抛出异常。
  • +
+

我们有两种方式完成整合。

+
方式一、使用配置(推荐)
+

直接在 application.yml 配置即可:

+ + + +
sa-token: 
+    # 打开 RPC 调用鉴权 
+    check-same-token: true
  • 1
  • 2
  • 3
+ + +
# 打开 RPC 调用鉴权 
+sa-token.check-same-token=true
  • 1
  • 2
+ + +
方式二、自建 Dubbo 过滤器校验
+

此方式略显繁琐,好处是除了Same-Token,我们还可以添加其它自定义参数 (attachment)。

+

1、在 [ 调用端 ] 的 \resources\META-INF\dubbo\ 目录新建 org.apache.dubbo.rpc.Filter 文件

+
dubboConsumerFilter=com.pj.DubboConsumerFilter
  • 1
+

新建 DubboConsumerFilter.java 过滤器

+
package com.pj;
+
+import org.apache.dubbo.common.constants.CommonConstants;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.*;
+
+import cn.dev33.satoken.same.SaSameUtil;
+
+/**
+ * Sa-Token 整合 Dubbo Consumer端过滤器 
+ */
+@Activate(group = {CommonConstants.CONSUMER}, order = -10000)
+public class DubboConsumerFilter implements Filter {
+
+    @Override
+    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+
+        // 追加 Same-Token 参数 
+        RpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
+
+        // 如果有其他自定义附加数据,如租户
+        // RpcContext.getContext().setAttachment("tenantContext", tenantContext);
+
+        // 开始调用
+        return invoker.invoke(invocation);
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
+

2、在 [ 被调用端 ] 的 \resources\META-INF\dubbo\ 目录新建 org.apache.dubbo.rpc.Filter 文件

+
dubboProviderFilter=com.pj.DubboProviderFilter
  • 1
+

新建 DubboProviderFilter.java 过滤器

+
package com.pj;
+
+import org.apache.dubbo.common.constants.CommonConstants;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.*;
+
+import cn.dev33.satoken.same.SaSameUtil;
+
+/**
+ * Sa-Token 整合 Dubbo Provider端过滤器 
+ */
+@Activate(group = {CommonConstants.PROVIDER}, order = -10000)
+public class DubboProviderFilter implements Filter {
+
+    @Override
+    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+
+        // 取出 Same-Token 进行校验 
+        String sameToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN);
+        SaSameUtil.checkToken(sameToken);
+
+        // 取出其他自定义附加数据
+        // TenantContext tenantContext = invocation.getAttachment("tenantContext");
+
+        // 开始调用
+        return invoker.invoke(invocation);
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
+

然后我们就可以进行安全的 RPC 调用了,不带有 Same-Token 参数的调用都会抛出异常,无法调用成功。

+
+

参考资料

+ +
+← 使用 Sa-Token 实现不同的登录模式:单地登录、多地登录、同端互斥登录 +前后端分离架构下使用 Sa-Token 完成登录认证 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/bu-xiang-xie-dai-ma-dan-xiang-yao-ji-cheng-yi-ge-deng-lu-ye-mian-sa-token-quick.html b/sa-token-doc/blog/essays/bu-xiang-xie-dai-ma-dan-xiang-yao-ji-cheng-yi-ge-deng-lu-ye-mian-sa-token-quick.html new file mode 100644 index 000000000..6cdfc55a9 --- /dev/null +++ b/sa-token-doc/blog/essays/bu-xiang-xie-dai-ma-dan-xiang-yao-ji-cheng-yi-ge-deng-lu-ye-mian-sa-token-quick.html @@ -0,0 +1,456 @@ + + + + + 不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现

+ +

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。

+
+

Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

今天介绍一下 Sa-Token 中的 Sa-Token-Quick-Login 插件。

+

一、解决什么问题

+

Sa-Token-Quick-Login 可以为一个系统快速的、零代码 注入一个登录页面。

+

试想一下,假如我们开发了一个非常简单的小系统,比如说:服务器性能监控页面,
+我们将它部署在服务器上,通过访问这个页面,我们可以随时了解服务器性能信息,非常方便。

+

然而,这个页面方便我们的同时,也方便了一些不法的攻击者,由于这个页面毫无防护的暴露在公网中,任何一台安装了浏览器的电脑都可以随时访问它!

+

为此,我们必须给这个系统加上一个登录认证,只有知晓了后台密码的人员才可以进行访问。

+

细细想来,完成这个功能你需要:

+
    +
  1. 编写前端登录页面,手写各种表单样式
  2. +
  3. 寻找合适的ajax类库,jQueryAxios?还是直接前后台不分离?
  4. +
  5. 寻找合适的模板引擎,比如jspThymeleafFreeMarkerVelocity……选哪个呢?
  6. +
  7. 处理后台各种拦截认证逻辑,前后台接口对接
  8. +
  9. 你可能还会遇到令人头痛欲裂的模板引擎中ContextPath处理
  10. +
  11. ……
  12. +
+

你马上就会发现,写个监控页你一下午就可以搞定,然而这个登录页你却可能需要花上两三天的时间,这是一笔及其不划算的时间浪费

+

那么现在你可能就会有个疑问,难道就没有什么方法给我的小项目快速增加一个登录功能吗?

+

Sa-Token-Quick-Login便是为了解决这个问题!

+

二、适用场景

+

Sa-Token-Quick-Login 旨在用最小的成本为项目增加一个登录认证功能

+
    +
  • 简单:只需要引入一个依赖便可为系统注入登录功能,快速、简单、零代码!
  • +
  • 不可定制:由于登录页面不可定制,所以Sa-Token-Quick-Login非常不适合普通项目的登录认证模块,STQL也无意去解决所有项目的登录认证模块
  • +
+

Sa-Token-Quick-Login的定位是这样的场景:你的项目需要一个登录认证功能、这个认证页面可以不华丽、可以烂,但是一定要有,同时你又不想花费太多的时间浪费在登录页面上,
+那么你便可以尝试一下Sa-Token-Quick-Login

+

三、集成步骤

+

首先我们需要创建一个SpringBoot的demo项目,比如:sa-token-demo-quick-login

+
1、添加pom依赖
+
<!-- Sa-Token 启动依赖 -->
+<dependency>
+        <groupId>cn.dev33</groupId>
+        <artifactId>sa-token-spring-boot-starter</artifactId>
+        <version>${sa.top.version}</version>
+</dependency>
+
+<!-- Sa-Token-Quick-Login 插件依赖 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-quick-login</artifactId>
+    <version>${sa.top.version}</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

Gradle 方式:

+
// Sa-Token 启动依赖
+implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'
+
+// Sa-Token-Quick-Login 插件
+implementation 'cn.dev33:sa-token-quick-login:${sa.top.version}'
  • 1
  • 2
  • 3
  • 4
  • 5
+
2、启动类
+
@SpringBootApplication
+public class SaTokenQuickDemoApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaTokenQuickDemoApplication.class, args);
+
+        System.out.println("\n------ 启动成功 ------");
+        System.out.println("name: " + SaQuickManager.getConfig().getName());
+        System.out.println("pwd:  " + SaQuickManager.getConfig().getPwd());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+
3、新建测试Controller
+
/**
+ * 测试专用Controller 
+ */
+@RestController
+public class TestController {
+    // 浏览器访问测试: http://localhost:8081
+    @RequestMapping({"/", "/index"})
+    public String index() {
+        String str = "<br />"
+                + "<h1 style='text-align: center;'>资源页 (登录后才可进入本页面) </h1>"
+                + "<hr/>"
+                + "<p style='text-align: center;'> Sa-Token " + SaTokenConsts.VERSION_NO + " </p>";
+        return str;
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

四、测试访问

+

启动项目,使用浏览器访问:http://localhost:8081,首次访问时,由于处于未登录状态,会被强制进入登录页面

+

登录

+

使用默认账号:sa / 123456进行登录,会看到资源页面

+

登录

+

五、可配置信息

+

你可以在yml中添加如下配置 (所有配置都是可选的)

+ + +
# Sa-Token-Quick-Login 配置
+sa: 
+    # 登录账号
+    name: sa
+    # 登录密码
+    pwd: 123456
+    # 是否自动随机生成账号密码 (此项为true时, name与pwd失效)
+    auto: false
+    # 是否开启全局认证(关闭后将不再强行拦截) 
+    auth: true
+    # 登录页标题
+    title: Sa-Token 登录
+    # 是否显示底部版权信息 
+    copr: true
+    # 指定拦截路径 
+    # include: /**
+    # 指定排除路径
+    # exclude: /1.jpg
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
+

六、使用独立jar包运行

+

使用sa-token-quick-login只需引入一个依赖即可为系统注入登录模块,现在我们更进一步,将这个项目打成一个独立的jar包。

+

通过这个jar包,我们可以方便的部署任意静态网站!做到真正的零编码注入登录功能。

+
打包步骤
+

首先放上懒人链接:sa-quick-dist.jar ,不想手动操作的同学可以直接点此链接下载打包后的jar文件

+

1、首先将 sa-token-demo-quick-login 模块添加到顶级父模块的<modules>节点中

+
<!-- 所有模块 -->
+<modules>
+    <module>sa-token-core</module>
+    <module>sa-token-starter</module>
+    <module>sa-token-plugin</module>
+    <module>sa-token-demo\sa-token-demo-quick-login</module>
+</modules>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

2、在项目根目录进入cmd执行打包命令

+
mvn clean package
  • 1
+

3、进入\sa-token-demo\sa-token-demo-quick-login\target 文件夹,找到打包好的jar文件

+
sa-token-demo-quick-login-0.0.1-SNAPSHOT.jar
  • 1
+

4、我们将其重命名为sa-quick-dist.jar,现在这个jar包就是我们的最终程序,我们在这个\target目录直接进入cmd,执行如下命令启动jar包

+
java -jar sa-quick-dist.jar
  • 1
+

5、测试访问,根据控制台输出提示,我们使用浏览器访问测试: http://localhost:8080

+

sa-quick-start

+

如果可以进入登录界面,则代表打包运行成功

+当然仅仅运行成功还不够,下面我们演示一下如何使用这个jar包进行静态网站部署

+

八、所有功能示例

+
Case 1. 指定静态资源路径
+
java -jar sa-quick-dist.jar --sa.dir file:E:\www
  • 1
+

使用dir参数指定E:\www目录作为资源目录进行部署 (现在我们可以通过浏览器访问E:\www目录下的文件了!)

+
Case 2. 指定登录名与密码
+
java -jar sa-quick-dist.jar --sa.name=zhang --sa.pwd=zhang123
  • 1
+

现在,默认的账号sa/123456将被废弃,而是使用zhang/zhang123进行账号校验

+
Case 3. 指定其自动生成账号密码
+
java -jar sa-quick-dist.jar --sa.auto=true
  • 1
+

每次启动时随机生成账号密码(会在启动成功时打印到控制台上)

+
Case 4. 指定登录页的标题
+
java -jar sa-quick-dist.jar --sa.title="XXX 系统登录"
  • 1
+
Case 5. 关闭账号校验,仅作为静态资源部署使用
+
java -jar sa-quick-dist.jar --sa.auth=false
  • 1
+
Case 6. 指定启动端口(默认8080)
+
java -jar sa-quick-dist.jar --server.port=80 
  • 1
+

注:所有参数可组合使用

+

九、使用SpringBoot默认资源路径

+

SpringBoot默认开放了一些路径作为资源目录,比如classpath:/static/
+怎么使用呢?我们只需要在jar包同目录创建一个\static文件夹,将静态资源文件复制到此目录下,然后启动jar包即可访问

+

同时,我们还可以在jar包同目录创建yml配置文件,来覆盖jar包内的yml配置,如下图所示:

+

sa-quick-case.png

+

例如如上目录中/static中有一个1.jpg文件,我们启动jar包后访问http://localhost:8080/1.jpg即可查看到此文件,这是Springboot自带的功能,在此不再赘述

+
+

参考资料

+ +
+← 一文详解 Sa-Token 中的 SaSession 对象 +使用 Sa-Token 完成踢人下线功能 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/dui-min-gan-cao-zuo-de-er-ci-ren-zheng-xiang-jie-sa-token-er-ji-ren-zheng.html b/sa-token-doc/blog/essays/dui-min-gan-cao-zuo-de-er-ci-ren-zheng-xiang-jie-sa-token-er-ji-ren-zheng.html new file mode 100644 index 000000000..158be527b --- /dev/null +++ b/sa-token-doc/blog/essays/dui-min-gan-cao-zuo-de-er-ci-ren-zheng-xiang-jie-sa-token-er-ji-ren-zheng.html @@ -0,0 +1,512 @@ + + + + + 对敏感操作的二次认证 —— 详解 Sa-Token 二级认证 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

对敏感操作的二次认证 —— 详解 Sa-Token 二级认证

+ +

一、需求分析

+

在某些敏感操作下,我们需要对已登录的会话进行二次验证。

+

比如代码托管平台的仓库删除操作,尽管我们已经登录了账号,当我们点击 [删除] 按钮时,还是需要再次输入一遍密码,这么做主要为了两点:

+
    +
  1. 保证操作者是当前账号本人。
  2. +
  3. 增加操作步骤,防止误删除重要数据。
  4. +
+

这就是我们本篇要讲的 —— 二级认证,即:在已登录会话的基础上,进行再次验证,提高会话的安全性。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

本文将介绍在 SpringBoot 架构下,如何使用 Sa-Token 完成二级认证操作。

+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+

二、具体API

+

Sa-Token中进行二级认证非常简单,只需要使用以下API:

+
// 在当前会话 开启二级认证,时间为120秒
+StpUtil.openSafe(120); 
+
+// 获取:当前会话是否处于二级认证时间内
+StpUtil.isSafe(); 
+
+// 检查当前会话是否已通过二级认证,如未通过则抛出异常
+StpUtil.checkSafe(); 
+
+// 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
+StpUtil.getSafeTime(); 
+
+// 在当前会话 结束二级认证
+StpUtil.closeSafe(); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

三、一个小示例

+

一个完整的二级认证业务流程,应该大致如下:

+
package com.pj.cases.up;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * Sa-Token 二级认证示例 
+ * 
+ * @author kong
+ * @since 2022-10-16 
+ */
+@RestController
+@RequestMapping("/safe/")
+public class SafeAuthController {
+
+    // 删除仓库    ---- http://localhost:8081/safe/deleteProject
+    @RequestMapping("deleteProject")
+    public SaResult deleteProject(String projectId) {
+        // 第1步,先检查当前会话是否已完成二级认证 
+        //      这个地方既可以通过 StpUtil.isSafe() 手动判断,
+        //      也可以通过 StpUtil.checkSafe() 或者 @SaCheckSafe 来校验(校验不通过时将抛出 NotSafeException 异常)
+        if(!StpUtil.isSafe()) {
+            return SaResult.error("仓库删除失败,请完成二级认证后再次访问接口");
+        }
+
+        // 第2步,如果已完成二级认证,则开始执行业务逻辑
+        // ... 
+
+        // 第3步,返回结果 
+        return SaResult.ok("仓库删除成功"); 
+    }
+
+    // 提供密码进行二级认证    ---- http://localhost:8081/safe/openSafe?password=123456
+    @RequestMapping("openSafe")
+    public SaResult openSafe(String password) {
+        // 比对密码(此处只是举例,真实项目时可拿其它参数进行校验)
+        if("123456".equals(password)) {
+
+            // 比对成功,为当前会话打开二级认证,有效期为120秒,意为在120秒内再调用 deleteProject 接口都无需提供密码 
+            StpUtil.openSafe(120);
+            return SaResult.ok("二级认证成功");
+        }
+
+        // 如果密码校验失败,则二级认证也会失败
+        return SaResult.error("二级认证失败"); 
+    }
+
+    // 手动关闭二级认证    ---- http://localhost:8081/safe/closeSafe
+    @RequestMapping("closeSafe")
+    public SaResult closeSafe() {
+        StpUtil.closeSafe();
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
+

全局异常拦截器,统一返回给前端的格式,参考:

+
@RestControllerAdvice
+public class GlobalExceptionHandler {
+    // 全局异常拦截 
+    @ExceptionHandler
+    public SaResult handlerException(Exception e) {
+        e.printStackTrace(); 
+        return SaResult.error(e.getMessage());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
+

前提:二级认证时我们必须处于登录状态(可参考之前的登录认证章节代码),完成登录后,调用接口进行二级认证校验:

+
    +
  1. 前端调用 deleteProject 接口,尝试删除仓库。 ---- http://localhost:8081/safe/deleteProject
  2. +
  3. 后端校验会话尚未完成二级认证,返回: 仓库删除失败,请完成二级认证后再次访问接口。
  4. +
  5. 前端将信息提示给用户,用户输入密码,调用 openSafe 接口。 ---- http://localhost:8081/safe/openSafe?password=123456
  6. +
  7. 后端比对用户输入的密码,完成二级认证,有效期为:120秒。
  8. +
  9. 前端在 120 秒内再次调用 deleteProject 接口,尝试删除仓库。 ---- http://localhost:8081/safe/deleteProject
  10. +
  11. 后端校验会话已完成二级认证,返回:仓库删除成功。
  12. +
+

四、指定业务标识进行二级认证

+

如果项目有多条业务线都需要敏感操作验证,则 StpUtil.openSafe() 无法提供细粒度的认证操作,
+此时我们可以指定一个业务标识来分辨不同的业务线:

+
// 在当前会话 开启二级认证,业务标识为client,时间为600秒
+StpUtil.openSafe("client", 600); 
+
+// 获取:当前会话是否已完成指定业务的二级认证 
+StpUtil.isSafe("client"); 
+
+// 校验:当前会话是否已完成指定业务的二级认证 ,如未认证则抛出异常
+StpUtil.checkSafe("client"); 
+
+// 获取当前会话指定业务二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
+StpUtil.getSafeTime("client"); 
+
+// 在当前会话 结束指定业务标识的二级认证
+StpUtil.closeSafe("client"); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

业务标识可以填写任意字符串,不同业务标识之间的认证互不影响,比如:

+
// 打开了业务标识为 client 的二级认证 
+StpUtil.openSafe("client"); 
+
+// 判断是否处于 shop 的二级认证,会返回 false 
+StpUtil.isSafe("shop");  // 返回 false 
+
+// 也不会通过校验,会抛出异常 
+StpUtil.checkSafe("shop"); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

代码示例:

+
package com.pj.cases.up;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * Sa-Token 二级认证示例 
+ * 
+ * @author kong
+ * @since 2022-10-16 
+ */
+@RestController
+@RequestMapping("/safe/")
+public class SafeAuthController {
+
+    // ------------------ 指定业务类型进行二级认证 
+
+    // 获取应用秘钥    ---- http://localhost:8081/safe/getClientSecret
+    @RequestMapping("getClientSecret")
+    public SaResult getClientSecret() {
+        // 第1步,先检查当前会话是否已完成 client业务 的二级认证 
+        StpUtil.checkSafe("client");
+
+        // 第2步,如果已完成二级认证,则返回数据 
+        return SaResult.data("aaaa-bbbb-cccc-dddd-eeee");
+    }
+
+    // 提供手势密码进行二级认证    ---- http://localhost:8081/safe/openClientSafe?gesture=35789
+    @RequestMapping("openClientSafe")
+    public SaResult openClientSafe(String gesture) {
+        // 比对手势密码(此处只是举例,真实项目时可拿其它参数进行校验)
+        if("35789".equals(gesture)) {
+
+            // 比对成功,为当前会话打开二级认证:
+            // 业务类型为:client 
+            // 有效期为600秒==10分钟,意为在10分钟内,调用 getClientSecret 时都无需再提供手势密码 
+            StpUtil.openSafe("client", 600);
+            return SaResult.ok("二级认证成功");
+        }
+
+        // 如果密码校验失败,则二级认证也会失败
+        return SaResult.error("二级认证失败"); 
+    }
+
+    // 查询当前会话是否已完成指定的二级认证    ---- http://localhost:8081/safe/isClientSafe
+    @RequestMapping("isClientSafe")
+    public SaResult isClientSafe() {
+        return SaResult.ok("当前是否已完成 client 二级认证:" + StpUtil.isSafe("client")); 
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
+

五、使用注解进行二级认证

+

在一个方法上使用 @SaCheckSafe 注解,可以在代码进入此方法之前进行一次二级认证校验

+
// 二级认证:必须二级认证之后才能进入该方法 
+@SaCheckSafe      
+@RequestMapping("add")
+public String add() {
+    return "用户增加";
+}
+
+// 指定业务类型,进行二级认证校验
+@SaCheckSafe("art")
+@RequestMapping("add2")
+public String add2() {
+    return "文章增加";
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

标注 @SaCheckSafe 的方法必须在二级认证后才能访问成功,其效果与代码 StpUtil.checkSafe() 等同。

+
+

参考资料

+ +
+← 在微服务中使用 Sa-Token:分布式Session会话、网关统一鉴权 +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/qian-hou-duan-fen-li-jia-gou-xia-shi-yong-sa-token-wan-cheng-deng-lu-ren-zheng.html b/sa-token-doc/blog/essays/qian-hou-duan-fen-li-jia-gou-xia-shi-yong-sa-token-wan-cheng-deng-lu-ren-zheng.html new file mode 100644 index 000000000..38fe1e3f7 --- /dev/null +++ b/sa-token-doc/blog/essays/qian-hou-duan-fen-li-jia-gou-xia-shi-yong-sa-token-wan-cheng-deng-lu-ren-zheng.html @@ -0,0 +1,515 @@ + + + + + 前后端分离架构下使用 Sa-Token 完成登录认证 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

前后端分离架构下使用 Sa-Token 完成登录认证

+ +

一、架构分析

+

目前绝大多数系统都已经采用 “前后端分离” 架构来设计了,传统的Session模式鉴权也不再适合这种架构(或者需要额外写很多的代码来专门适配)。

+

Sa-Token 是一个 java 轻量级权限认证框架,专为前后端分离架构打造,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。

+
+

Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

本文将介绍在 Springboot 架构下的前后端分离项目,如何使用 Sa-Token 方便的完成登录认证。

+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+ +

无 Cookie 模式:特指不支持 Cookie 功能的终端,通俗来讲就是我们常说的 —— 前后端分离模式

+

常规 Web 端鉴权方法,一般由 Cookie模式 完成,而 Cookie 有两个特性:
+1. 可由后端控制写入。
+2. 每次请求自动提交。

+

这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)

+而在app、小程序等前后端分离场景中,一般是没有 Cookie 这一功能的,此时大多数人都会一脸懵逼,咋进行鉴权啊?

+

见招拆招,其实答案很简单:
+- 不能后端控制写入了,就前端自己写入。(难点在后端如何将 Token 传递到前端
+- 每次请求不能自动提交了,那就手动提交。(难点在前端如何将 Token 传递到后端,同时后端将其读取出来

+

三、后端将 token 返回到前端

+
    +
  1. 首先调用 StpUtil.login(id) 进行登录。
  2. +
  3. 调用 StpUtil.getTokenInfo() 返回当前会话的 token 详细参数。
      +
    • 此方法返回一个对象,其有两个关键属性:tokenNametokenValue(token 的名称和 token 的值)。
    • +
    • 将此对象传递到前台,让前端人员将这两个值保存到本地。
    • +
    +
  4. +
+

代码示例:

+
// 登录接口
+@RequestMapping("doLogin")
+public SaResult doLogin() {
+    // 第1步,先登录上 
+    StpUtil.login(10001);
+    // 第2步,获取 Token  相关参数 
+    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
+    // 第3步,返回给前端 
+    return SaResult.data(tokenInfo);
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

四、前端将 token 提交到后端

+
    +
  1. 无论是app还是小程序,其传递方式都大同小异。
  2. +
  3. 那就是,将 token 塞到请求header里 ,格式为:{tokenName: tokenValue}
  4. +
  5. 以经典跨端框架 uni-app 为例:
  6. +
+

方式1,简单粗暴

+
// 1、首先在登录时,将 tokenValue 存储在本地,例如:
+uni.setStorageSync('tokenValue', tokenValue);
+
+// 2、在发起ajax请求的地方,获取这个值,并塞到header里 
+uni.request({
+    url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。
+    header: {
+        "content-type": "application/x-www-form-urlencoded",
+        "satoken": uni.getStorageSync('tokenValue')     // 关键代码, 注意参数名字是 satoken 
+    },
+    success: (res) => {
+        console.log(res.data);  
+    }
+});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

方式2,更加灵活

+
// 1、首先在登录时,将tokenName和tokenValue一起存储在本地,例如:
+uni.setStorageSync('tokenName', tokenName); 
+uni.setStorageSync('tokenValue', tokenValue); 
+
+// 2、在发起ajax的地方,获取这两个值, 并组织到head里 
+var tokenName = uni.getStorageSync('tokenName');    // 从本地缓存读取tokenName值
+var tokenValue = uni.getStorageSync('tokenValue');  // 从本地缓存读取tokenValue值
+var header = {
+    "content-type": "application/x-www-form-urlencoded"
+};
+if (tokenName != undefined && tokenName != '') {
+    header[tokenName] = tokenValue;
+}
+
+// 3、后续在发起请求时将 header 对象塞到请求头部 
+uni.request({
+    url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。
+    header: header,
+    success: (res) => {
+        console.log(res.data);  
+    }
+});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
+
    +
  1. 只要按照如此方法将token值传递到后端,Sa-Token 就能像传统PC端一样自动读取到 token 值,进行鉴权。
  2. +
  3. 你可能会有疑问,难道我每个ajax都要写这么一坨?岂不是麻烦死了?
      +
    • 你当然不能每个 ajax 都写这么一坨,因为这种重复性代码都是要封装在一个函数里统一调用的。
    • +
    +
  4. +
+

其它解决方案:

+

如果你对 Cookie 非常了解,那你就会明白,所谓 Cookie ,本质上就是一个特殊的header参数而已,
+而既然它只是一个 header 参数,我们就能手动模拟实现它,从而完成鉴权操作。

+

这其实是对无Cookie模式的另一种解决方案,有兴趣的同学可以百度了解一下,在此暂不赘述。

+

五、代码对比

+

为了更加直观的显示出 前后端一体架构 和 前后端分离架构 的差异,此处再提供一个示例:

+
package com.pj.cases.up;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.dev33.satoken.stp.SaTokenInfo;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * Sa-Token 前后端分离模式示例 
+ * 
+ * @author kong
+ * @since 2022-10-17 
+ */
+@RestController
+@RequestMapping("/NotCookie/")
+public class NotCookieController {
+
+    // 前后端一体模式的登录样例    ---- http://localhost:8081/NotCookie/doLogin?name=zhang&pwd=123456
+    @RequestMapping("doLogin")
+    public SaResult doLogin(String name, String pwd) {
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+            // 会话登录 
+            StpUtil.login(10001);
+            return SaResult.ok();
+        }
+        return SaResult.error("登录失败");
+    }
+
+    // 前后端分离模式的登录样例    ---- http://localhost:8081/NotCookie/doLogin2?name=zhang&pwd=123456
+    @RequestMapping("doLogin2")
+    public SaResult doLogin2(String name, String pwd) {
+
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+
+            // 会话登录 
+            StpUtil.login(10001);
+
+            // 与常规登录不同点之处:这里需要把 Token 信息从响应体中返回到前端 
+            SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
+            return SaResult.data(tokenInfo);
+        }
+        return SaResult.error("登录失败");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
+
    +
  • 接口一:Token 将在 Cookie 上下文返回到前端,并由浏览器每次请求时自动提交,这种模式适合前后端一体的架构。
  • +
  • 接口二:Token 将在响应 body 里返回到前端,并由前端手动存储,并手动在每次请求时提交,这种模式适合前后端分离的架构。
  • +
+

六、自定义 Token 提交的前缀

+

在某些系统中,前端提交token时会在前面加个固定的前缀,例如:

+
{
+    "satoken": "Bearer xxxx-xxxx-xxxx-xxxx"
+}
  • 1
  • 2
  • 3
+

此时后端如果不做任何特殊处理,框架将会把Bearer视为token的一部分,无法正常读取token信息,导致鉴权失败。

+

为此,我们需要在yml中添加如下配置:

+
sa-token: 
+    # token前缀
+    token-prefix: Bearer
  • 1
  • 2
  • 3
+

此时 Sa-Token 便可在读取 Token 时裁剪掉 Bearer,成功获取xxxx-xxxx-xxxx-xxxx

+

注意点:

+
    +
  1. Token前缀 与 Token值 之间必须有一个空格。
  2. +
  3. 一旦配置了 Token前缀,则前端提交 Token 时,必须带有前缀,否则会导致框架无法读取 Token。
  4. +
  5. 由于Cookie中无法存储空格字符,也就意味配置 Token 前缀后,Cookie 鉴权方式将会失效,此时只能将 Token 提交到header里进行传输。
  6. +
+

七、自定义 Token 风格

+

Sa-Token默认的token生成策略是uuid风格,其模样类似于:623368f0-ae5e-4475-a53f-93e4225f16ae

+如果你对这种风格不太感冒,还可以将token生成设置为其他风格。

+

怎么设置呢?只需要在yml配置文件里设置 sa-token.token-style=风格类型 即可,其有多种取值:

+
// 1. token-style=uuid    —— uuid风格 (默认风格)
+"623368f0-ae5e-4475-a53f-93e4225f16ae"
+
+// 2. token-style=simple-uuid    —— 同上,uuid风格, 只不过去掉了中划线
+"6fd4221395024b5f87edd34bc3258ee8"
+
+// 3. token-style=random-32    —— 随机32位字符串
+"qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W"
+
+// 4. token-style=random-64    —— 随机64位字符串
+"v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc"
+
+// 5. token-style=random-128    —— 随机128位字符串
+"nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj"
+
+// 6. token-style=tik    —— tik风格
+"gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
+

八、自定义 Token 生成策略

+

如果你觉着以上风格都不是你喜欢的类型,那么你还可以自定义token生成策略,来定制化token生成风格。

+

怎么做呢?只需要重写 SaStrategy 策略类的 createToken 算法即可:

+

参考步骤如下:

+

1、在SaTokenConfigure配置类中添加代码:

+
@Configuration
+public class SaTokenConfigure {
+    /**
+     * 重写 Sa-Token 框架内部算法策略 
+     */
+    @Autowired
+    public void rewriteSaStrategy() {
+        // 重写 Token 生成策略 
+        SaStrategy.me.createToken = (loginId, loginType) -> {
+            return SaFoxUtil.getRandomString(60);   // 随机60位长度字符串
+        };
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

2、再次调用 StpUtil.login(10001)方法进行登录,观察其生成的token样式:

+
gfuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH
  • 1
+
+

参考资料

+ +
+← 别人绕过我的网关直接调用资源服务怎么办?使用 Sa-Token 解决:网关转发鉴权、RPC调用鉴权 +在微服务中使用 Sa-Token:分布式Session会话、网关统一鉴权 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-er-pian-tong-yu-tong-redis-xia-de-sso-dui-jie-bu-zh.html b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-er-pian-tong-yu-tong-redis-xia-de-sso-dui-jie-bu-zh.html new file mode 100644 index 000000000..1ef3ef783 --- /dev/null +++ b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-er-pian-tong-yu-tong-redis-xia-de-sso-dui-jie-bu-zh.html @@ -0,0 +1,519 @@ + + + + + Sa-Token 单点登录第二篇:同域、同 Redis 下的 SSO 对接步骤 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 单点登录第二篇:同域、同 Redis 下的 SSO 对接步骤

+ +

大家好,由于单篇博客长度有限,我将针对 SSO 单点登录 开启一个系列文章,此为第二篇,想完整的了解 SSO 技术的同学欢迎点开我的账号动态参考本系列历史文章。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录 —— 便是我们搭建统一认证中心的关键。

+

如果我们的多个系统可以做到:前端同域、后端同Redis,那么便可以使用 SSO 模式一:[共享Cookie同步会话] 的方式做到单点登录。

+

一、设计思路

+

首先我们分析一下多个系统之间,为什么无法同步登录状态?
+1. 前端的 Token 无法在多个系统下共享。
+2. 后端的 Session 无法在多个系统间共享。

+

所以单点登录第一招,就是对症下药:
+1. 使用 共享Cookie 来解决 Token 共享问题。
+2. 使用 Redis 来解决 Session 共享问题。

+

所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名stp.com下的Cookie,在s1.stp.coms2.stp.com等子域名都是可以共享访问的。

+

而共享Redis,并不需要我们把所有项目的数据都放在同一个Redis中,Sa-Token提供了 [权限缓存与业务缓存分离] 的解决方案:Alone独立Redis插件。

+

动态演示图:

+

SSO 模式一

+

OK,所有理论就绪,下面开始实战:

+

二、准备工作

+

首先修改hosts文件(C:\windows\system32\drivers\etc\hosts),添加以下IP映射,方便我们进行测试:

+
127.0.0.1 sso.stp.com
+127.0.0.1 s1.stp.com
+127.0.0.1 s2.stp.com
+127.0.0.1 s3.stp.com
  • 1
  • 2
  • 3
  • 4
+

其中:sso.stp.com为统一认证中心地址,当用户在其它 Client 端发起登录请求时,均将其重定向至认证中心,待到登录成功之后再原路返回到 Client 端。

+ +

sso.stp.com访问服务器,其Cookie也只能写入到sso.stp.com下,为了将Cookie写入到其父级域名stp.com下,我们需要更改 SSO-Server 端的 yml 配置:

+ + + +
sa-token: 
+    cookie: 
+        # 配置 Cookie 作用域 
+        domain: stp.com 
  • 1
  • 2
  • 3
  • 4
+ + +
# 配置 Cookie 作用域 
+sa-token.cookie.domain=stp.com
  • 1
  • 2
+ + +

这个配置原本是被注释掉的,现在将其打开。另外我们格外需要注意:
+在SSO模式一测试完毕之后,一定要将这个配置再次注释掉,因为模式一与模式二三使用不同的授权流程,这行配置会影响到我们模式二和模式三的正常运行。

+

四、搭建 Client 端项目

+
+

搭建示例在 Sa-Token 官方仓库的 /sa-token-demo/sa-token-demo-sso1-client/,如遇到难点可结合源码进行测试学习。

+
+

4.1、引入依赖

+

新建项目 sa-token-demo-sso1-client,并添加以下依赖:

+ + + +
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.com -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<!-- Sa-Token 插件:整合SSO -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-sso</artifactId>
+    <version>1.34.0</version>
+</dependency>
+
+<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-dao-redis-jackson</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.commons</groupId>
+    <artifactId>commons-pool2</artifactId>
+</dependency>
+
+<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-alone-redis</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
+

Gradle 方式:

+
// Sa-Token 权限认证,在线文档:https://sa-token.com
+implementation 'cn.dev33:sa-token-spring-boot-starter:1.34.0'
+
+// Sa-Token 插件:整合SSO
+implementation 'cn.dev33:sa-token-sso:1.34.0'
+
+// Sa-Token 整合 Redis (使用 jackson 序列化方式)
+implementation 'cn.dev33:sa-token-dao-redis-jackson:1.34.0'
+implementation 'org.apache.commons:commons-pool2'
+
+// Sa-Token插件:权限缓存与业务缓存分离
+implementation 'cn.dev33:sa-token-alone-redis:1.34.0'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+ + +

4.2、新建 Controller 控制器

+
/**
+ * Sa-Token-SSO Client端 Controller 
+ * @author kong
+ */
+@RestController
+public class SsoClientController {
+
+    // SSO-Client端:首页 
+    @RequestMapping("/")
+    public String index() {
+        String authUrl = SaSsoManager.getConfig().splicingAuthUrl();
+        String solUrl = SaSsoManager.getConfig().splicingSloUrl();
+        String str = "<h2>Sa-Token SSO-Client 应用端</h2>" + 
+                    "<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" + 
+                    "<p><a href=\"javascript:location.href='" + authUrl + "?mode=simple&redirect=' + encodeURIComponent(location.href);\">登录</a> " + 
+                    "<a href=\"javascript:location.href='" + solUrl + "?back=' + encodeURIComponent(location.href);\">注销</a> </p>";
+        return str;
+    }
+
+    // 全局异常拦截 
+    @ExceptionHandler
+    public SaResult handlerException(Exception e) {
+        e.printStackTrace(); 
+        return SaResult.error(e.getMessage());
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
+

4.3、application.yml 配置

+ + + +
# 端口
+server:
+    port: 9001
+
+# Sa-Token 配置 
+sa-token: 
+    # SSO-相关配置
+    sso: 
+        # SSO-Server端-单点登录授权地址 
+        auth-url: http://sso.stp.com:9000/sso/auth
+        # SSO-Server端-单点注销地址
+        slo-url: http://sso.stp.com:9000/sso/signout
+
+    # 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
+    alone-redis: 
+        # Redis数据库索引
+        database: 1
+        # Redis服务器地址
+        host: 127.0.0.1
+        # Redis服务器连接端口
+        port: 6379
+        # Redis服务器连接密码(默认为空)
+        password: 
+        # 连接超时时间
+        timeout: 10s
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
+

如果是 application.properties 风格:

+
# 端口
+server.port=9001
+
+######### Sa-Token 配置 #########
+
+# SSO-Server端-单点登录授权地址 
+sa-token.sso.auth-url=http://sso.stp.com:9000/sso/auth
+# SSO-Server端-单点注销地址
+sa-token.sso.slo-url=http://sso.stp.com:9000/sso/signout
+
+# 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
+# Redis数据库索引
+sa-token.alone-redis.database=1
+# Redis服务器地址
+sa-token.alone-redis.host=127.0.0.1
+# Redis服务器连接端口
+sa-token.alone-redis.port=6379
+# Redis服务器连接密码(默认为空)
+sa-token.alone-redis.password=
+# 连接超时时间
+sa-token.alone-redis.timeout=10s
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
+ + +

4.4、启动类

+
/**
+ * SSO模式一,Client端 Demo 
+ */
+@SpringBootApplication
+public class SaSso1ClientApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaSso1ClientApplication.class, args);
+        System.out.println("\nSa-Token SSO模式一 Client端启动成功");
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

五、访问测试

+

启动项目,依次访问三个应用端:

+
+
    +
  • http://s1.stp.com:9001/
  • +
  • http://s2.stp.com:9001/
  • +
  • http://s3.stp.com:9001/
  • +
+
+

均返回:

+

sso1--index.png

+

然后点击登录,被重定向至SSO认证中心:

+

sso1--login-page2.png

+

我们点击登录,然后刷新页面:

+

sso1-login-ok.png

+

刷新另外两个Client端,均显示已登录

+

sso1-login-ok2.png

+

测试完成

+

六、下篇预告:跨域模式下的解决方案

+

如上,我们使用简单的步骤实现了同域下的单点登录,聪明如你😏,马上想到了这种模式有着一个不小的限制:

+
+

所有子系统的域名,必须同属一个父级域名

+
+

如果我们的子系统在完全不同的域名下,我们又该怎么完成单点登录功能呢?

+

这就是下篇博客要介绍的 SSO 模式二:跨域、同 Redis 下的 SSO 对接步骤。

+
+

参考资料

+ +
+← Sa-Token 单点登录第三篇:跨域、同 Redis 下的 SSO 对接步骤 +Sa-Token 单点登录第五篇:前后端分离架构下的 SSO 对接步骤 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-liu-pian-ding-zhi-hua-deng-lu-ye-mian.html b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-liu-pian-ding-zhi-hua-deng-lu-ye-mian.html new file mode 100644 index 000000000..47d1afdcd --- /dev/null +++ b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-liu-pian-ding-zhi-hua-deng-lu-ye-mian.html @@ -0,0 +1,497 @@ + + + + + Sa-Token 单点登录第六篇:定制化登录页面 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 单点登录第六篇:定制化登录页面

+ +

大家好,由于单篇博客长度有限,我将针对 SSO 单点登录 开启一个系列文章,此为第六篇,想完整的了解 SSO 技术的同学欢迎点开我的账号动态参考本系列历史文章。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

本篇将介绍在集成 Sa-Token SSO 模块时,如何自定义登录页面等信息。

+

一、何时引导用户去登录?

+

方案一:前端按钮跳转

+

前端页面准备一个[登录]按钮,当用户点击按钮时,跳转到登录接口

+
<a href="javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);">登录</a>
  • 1
+

方案二:后端拦截重定向

+

在后端注册全局过滤器(或拦截器、或全局异常处理),拦截需要登录后才能访问的页面资源,将未登录的访问重定向至登录接口

+
/**
+ * Sa-Token 配置类 
+ */
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    /** 注册 [Sa-Token全局过滤器] */
+    @Bean
+    public SaServletFilter getSaServletFilter() {
+        return new SaServletFilter()
+                .addInclude("/**")
+                .addExclude("/sso/*", "/favicon.ico")
+                .setAuth(obj -> {
+                    if(StpUtil.isLogin() == false) {
+                        String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());
+                        SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back));
+                        SaRouter.back();
+                    }
+                })
+                ;
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
+

方案三:后端拦截 + 前端跳转

+

首先,后端仍需要提供拦截,但是不直接引导用户重定向,而是返回未登录的提示信息

+
/**
+ * Sa-Token 配置类 
+ */
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    /** 注册 [Sa-Token全局过滤器] */
+    @Bean
+    public SaServletFilter getSaServletFilter() {
+        return new SaServletFilter()
+                .addInclude("/**")
+                .addExclude("/sso/*", "/favicon.ico")
+                .setAuth(obj -> {
+                    if(StpUtil.isLogin() == false) {
+                        // 与前端约定好,code=401时代表会话未登录 
+                        SaRouter.back(SaResult.ok().setCode(401));
+                    }
+                })
+                ;
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
+

前端接受到返回结果 code=401 时,开始跳转至登录接口

+
if(res.code == 401) {
+    location.href = '/sso/login?back=' + encodeURIComponent(location.href);
+}
  • 1
  • 2
  • 3
+

这种方案比较适合以 Ajax 访问的 RestAPI 接口重定向

+

二、如何自定义登录视图?

+

方式一:在demo示例中直接更改 login.html 页面代码即可

+

方式二:在配置中配置登录视图地址

+
// 配置:未登录时返回的View 
+sso.setNotLoginView(() -> {
+    return new ModelAndView("xxx.html");
+})
  • 1
  • 2
  • 3
  • 4
+

三、如何自定义登录API的接口地址?

+

根据需求点选择解决方案:

+

3.1、如果只是想在 setDoLoginHandle 函数里获取除 name、pwd 以外的参数?

+
// 在任意代码处获取前端提交的参数 
+String xxx = SaHolder.getRequest().getParam("xxx");
  • 1
  • 2
+

3.2、想完全自定义一个接口来接受前端登录请求?

+
// 直接定义一个拦截路由为 `/sso/doLogin` 的接口即可 
+@RequestMapping("/sso/doLogin")
+public SaResult ss(String name, String pwd) {
+    System.out.println("------ 请求进入了自定义的API接口 ---------- ");
+    if("sa".equals(name) && "123456".equals(pwd)) {
+        StpUtil.login(10001);
+        return SaResult.ok("登录成功!");
+    }
+    return SaResult.error("登录失败!");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

3.3、不想使用/sso/doLogin这个接口,想自定义一个API地址?

+

答:直接在前端更改点击按钮时 Ajax 的请求地址即可

+

四、如何自定义 API 路由

+

方式一:修改全局变量

+

在之前的章节中,我们演示了如何搭建一个SSO认证中心:

+
/**
+ * Sa-Token-SSO Server端 Controller 
+ */
+@RestController
+public class SsoServerController {
+
+    // SSO-Server端:处理所有SSO相关请求 
+    @RequestMapping("/sso/*")
+    public Object ssoRequest() {
+        return SaSsoProcessor.instance.serverDister();
+    }
+
+    // ... 其它代码
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

这种写法集成简单但却不够灵活。例如认证中心地址只能是:http://{host}:{port}/sso/auth,如果我们想要自定义其API地址,应该怎么做呢?

+

打开SSO模块相关源码,有关 API 的设计都定义在:
+ApiName.java
+中,我们可以对其进行二次修改。

+

例如,我们可以在 Main 方法启动类或者 SSO 配置方法中修改变量值:

+
// 配置SSO相关参数 
+@Autowired
+private void configSso(SaSsoConfig sso) {
+    // 自定义API地址
+    SaSsoUtil.ssoTemplate.apiName.ssoAuth = "/sso/auth2";
+    // ... 
+
+    // SSO 相关配置
+    sso.setXxx ... ;
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

启动项目,统一认证地址就被我们修改成了:http://{host}:{port}/sso/auth2

+

方式二:拆分路由入口

+

根据上述路由入口:@RequestMapping("/sso/*"),我们给它起一个合适的名字 —— 聚合式路由。

+

与之对应的,我们可以将其修改为拆分式路由:

+
/**
+ * Sa-Token-SSO Server端 Controller 
+ */
+@RestController
+public class SsoServerController {
+
+    // SSO-Server:统一认证地址 
+    @RequestMapping("/sso/auth")
+    public Object ssoAuth() {
+        return SaSsoProcessor.instance.ssoAuth();
+    }
+
+    // SSO-Server:RestAPI 登录接口 
+    @RequestMapping("/sso/doLogin")
+    public Object ssoDoLogin() {
+        return SaSsoProcessor.instance.ssoDoLogin();
+    }
+
+    // SSO-Server:校验ticket 获取账号id 
+    @RequestMapping("/sso/checkTicket")
+    public Object ssoCheckTicket() {
+        return SaSsoProcessor.instance.ssoCheckTicket();
+    }
+
+    // SSO-Server:单点注销 
+    @RequestMapping("/sso/signout")
+    public Object ssoSignout() {
+        return SaSsoProcessor.instance.ssoSignout();
+    }
+
+    // ... 其它方法 
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
+

拆分式路由 与 聚合式路由 在功能上完全等价,且提供了更为细致的路由管控。

+

五、易错点

+

易错点一:搭建好 sso-serversso-client 服务后,访问返回:{"msg": "not handle"}

+

返回这个信息,代表你访问的路由有错误,比如说:

+
    +
  • 统一认证登录地址是:http://{host}:{port}/sso/auth
  • +
  • 而你访问的却是:http://{host}:{port}/sso/auth2
  • +
+

地址写错了,框架就不会处理这个请求,会直接返回 {"msg": "not handle"},所有开放地址可参考:SSO 开放接口

+

如果仔细检查地址后没有写错,却依然返回了这个信息,那有可能是对应的接口没有打开,比如说:

+
    +
  • sso-server 端的单点注销地址:http://{host}:{port}/sso/signout
  • +
  • sso-client 端的注销地址:http://{host}:{port}/sso/logout
  • +
+

都需要在配置文件配置:sa-token.sso.is-slo=true后,才会打开。

+

易错点二:参照文档搭建SSO-Client,一直提示:Ticket无效,这是怎么回事?

+

如果使用的是模式二,出现此异常概率最大的原因是因为 ClientServer 没有连接同一个Redis,SSO模式二中两者必须连接同一个 Redis 才可以登录成功。

+

你可能会问:我看配置文件明明是同一个啊?

+

我的建议是:排查时不要仅凭肉眼判断,分别在你的 ClientServer 启动后调用 SaManager.getSaTokenDao().set("name", "value", 100000);
+随便写入一个值,看看能不能根据你的预期写进同一个Redis里,如果能的话才能证明 ClientServer 连接的Reids 是同一个,再进行下一步排查。

+
@SpringBootApplication
+public class SaSsoServerApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaSsoServerApplication.class, args);
+        System.out.println("\n------ Sa-Token-SSO 统一认证中心启动成功 ");
+        // 分别在 Client 与 Server 启动后调用 set 数据代码,看看能否根据预期写入同一个 reids 
+        SaManager.getSaTokenDao().set("name", "value", 100000);
+    }   
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
+

如果使用的是模式三,则排查是否有重复校验 ticket 的代码,一个 ticket 码只能使用一次,多次重复使用就会提示这个。

+
+

参考资料

+ +
+← Sa-Token 单点登录第五篇:前后端分离架构下的 SSO 对接步骤 +Sa-Token 单点登录第四篇:跨域、跨 Redis 下的 SSO 对接步骤 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-san-pian-kua-yu-tong-redis-xia-de-sso-dui-jie-bu-zh.html b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-san-pian-kua-yu-tong-redis-xia-de-sso-dui-jie-bu-zh.html new file mode 100644 index 000000000..0e92a6c8c --- /dev/null +++ b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-san-pian-kua-yu-tong-redis-xia-de-sso-dui-jie-bu-zh.html @@ -0,0 +1,596 @@ + + + + + Sa-Token 单点登录第三篇:跨域、同 Redis 下的 SSO 对接步骤 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 单点登录第三篇:跨域、同 Redis 下的 SSO 对接步骤

+ +

大家好,由于单篇博客长度有限,我将针对 SSO 单点登录 开启一个系列文章,此为第三篇,想完整的了解 SSO 技术的同学欢迎点开我的账号动态参考本系列历史文章。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录 —— 便是我们搭建统一认证中心的关键。

+

如果我们的多个系统:部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 SSO 模式二:[URL重定向传播会话] 的方式做到单点登录。

+

一、设计思路

+

首先我们再次复习一下,多个系统之间为什么无法同步登录状态?

+
    +
  1. 前端的Token无法在多个系统下共享。
  2. +
  3. 后端的Session无法在多个系统间共享。
  4. +
+

关于第二点,我们已在之前的章节 "SSO模式一" 章节中阐述,使用 Alone独立Redis插件 做到权限缓存直连 SSO-Redis 数据中心,在此不再赘述。

+

而第一点,才是我们解决问题的关键所在,在跨域模式下,意味着 "共享Cookie方案" 的失效,我们必须采用一种新的方案来传递Token。

+
    +
  1. 用户在 子系统 点击 [登录] 按钮。
  2. +
  3. 用户跳转到子系统登录接口 /sso/login,并携带 back参数 记录初始页面URL。
      +
    • 形如:http://{sso-client}/sso/login?back=xxx
    • +
    +
  4. +
  5. 子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带redirect参数记录子系统的登录页URL。
      +
    • 形如:http://{sso-server}/sso/auth?redirect=xxx?back=xxx
    • +
    +
  6. +
  7. 用户进入了 SSO认证中心 的登录页面,开始登录。
  8. +
  9. 用户 输入账号密码 并 登录成功,SSO认证中心再次将用户重定向至子系统的登录接口/sso/login,并携带ticket码参数。
      +
    • 形如:http://{sso-client}/sso/login?back=xxx&ticket=xxxxxxxxx
    • +
    +
  10. +
  11. 子系统根据 ticket码SSO-Redis 中获取账号id,并在子系统登录此账号会话。
  12. +
  13. 子系统将用户再次重定向至最初始的 back 页面。
  14. +
+

整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击[登录]按钮,由于此用户在SSO认证中心已有会话存在,
+所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。

+

动态演示图:

+

SSO 模式二

+

下面我们按照步骤依次完成上述过程:

+

二、准备工作

+

首先修改hosts文件(C:\windows\system32\drivers\etc\hosts),添加以下IP映射,方便我们进行测试:

+
127.0.0.1 sa-sso-server.com
+127.0.0.1 sa-sso-client1.com
+127.0.0.1 sa-sso-client2.com
+127.0.0.1 sa-sso-client3.com
  • 1
  • 2
  • 3
  • 4
+

三、搭建 Client 端项目

+
+

搭建示例在 Sa-Token 官方仓库的 /sa-token-demo/sa-token-demo-sso2-client/,如遇到难点可结合源码进行测试学习

+
+ +

在SSO模式一章节中我们打开了配置:

+ + + +
sa-token: 
+    cookie: 
+        # 配置 Cookie 作用域 
+        domain: stp.com 
  • 1
  • 2
  • 3
  • 4
+ + +

此为模式一专属配置,现在我们将其注释掉

+

3.2、创建 SSO-Client 端项目

+

创建一个 SpringBoot 项目 sa-token-demo-sso2-client,引入依赖:

+ + + +
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.com -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<!-- Sa-Token 插件:整合SSO -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-sso</artifactId>
+    <version>1.34.0</version>
+</dependency>
+
+<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-dao-redis-jackson</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.commons</groupId>
+    <artifactId>commons-pool2</artifactId>
+</dependency>
+
+<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-alone-redis</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
+

Gradle 方式:

+
// Sa-Token 权限认证,在线文档:https://sa-token.com
+implementation 'cn.dev33:sa-token-spring-boot-starter:1.34.0'
+
+// Sa-Token 插件:整合SSO
+implementation 'cn.dev33:sa-token-sso:1.34.0'
+
+// Sa-Token 整合 Redis (使用 jackson 序列化方式)
+implementation 'cn.dev33:sa-token-dao-redis-jackson:1.34.0'
+implementation 'org.apache.commons:commons-pool2'
+
+// Sa-Token插件:权限缓存与业务缓存分离
+implementation 'cn.dev33:sa-token-alone-redis:1.34.0'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+ + +

3.3、创建 SSO-Client 端认证接口

+

同 SSO-Server 一样,Sa-Token 为 SSO-Client 端所需代码也提供了完整的封装,你只需提供一个访问入口,接入 Sa-Token 的方法即可。

+

+/**
+ * Sa-Token-SSO Client端 Controller 
+ */
+@RestController
+public class SsoClientController {
+
+    // 首页 
+    @RequestMapping("/")
+    public String index() {
+        String str = "<h2>Sa-Token SSO-Client 应用端</h2>" + 
+                    "<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" + 
+                    "<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a> " + 
+                    "<a href='/sso/logout?back=self'>注销</a></p>";
+        return str;
+    }
+
+    /*
+     * SSO-Client端:处理所有SSO相关请求 
+     *      http://{host}:{port}/sso/login          -- Client端登录地址,接受参数:back=登录后的跳转地址 
+     *      http://{host}:{port}/sso/logout         -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址 
+     *      http://{host}:{port}/sso/logoutCall     -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
+     */
+    @RequestMapping("/sso/*")
+    public Object ssoRequest() {
+        return SaSsoProcessor.instance.clientDister();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
+
3.4、配置SSO认证中心地址
+

你需要在 application.yml 配置如下信息:

+ + + +
# 端口
+server:
+    port: 9001
+
+# sa-token配置 
+sa-token: 
+    # SSO-相关配置
+    sso: 
+        # SSO-Server端 统一认证地址 
+        auth-url: http://sa-sso-server.com:9000/sso/auth
+        # 是否打开单点注销接口
+        is-slo: true
+
+    # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
+    alone-redis: 
+        # Redis数据库索引 (默认为0)
+        database: 1
+        # Redis服务器地址
+        host: 127.0.0.1
+        # Redis服务器连接端口
+        port: 6379
+        # Redis服务器连接密码(默认为空)
+        password: 
+        # 连接超时时间
+        timeout: 10s
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
+

properties 风格:

+
# 端口
+server.port=9001
+
+######### Sa-Token 配置 #########
+# SSO-Server端 统一认证地址 
+sa-token.sso.auth-url=http://sa-sso-server.com:9000/sso/auth
+# 是否打开单点注销接口
+sa-token.sso.is-slo=true
+
+# 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
+# Redis数据库索引
+sa-token.alone-redis.database=1
+# Redis服务器地址
+sa-token.alone-redis.host=127.0.0.1
+# Redis服务器连接端口
+sa-token.alone-redis.port=6379
+# Redis服务器连接密码(默认为空)
+sa-token.alone-redis.password=
+# 连接超时时间
+sa-token.alone-redis.timeout=10s
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
+ + +

注意点:sa-token.alone-redis 的配置需要和SSO-Server端连接同一个Redis(database也要一样)

+

3.5、写启动类

+
@SpringBootApplication
+public class SaSso2ClientApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaSso2ClientApplication.class, args);
+        System.out.println("\nSa-Token-SSO Client端启动成功");
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

启动项目

+

四、测试访问

+

(1) 依次启动 SSO-ServerSSO-Client,然后从浏览器访问:http://sa-sso-client1.com:9001/

+

sso-client-index.png

+

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

+

sso-server-auth.png

+

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

+

sso-server-dologin.png

+

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

+

sso-client-index-ok.png

+

(5) 页面被重定向至Client端首页,并提示登录成功,至此,Client1应用已单点登录成功!

+

(6) 我们再次访问Client2http://sa-sso-client2.com:9001/

+

sso-client2-index.png

+

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

+

sso-client2-index-ok.png

+

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

+

sso-client3-index-ok.png

+

至此,测试完毕!

+

可以看出,除了在Client1端我们需要手动登录一次之外,在Client2端Client3端都是可以无需再次认证,直接登录成功的。

+

我们可以通过 F12控制台 Network 跟踪整个过程

+

sso-genzong

+

五、Ticket 劫持攻击

+

在上面的示例中,配置项 sa-token.sso.allow-url=* 意为配置所有允许的Client端授权地址,不在此配置项中的URL将无法单点登录成功。

+

为了方便测试,上述代码将其配置为*,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 Ticket 劫持的风险。

+

假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL:

+
+

http://sa-sso-server.com:9000/sso/auth?redirect=https://www.baidu.com/

+
+

当不知情的小红被诱导访问了这个URL时,它将被重定向至百度首页

+

sso-ticket-jc

+

可以看到,代表着用户身份的 Ticket 码也显现到了URL之中,借此漏洞,攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器,伪造小红身份登录网站

+

六、防范方法

+

造成T icket 劫持攻击的直接原因就是SSO-Server认证中心没有对 redirect地址 进行任何的限制,防范的方法也很简单,就是对redirect参数进行校验,如果其不在指定的URL列表中时,拒绝下放ticket

+

我们将其配置为一个具体的URL:

+ + + +
sa-token: 
+    sso: 
+        # 配置允许单点登录的 url 
+        allow-url: http://sa-sso-client1.com:9001/sso/login
  • 1
  • 2
  • 3
  • 4
+ + +
# 配置允许单点登录的 url 
+sa-token.sso.allow-url=http://sa-sso-client1.com:9001/sso/login
  • 1
  • 2
+ + +

再次访问上述链接:

+

sso-feifa-rf

+

域名没有通过校验,拒绝授权!

+
配置安全性参考表
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
配置方式举例安全性建议
配置为**禁止在生产环境下使用
配置到域名http://sa-sso-client1.com/*不建议在生产环境下使用
配置到详细地址http://sa-sso-client1.com:9001/sso/login可以在生产环境下使用
+
疑问:为什么不直接回传 Token,而是先回传 Ticket,再用 Ticket 去查询对应的账号id?
+

Token 作为长时间有效的会话凭证,在任何时候都不应该直接暴露在 URL 之中(虽然 Token 直接的暴露本身不会造成安全漏洞,但会为很多漏洞提供可乘之机)

+

为了不让系统安全处于亚健康状态,Sa-Token-SSO 选择先回传 Ticket,再由 Ticket 获取账号id,且 Ticket 一次性用完即废,提高安全性。

+

七、下篇预告:跨 Redis 模式下的解决方案

+

以上流程解决了跨域模式下的单点登录,但是后端仍然采用了共享 Redis 来同步会话,如果我们的架构设计中Client端与Server端无法共享Redis,又该怎么完成单点登录?

+

这就是下篇博客要介绍的 SSO 模式三:跨域、跨 Redis 下的 SSO 对接步骤。

+
+

参考资料

+ +
+← Sa-Token 单点登录第一篇:搭建 SSO-Server 统一认证中心 +Sa-Token 单点登录第二篇:同域、同 Redis 下的 SSO 对接步骤 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-si-pian-kua-yu-kua-redis-xia-de-sso-dui-jie-bu-zhou.html b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-si-pian-kua-yu-kua-redis-xia-de-sso-dui-jie-bu-zhou.html new file mode 100644 index 000000000..1b4ed8c62 --- /dev/null +++ b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-si-pian-kua-yu-kua-redis-xia-de-sso-dui-jie-bu-zhou.html @@ -0,0 +1,537 @@ + + + + + Sa-Token 单点登录第四篇:跨域、跨 Redis 下的 SSO 对接步骤 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 单点登录第四篇:跨域、跨 Redis 下的 SSO 对接步骤

+ +

大家好,由于单篇博客长度有限,我将针对 SSO 单点登录 开启一个系列文章,此为第四篇,想完整的了解 SSO 技术的同学欢迎点开我的账号动态参考本系列历史文章。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录 —— 便是我们搭建统一认证中心的关键。

+

如果我们的多个系统:如果既无法做到前端同域,也无法做到后端同Redis,那么可以使用 SSO 模式三 [Http请求获取会话] 的方式完成单点登录。

+
+

阅读本篇之前请务必先熟读SSO模式二!因为模式三仅仅属于模式二的一个特殊场景,熟读模式二有助于您快速理解本章内容

+
+

一、问题分析

+

我们先来分析一下,当后端不使用共享 Redis 时,会对架构产生哪些影响:

+
    +
  1. Client 端无法直连 Redis 校验 ticket,取出账号id。
  2. +
  3. Client 端无法与 Server 端共用一套会话,需要自行维护子会话。
  4. +
  5. 由于不是一套会话,所以无法“一次注销,全端下线”,需要额外编写代码完成单点注销。
  6. +
+

所以模式三的主要目标:也就是在 模式二的基础上 解决上述 三个难题

+
+

模式三的 Demo 示例在 Sa-Token 官方仓库 /sa-token-demo/sa-token-demo-sso3-client/ ,如遇难点可参考示例

+
+

二、在 Client 端更改 Ticket 校验方式

+

2.1、增加 pom.xml 配置

+ + + +
<!-- Http请求工具 -->
+<dependency>
+    <groupId>com.dtflys.forest</groupId>
+    <artifactId>forest-spring-boot-starter</artifactId>
+    <version>1.5.26</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+ + +
// Http请求工具
+implementation 'com.dtflys.forest:forest-spring-boot-starter:1.5.26'
  • 1
  • 2
+ + +
+

Forest 是一个轻量级 http 请求工具,详情参考:Forest

+
+

2.2、配置 http 请求处理器

+

在SSO-Client端的 SsoClientController 中,新增以下配置

+
// 配置SSO相关参数 
+@Autowired
+private void configSso(SaSsoConfig sso) {
+    // ... 其他代码
+
+    // 配置 Http 请求处理器
+    sso.setSendHttp(url -> {
+        System.out.println("------ 发起请求:" + url);
+        return Forest.get(url).executeAsString();
+    });
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

2.3、application.yml 新增配置

+ + + +
sa-token: 
+    sso: 
+        # 打开模式三(使用Http请求校验ticket)
+        is-http: true
+        # SSO-Server端 ticket校验地址 
+        check-ticket-url: http://sa-sso-server.com:9000/sso/checkTicket
+
+forest: 
+    # 关闭 forest 请求日志打印
+    log-enabled: false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+ + +
# 打开模式三(使用Http请求校验ticket)
+sa-token.sso.is-http=true
+# SSO-Server端 ticket校验地址 
+sa-token.sso.check-ticket-url=http://sa-sso-server.com:9000/sso/checkTicket
+
+# 关闭 forest 请求日志打印
+forest.log-enabled: false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+ + +

因为我们已经在控制台手动打印 url 请求日志了,所以此处 forest.log-enabled=false 关闭 Forest 框架自身的日志打印,这不是必须的,你可以将其打开。

+

2.4、启动项目测试

+

重启项目,访问测试:
+- http://sa-sso-client1.com:9001/
+- http://sa-sso-client2.com:9001/
+- http://sa-sso-client3.com:9001/

+
+

注:如果已测试运行模式二,可先将Redis中的数据清空,以防旧数据对测试造成干扰

+
+

三、获取 Userinfo

+

除了账号id,我们可能还需要将用户的昵称、头像等信息从 Server端 带到 Client端,即:用户资料的同步。

+

在模式二中我们只需要将需要同步的资料放到 SaSession 即可,但是在模式三中两端不再连接同一个Redis,这时候我们需要通过http接口来同步信息:

+

3.1、在 Server 端自定义接口,查询用户资料

+
// 自定义接口:获取userinfo 
+@RequestMapping("/sso/userinfo")
+public Object userinfo(String loginId) {
+    System.out.println("---------------- 获取userinfo --------");
+
+    // 校验签名,防止敏感信息外泄  
+    SaSsoUtil.checkSign(SaHolder.getRequest());
+
+    // 自定义返回结果(模拟)
+    return SaResult.ok()
+            .set("id", loginId)
+            .set("name", "linxiaoyu")
+            .set("sex", "女")
+            .set("age", 18);
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

3.2、在 Client 端调用此接口查询 userinfo

+

首先在 application.yml 中配置接口地址:

+ + + +
sa-token: 
+    sso: 
+        # SSO-Server端 查询userinfo地址 
+        userinfo-url: http://sa-sso-server.com:9000/sso/userinfo
  • 1
  • 2
  • 3
  • 4
+ + +
# SSO-Server端 查询userinfo地址 
+sa-token.sso.userinfo-url=http://sa-sso-server.com:9000/sso/userinfo
  • 1
  • 2
+ + +

然后在SsoClientController中新增接口

+
// 查询我的账号信息 
+@RequestMapping("/sso/myinfo")
+public Object myinfo() {
+    Object userinfo = SaSsoUtil.getUserinfo(StpUtil.getLoginId());
+    System.out.println("--------info:" + userinfo);
+    return userinfo;
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

3.3、访问测试

+

访问测试:http://sa-sso-client1.com:9001/sso/myinfo

+

四、自定义接口通信

+

群里有小伙伴提问:SaSsoUtil.getUserinfo 提供的参数太少,只有一个 loginId,无法满足业务需求怎么办?

+

答:SaSsoUtil.getUserinfo只是为了避免你在项目中硬编码认证中心 url 而提供的简易封装,如果这个API无法满足你的业务需求,
+你完全可以在 Server 端自定义一些接口然后从 Client 端使用 http 工具调用即可。

+

以下是一个简单的示例:

+

4.1、先在 sso-server 端自定义一个接口

+
// 获取指定用户的关注列表 
+@RequestMapping("/sso/getFollowList")
+public Object ssoRequest(Long loginId) {
+
+    // 校验签名,签名不通过直接抛出异常 
+    SaSsoUtil.checkSign(SaHolder.getRequest());
+
+    // 查询数据 (此处仅做模拟)
+    List<Integer> list = Arrays.asList(10041, 10042, 10043, 10044);
+
+    // 返回 
+    return list;
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

4.2、然后在 sso-client 端调用这个接口

+
// 查询我的账号信息 
+@RequestMapping("/sso/myFollowList")
+public Object myFollowList() {
+    // 组织url,加上签名参数 
+    String url = SaSsoUtil.addSignParams("http://sa-sso-server.com:9000/sso/getFollowList", StpUtil.getLoginId());
+
+    // 调用,并返回 SaResult 结果 
+    SaResult res = SaSsoUtil.request(url);
+
+    // 返回给前端 
+    return res;
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+

4.3、访问测试

+

访问测试:http://sa-sso-client1.com:9001/sso/myFollowList

+

五、无刷单点注销

+

有了单点登录就必然要有单点注销,网上给出的大多数解决方案是将注销请求重定向至SSO-Server中心,逐个通知Client端下线

+

在某些场景下,页面的跳转可能造成不太好的用户体验,Sa-Token-SSO 允许你以 REST API 的形式构建接口,做到页面无刷新单点注销。

+
    +
  1. Client 端在校验 ticket 时,将注销回调地址发送到 Server 端。
  2. +
  3. Server 端将此 Client 的注销回调地址存储到 Set 集合。
  4. +
  5. Client 端向 Server 端发送单点注销请求。
  6. +
  7. Server 端遍历Set集合,逐个通知 Client 端下线。
  8. +
  9. Server 端注销下线。
  10. +
  11. 单点注销完成。
  12. +
+

+

这些逻辑 Sa-Token 内部已经封装完毕,你只需按照文档增加以下配置即可:

+

5.1、SSO-Client 端新增配置

+

application.yml 增加配置:API调用秘钥单点注销接口URL

+ + + +
sa-token: 
+    sso: 
+        # 打开单点注销功能 
+        is-slo: true
+        # 单点注销地址 
+        slo-url: http://sa-sso-server.com:9000/sso/signout
+        # 接口调用秘钥 
+        secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+ + +
# 打开单点注销功能 
+sa-token.sso.is-slo=true
+# 单点注销地址 
+sa-token.sso.slo-url=http://sa-sso-server.com:9000/sso/signout
+# 接口调用秘钥 
+sa-token.sso.secretkey=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+ + +

注意 secretkey 秘钥需要与SSO认证中心的一致

+

5.2、启动测试

+

重启项目,访问测试:http://sa-sso-client1.com:9001/
+我们主要的测试点在于 单点注销,正常登录即可。

+

sso-type3-client-index.png

+

点击 [注销] 按钮,即可单点注销成功。

+ + +

sso-type3-slo-index.png

+

PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用 Ajax 调用接口即可做到无刷单点登录退出。

+

例如,我们使用 Apifox 接口测试工具 可以做到同样的效果:

+

sso-slo-apifox.png

+

测试完毕!

+

六、下篇预告:前后端分离模式下的 SSO 对接方案

+

当我们熟读三种模式的单点登录之后,其实不难发现:所谓单点登录,其本质就是多个系统之间的会话共享。

+

当我们理解这一点之后,三种模式的工作原理也浮出水面:

+
    +
  • 模式一:采用共享 Cookie 来做到前端 Token 的共享,从而达到后端的 Session 会话共享。
  • +
  • 模式二:采用 URL 重定向,以 ticket 码为授权中介,做到多个系统间的会话传播。
  • +
  • 模式三:采用 Http 请求主动查询会话,做到 Client 端与 Server 端的会话同步。
  • +
+

以上三种模式解决了前后端一体架构下的单点登录,下篇博客将介绍:前后端分离架构下的 SSO 对接方案。

+
+

参考资料

+ +
+← Sa-Token 单点登录第六篇:定制化登录页面 +Sa-Token 多账号认证:同时为系统的 Admin 账号和 User 账号提供鉴权操作 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-wu-pian-qian-hou-duan-fen-li-jia-gou-xia-de-sso-dui.html b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-wu-pian-qian-hou-duan-fen-li-jia-gou-xia-de-sso-dui.html new file mode 100644 index 000000000..0e9cf6811 --- /dev/null +++ b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-wu-pian-qian-hou-duan-fen-li-jia-gou-xia-de-sso-dui.html @@ -0,0 +1,746 @@ + + + + + Sa-Token 单点登录第五篇:前后端分离架构下的 SSO 对接步骤 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 单点登录第五篇:前后端分离架构下的 SSO 对接步骤

+ +

大家好,由于单篇博客长度有限,我将针对 SSO 单点登录 开启一个系列文章,此为第五篇,想完整的了解 SSO 技术的同学欢迎点开我的账号动态参考本系列历史文章。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录 —— 便是我们搭建统一认证中心的关键。

+

前面几篇文章介绍了在前后端一体同域、跨域、同Redis、跨Redis 等架构下的单点登录,本篇将介绍:前后端分离架构下的 SSO 对接方案。

+

以之前的 SSO 模式二对接 demo 为例,将其改造为前后端分离模式,只需以下几步:

+

一、新建H5Controller开放接口

+
/**
+ * 前后端分离架构下集成SSO所需的接口 
+ */
+@RestController
+public class H5Controller {
+
+    // 当前是否登录 
+    @RequestMapping("/sso/isLogin")
+    public Object isLogin() {
+        return SaResult.data(StpUtil.isLogin());
+    }
+
+    // 返回SSO认证中心登录地址 
+    @RequestMapping("/sso/getSsoAuthUrl")
+    public SaResult getSsoAuthUrl(String clientLoginUrl) {
+        String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(clientLoginUrl, "");
+        return SaResult.data(serverAuthUrl);
+    }
+
+    // 根据ticket进行登录 
+    @RequestMapping("/sso/doLoginByTicket")
+    public SaResult doLoginByTicket(String ticket) {
+        Object loginId = SaSsoProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket");
+        if(loginId != null) {
+            StpUtil.login(loginId);
+            return SaResult.data(StpUtil.getTokenValue());
+        }
+        return SaResult.error("无效ticket:" + ticket); 
+    }
+
+    // 全局异常拦截 
+    @ExceptionHandler
+    public SaResult handlerException(Exception e) {
+        e.printStackTrace(); 
+        return SaResult.error(e.getMessage());
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
+

二、增加跨域过滤器CorsFilter.java

+
/**
+ * 跨域过滤器
+ */
+@Component
+@Order(-200)
+public class CorsFilter implements Filter {
+
+    static final String OPTIONS = "OPTIONS";
+
+    @Override
+    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+            throws IOException, ServletException {
+        HttpServletRequest request = (HttpServletRequest) req;
+        HttpServletResponse response = (HttpServletResponse) res;
+
+        // 允许指定域访问跨域资源
+        response.setHeader("Access-Control-Allow-Origin", "*");
+        // 允许所有请求方式
+        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
+        // 有效时间
+        response.setHeader("Access-Control-Max-Age", "3600");
+        // 允许的header参数
+        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");
+
+        // 如果是预检请求,直接返回
+        if (OPTIONS.equals(request.getMethod())) {
+            System.out.println("---------------------- 浏览器发来了 OPTIONS 预检请求 ----------------------");
+            response.getWriter().print("");
+            return;
+        }
+
+        chain.doFilter(req, res);
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) {
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
+

三、新建前端项目

+

任意文件夹新建前端项目:sa-token-demo-sso-client-h5,在根目录添加测试文件:index.html

+
<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>Sa-Token-SSO-Client端-测试页(前后端分离版)</title>
+    </head>
+    <body>
+        <h2>Sa-Token SSO-Client 应用端(前后端分离版)</h2>
+        <p>当前是否登录:<b class="is-login"></b></p>
+        <p>
+            <a href="javascript:location.href='sso-login.html?back=' + encodeURIComponent(location.href);">登录</a>
+            <a href="javascript:location.href=baseUrl + '/sso/logout?satoken=' + localStorage.satoken + '&back=' + encodeURIComponent(location.href);">注销</a>
+        </p>
+        <script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+        <script type="text/javascript">
+
+            // 后端接口地址 
+            var baseUrl = "http://sa-sso-client1.com:9001";
+
+            // 查询当前会话是否登录 
+            $.ajax({
+                url: baseUrl + '/sso/isLogin',
+                type: "post", 
+                dataType: 'json',
+                headers: {
+                    "X-Requested-With": "XMLHttpRequest",
+                    "satoken": localStorage.getItem("satoken")
+                },
+                success: function(res){
+                    $('.is-login').html(res.data + '');
+                },
+                error: function(xhr, type, errorThrown){
+                    return alert("异常:" + JSON.stringify(xhr));
+                }
+            });
+
+        </script>
+    </body>
+</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
+

四、添加登录处理文件sso-login.html

+
<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>Sa-Token-SSO-Client端-登录页</title>
+        <style type="text/css">
+
+        </style>
+    </head>
+    <body>
+        <div class="login-box">
+
+        </div>
+        <script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+        <script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
+        <script type="text/javascript">
+
+            // 后端接口地址 
+            var baseUrl = "http://sa-sso-client1.com:9001";
+
+            var back = getParam('back', '/');
+            var ticket = getParam('ticket');
+            $(function() {
+                if(ticket) {
+                    doLoginByTicket(ticket);
+                } else {
+                    goSsoAuthUrl();
+                }
+            })
+
+            // 重定向至认证中心 
+            function goSsoAuthUrl() {
+                sa.ajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {
+                    console.log(res);
+                    location.href = res.data;
+                })
+            }
+
+            // 根据ticket值登录 
+            function doLoginByTicket(ticket) {
+                sa.ajax('/sso/doLoginByTicket', {ticket: ticket}, function(res) {
+                    console.log(res);
+                    if(res.code == 200) {
+                        localStorage.setItem('satoken', res.data);
+                        location.href = decodeURIComponent(back); 
+                    } else {
+                        alert(res.msg);
+                    }
+                })
+            }
+
+            // 从url中查询到指定名称的参数值 
+            function getParam(name, defaultValue){
+                var query = window.location.search.substring(1);
+                var vars = query.split("&");
+                for (var i=0;i<vars.length;i++) {
+                    var pair = vars[i].split("=");
+                    if(pair[0] == name){return pair[1];}
+                }
+                return(defaultValue == undefined ? null : defaultValue);
+            }
+
+        </script>
+        <script type="text/javascript">
+            var sa = {};
+
+            // 封装一下Ajax
+            sa.ajax = function(url, data, successFn) {
+                // sa.loading("正在努力加载...");
+                $.ajax({
+                    url: baseUrl + url,
+                    type: "post", 
+                    data: data,
+                    dataType: 'json',
+                    headers: {
+                        satoken: localStorage.getItem('satoken')
+                    },
+                    success: function(res){
+                        console.log('返回数据:', res);
+                        successFn(res);
+                    },
+                    error: function(xhr, type, errorThrown){
+                        if(xhr.status == 0){
+                            return alert('无法连接到服务器,请检查网络');
+                        }
+                        return alert("异常:" + JSON.stringify(xhr));
+                    }
+                });
+            }
+
+        </script>
+    </body>
+</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
+

五、测试运行

+

先启动Server服务端与Client服务端,再随便找个能预览html的工具打开前端项目(比如HBuilderX),
+测试流程与之前的一体版一致,在此暂不赘述。

+

六、SSO-Server 端的前后端分离

+

疑问:上述代码都是针对 Client 端进行拆分,如果我想在 SSO-Server 端也进行前后端分离改造,应该怎么做?

+
+

答:解决思路都是大同小异的,与Client一样,我们需要把原本在 “后端处理的授权重定向逻辑” 拿到前端来实现。

+
+

新建 sa-token-demo-sso-server-h5 项目,在根目录下创建文件 sso-auth.html,复制以下代码:

+
<!DOCTYPE html>
+<html lang="zh">
+    <head>
+        <title>Sa-SSO-Server 认证中心-登录</title>
+        <meta charset="utf-8">
+        <base th:href="@{/}" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+        <link rel="stylesheet" href="./login.css">
+    </head>
+    <body>
+        <div class="view-box">
+            <div class="bg-1"></div>
+            <div class="content-box">
+                <div class="login-box">
+                    <div class="from-box">
+                        <h2 class="from-title">Sa-SSO-Server 认证中心(前后端分离版)</h2>
+                        <div class="from-item">
+                            <input class="s-input" name="name" placeholder="请输入账号" />
+                        </div>
+                        <div class="from-item">
+                            <input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
+                        </div>
+                        <div class="from-item">
+                            <button class="s-input s-btn login-btn">登录</button>
+                        </div>
+                        <div class="from-item reset-box">
+                            <a href="javascript: location.reload();" >刷新</a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <!-- 底部 版权 -->
+            <div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
+                This page is provided by Sa-Token-SSO 
+            </div>
+        </div>
+
+        <!-- scripts -->
+        <script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
+        <script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
+        <script src="./login.js"></script>
+
+    </body>
+</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
+

创建 login.css,复制以下代码:

+
*{margin: 0; padding: 0;}
+body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
+::-webkit-input-placeholder{color: #ccc;}
+
+/* 视图盒子 */
+.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
+/* 背景 EAEFF3 */
+.bg-1{height: 100%; background: #c0cCf4;}
+
+/* 内容盒子 */
+.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}
+
+/* 登录盒子 */
+/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
+.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}
+.login-box{display: flex; align-items: center; text-align: center;}
+
+/* 表单 */
+.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
+.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
+.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}
+
+/* 输入框 */
+.from-item{border: 0px #000 solid; margin-bottom: 15px;}
+.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
+.s-input{font-size: 12px;}
+.s-input:focus{border-color: #409eff}
+
+/* 登录按钮 */
+.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
+.s-btn:hover{background-color: #50aEFF;}
+
+/* 重置按钮 */
+.reset-box{text-align: left; font-size: 12px;}
+.reset-box a{text-decoration: none;}
+.reset-box a:hover{text-decoration: underline;}
+
+/* loading框样式 */
+.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
+.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
+.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
+

创建 login.js,复制以下代码:

+
// 服务端地址 
+var baseUrl = "http://sa-sso-server.com:9000";
+
+// sa 
+var sa = {};
+
+// 打开loading
+sa.loading = function(msg) {
+    layer.closeAll();   // 开始前先把所有弹窗关了
+    return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});
+};
+
+// 隐藏loading
+sa.hideLoading = function() {
+    layer.closeAll();
+};
+
+// 封装一下Ajax
+sa.ajax = function(url, data, successFn) {
+    $.ajax({
+        url: baseUrl + url,
+        type: "post", 
+        data: data,
+        dataType: 'json',
+        headers: {
+            'X-Requested-With': 'XMLHttpRequest',
+            'satoken': localStorage.getItem('satoken')
+        },
+        success: function(res){
+            console.log('返回数据:', res);
+            successFn(res);
+        },
+        error: function(xhr, type, errorThrown){
+            if(xhr.status == 0){
+                return alert('无法连接到服务器,请检查网络');
+            }
+            return alert("异常:" + JSON.stringify(xhr));
+        }
+    });
+}
+
+// ----------------------------------- 相关事件 -----------------------------------
+
+// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码 
+var pData = {
+    client: getParam('client', ''), 
+    redirect: getParam('redirect', ''), 
+    mode: getParam('mode', '')
+};
+sa.ajax("/sso/getRedirectUrl", pData, function(res) {
+    if(res.code == 200) {
+        // 已登录,并且redirect地址有效,开始跳转  
+        location.href = decodeURIComponent(res.data);
+    } else if(res.code == 401) {
+        console.log('未登录');
+    } else {
+        layer.alert(res.msg); 
+    }
+})
+
+// 登录
+$('.login-btn').click(function(){
+    sa.loading("正在登录...");
+    // 开始登录
+    var data = {
+        name: $('[name=name]').val(),
+        pwd: $('[name=pwd]').val()
+    };
+    sa.ajax("/sso/doLogin", data, function(res) {
+        sa.hideLoading();
+        if(res.code == 200) {
+            localStorage.setItem('satoken', res.data);
+            layer.msg('登录成功', {anim: 0, icon: 6 }); 
+            setTimeout(function() {
+                location.reload();
+            }, 800);
+        } else {
+            layer.msg(res.msg, {anim: 6, icon: 2 }); 
+        }
+    })
+});
+
+
+// 绑定回车事件
+$('[name=name],[name=pwd]').bind('keypress', function(event){
+    if(event.keyCode == "13") {
+        $('.login-btn').click();
+    }
+});
+
+// 输入框获取焦点
+$("[name=name]").focus();
+
+// 从url中查询到指定名称的参数值 
+function getParam(name, defaultValue){
+    var query = window.location.search.substring(1);
+    var vars = query.split("&");
+    for (var i=0;i<vars.length;i++) {
+        var pair = vars[i].split("=");
+        if(pair[0] == name){return pair[1] + (pair[2] ? '=' + pair[2] : '');}
+    }
+    return(defaultValue == undefined ? null : defaultValue);
+}
+
+// 打印信息 
+var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.com/";
+console.log(str);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
+

七、启动测试

+

使用前端ide导入项目 sa-token-demo-sso-server-h5,浏览器访问 sso-auth.html 页面:

+

sso-type2-server-h5-auth.png

+

复制上述地址,将其配置到 Client 端的 yml 配置文件中,例如:

+ + + +
sa-token: 
+    sso: 
+        # SSO-Server端 统一认证地址 
+        auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html
  • 1
  • 2
  • 3
  • 4
+ + +

然后我们启动项目 sa-token-demo-sso-serversa-token-demo-sso2-client,按照之前的测试步骤访问:
+http://sa-sso-client1.com:9001/,即可以前后端分离模式完成 SSO-Server 端的授权登录。

+
+

参考资料

+ +
+← Sa-Token 单点登录第二篇:同域、同 Redis 下的 SSO 对接步骤 +Sa-Token 单点登录第六篇:定制化登录页面 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-yi-pian-da-jian-sso-server-tong-yi-ren-zheng-zhong.html b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-yi-pian-da-jian-sso-server-tong-yi-ren-zheng-zhong.html new file mode 100644 index 000000000..1d5174197 --- /dev/null +++ b/sa-token-doc/blog/essays/sa-token-dan-dian-deng-lu-di-yi-pian-da-jian-sso-server-tong-yi-ren-zheng-zhong.html @@ -0,0 +1,702 @@ + + + + + Sa-Token 单点登录第一篇:搭建 SSO-Server 统一认证中心 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 单点登录第一篇:搭建 SSO-Server 统一认证中心

+ +

大家好,由于单篇博客长度有限,我将针对 SSO 单点登录 开启一个系列文章,此为第一篇,想完整的了解 SSO 技术的欢迎点开我的账号动态参考本系列其它文章。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录 —— 便是我们搭建统一认证中心的关键。

+

本篇将介绍如何使用 Sa-Token 方便的搭建一个 sso-server 认证中心。

+

一、什么是单点登录?解决什么问题?

+

举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉,
+为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统。

+

单点登录——就是为了解决这个问题而生!

+

简而言之,单点登录可以做到:在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。

+

二、架构选型

+

Sa-Token-SSO 由简入难划分为三种模式,解决不同架构下的 SSO 接入问题:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
系统架构采用模式简介
前端同域 + 后端同 Redis模式一共享 Cookie 同步会话
前端不同域 + 后端同 Redis模式二URL重定向传播会话
前端不同域 + 后端不同 Redis模式三Http请求获取会话
+
    +
  1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:c1.domain.comc2.domain.comc3.domain.com
  2. +
  3. 后端同Redis:就是指多个系统可以连接同一个Redis。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 [权限缓存与业务缓存分离] 的解决方案。
  4. +
  5. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成)。
  6. +
+

三、Sa-Token-SSO 特性

+
    +
  1. API 简单易用,文档介绍详细,且提供直接可用的集成示例。
  2. +
  3. 支持三种模式,不论是否跨域、是否共享Redis、是否前后端分离,都可以完美解决。
  4. +
  5. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝Ticket劫持Token窃取等常见攻击手段(文档讲述攻击原理和防御手段)。
  6. +
  7. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:http://a.com?id=1&name=2,登录成功之后就变成了:http://a.com?id=1,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面。
  8. +
  9. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决权限认证 + 单点登录问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合……
  10. +
  11. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发。
  12. +
+

在开始SSO三种模式的对接之前,我们必须先搭建一个 SSO-Server 认证中心

+

四、添加依赖

+

创建 SpringBoot 项目 sa-token-demo-sso-server,引入依赖:

+ + + +
<!-- Sa-Token 权限认证,在线文档:https://sa-token.com -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
+
+<!-- Sa-Token 插件:整合SSO -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-sso</artifactId>
+    <version>1.34.0</version>
+</dependency>
+
+<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-dao-redis-jackson</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.commons</groupId>
+    <artifactId>commons-pool2</artifactId>
+</dependency>
+
+<!-- 视图引擎(在前后端不分离模式下提供视图支持) -->
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-thymeleaf</artifactId>
+</dependency>
+
+<!-- Http请求工具(在模式三的单点注销功能下用到,如不需要可以注释掉) -->
+<dependency>
+    <groupId>com.dtflys.forest</groupId>
+    <artifactId>forest-spring-boot-starter</artifactId>
+    <version>1.5.26</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
+ + +

除了 sa-token-spring-boot-startersa-token-sso 以外,其它包都是可选的:
+- 在 SSO 模式三时 Redis 相关包是可选的
+- 在前后端分离模式下可以删除 thymeleaf 相关包
+- 在不需要 SSO 模式三单点注销的情况下可以删除 http 工具包

+

建议先完整测试三种模式之后再对pom依赖进行酌情删减。

+

五、开放认证接口

+

新建 SsoServerController,用于对外开放接口:

+
/**
+ * Sa-Token-SSO Server端 Controller 
+ */
+@RestController
+public class SsoServerController {
+
+    /*
+     * SSO-Server端:处理所有SSO相关请求 (下面的章节我们会详细列出开放的接口) 
+     */
+    @RequestMapping("/sso/*")
+    public Object ssoRequest() {
+        return SaSsoProcessor.instance.serverDister();
+    }
+
+    /**
+     * 配置SSO相关参数 
+     */
+    @Autowired
+    private void configSso(SaSsoConfig sso) {
+        // 配置:未登录时返回的View 
+        sso.setNotLoginView(() -> {
+            String msg = "当前会话在SSO-Server端尚未登录,请先访问"
+                    + "<a href='/sso/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
+                    + "进行登录之后,刷新页面开始授权";
+            return msg;
+        });
+
+        // 配置:登录处理函数 
+        sso.setDoLoginHandle((name, pwd) -> {
+            // 此处仅做模拟登录,真实环境应该查询数据进行登录 
+            if("sa".equals(name) && "123456".equals(pwd)) {
+                StpUtil.login(10001);
+                return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
+            }
+            return SaResult.error("登录失败!");
+        });
+
+        // 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉) 
+        sso.setSendHttp(url -> {
+            try {
+                // 发起 http 请求 
+                System.out.println("------ 发起请求:" + url);
+                return Forest.get(url).executeAsString();
+            } catch (Exception e) {
+                e.printStackTrace();
+                return null;
+            }
+        });
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
+

注意:
+- 在setDoLoginHandle函数里如果要获取name, pwd以外的参数,可通过SaHolder.getRequest().getParam("xxx")来获取
+- 在 setSendHttp 函数中,使用 try-catch 是为了提高整个注销流程的容错性,避免在一些极端情况下注销失败(例如:某个 Client 端上线之后又下线,导致 http 请求无法调用成功,从而阻断了整个注销流程)

+

全局异常处理:

+
@RestControllerAdvice
+public class GlobalExceptionHandler {
+    // 全局异常拦截 
+    @ExceptionHandler
+    public SaResult handlerException(Exception e) {
+        e.printStackTrace(); 
+        return SaResult.error(e.getMessage());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
+

六、application.yml配置

+ + + + +
# 端口
+server:
+    port: 9000
+
+# Sa-Token 配置
+sa-token: 
+    # ------- SSO-模式一相关配置  (非模式一不需要配置) 
+    # cookie: 
+        # 配置 Cookie 作用域 
+        # domain: stp.com 
+
+    # ------- SSO-模式二相关配置 
+    sso: 
+        # Ticket有效期 (单位: 秒),默认五分钟 
+        ticket-timeout: 300
+        # 所有允许的授权回调地址
+        allow-url: "*"
+        # 是否打开单点注销功能
+        is-slo: true
+
+        # ------- SSO-模式三相关配置 (下面的配置在SSO模式三并且 is-slo=true 时打开)
+        # 是否打开模式三 
+        isHttp: true
+        # 接口调用秘钥(用于SSO模式三的单点注销功能)
+        secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
+        # ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明) 
+
+spring: 
+    # Redis配置 (SSO模式一和模式二使用Redis来同步会话)
+    redis:
+        # Redis数据库索引(默认为0)
+        database: 1
+        # Redis服务器地址
+        host: 127.0.0.1
+        # Redis服务器连接端口
+        port: 6379
+        # Redis服务器连接密码(默认为空)
+        password: 
+
+forest: 
+    # 关闭 forest 请求日志打印
+    log-enabled: false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
+ + +

注意点:sa-token.sso.allow-url为了方便测试配置为*,线上生产环境一定要配置为详细URL地址 (之后的章节我们会详细阐述此配置项)

+

七、创建启动类

+
@SpringBootApplication
+public class SaSsoServerApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaSsoServerApplication.class, args);
+        System.out.println("\n------ Sa-Token-SSO 认证中心启动成功");
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

启动项目,不出意外的情况下我们将看到如下输出:

+

sso-server-start

+

访问统一授权地址:

+
+

http://localhost:9000/sso/auth

+
+

sso-server-init-login.png

+

可以看到这个页面目前非常简陋,这是因为我们以上的代码示例,主要目标是为了带大家从零搭建一个可用的SSO认证服务端,所以就对一些不太必要的步骤做了简化。

+

大家可以下载运行一下 Sa-Token 官方仓库里的示例/sa-token-demo/sa-token-demo-sso-server/,里面有制作好的登录页面:

+

sso-server-init-login2.png

+

默认账号密码为:sa / 123456,先别着急点击登录,因为我们还没有搭建对应的 Client 端项目,
+真实项目中我们是不会直接从浏览器访问 /sso/auth 授权地址的,我们需要在 Client 端点击登录按钮重定向而来。

+
+

现在我们先来看看除了 /sso/auth 统一授权地址,这个 SSO-Server 认证中心还开放了哪些API:

+

八、SSO-Server 认证中心开放接口参考

+

如果你的 SSO-Server 端和 SSO-Client 端都使用 Sa-Token-SSO 搭建,那么你可以忽略以下部分内容,直接使用 Sa-Token SSO 模块进行对接。

+

如果你仅在 SSO-Server 端使用 Sa-Token-SSO 搭建,而 SSO-Client 端使用其它框架的话,那么你就需要手动调用 http 请求来对接 SSO-Server 认证中心,
+下面的 API 列表将给你的对接步骤做一份参考。

+

1、单点登录授权地址

+
http://{host}:{port}/sso/auth
  • 1
+

接收参数:

+ + + + + + + + + + + + + + + + + + + + +
参数是否必填说明
redirect登录成功后的重定向地址,一般填写 location.href(从哪来回哪去)
mode授权模式,取值 [simple, ticket],simple=登录后直接重定向,ticket=带着ticket参数重定向,默认值为ticket
+

访问接口后有两种情况:
+- 情况一:当前会话在 SSO 认证中心未登录,会进入登录页开始登录。
+- 情况二:当前会话在 SSO 认证中心已登录,会被重定向至 redirect 地址,并携带 ticket 参数。

+

2、RestAPI 登录接口

+
http://{host}:{port}/sso/doLogin
  • 1
+

接收参数:

+ + + + + + + + + + + + + + + + + + + + +
参数是否必填说明
name用户名
pwd密码
+

此接口属于 RestAPI (使用ajax访问),会进入后端配置的 sso.setDoLoginHandle 函数中,此函数的返回值即是此接口的响应值。

+

另外需要注意:此接口并非只能携带 name、pwd 参数,因为你可以在方法里通过 SaHolder.getRequest().getParam("xxx") 来获取前端提交的其它参数。

+

3、Ticket 校验接口

+

此接口仅配置模式三 (isHttp=true) 时打开

+
http://{host}:{port}/sso/checkTicket
  • 1
+

接收参数:

+ + + + + + + + + + + + + + + + + + + + +
参数是否必填说明
ticket在步骤 1 中授权重定向时的 ticket 参数
ssoLogoutCall单点注销时的回调通知地址,只在SSO模式三单点注销时需要携带此参数
+

返回值场景:
+- 校验成功时:

+
{
+    "code": 200,
+    "msg": "ok",
+    "data": "10001" // 此 ticket 指向的 loginId
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+
    +
  • 校验失败时:
  • +
+
{
+    "code": 500,
+    "msg": "无效ticket:vESj0MtqrtSoucz4DDHJnsqU3u7AKFzbj0KH57EfJvuhkX1uAH23DuNrMYSjTnEq",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

4、单点注销接口

+
http://{host}:{port}/sso/signout
  • 1
+

此接口有两种调用方式

+
4.1、方式一:在 Client 的前端页面引导用户直接跳转,并带有 back 参数
+

例如:

+
http://{host}:{port}/sso/signout?back=xxx
  • 1
+

用户注销成功后将返回 back 地址

+
4.2、方式二:在 Client 的后端通过 http 工具来调用
+

接受参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数是否必填说明
loginId要注销的账号 id
timestamp当前时间戳,13位
nonce随机字符串
sign签名,生成算法:md5( loginId={账号id}&nonce={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )
+

例如:

+
http://{host}:{port}/sso/signout?loginId={value}&timestamp={value}&nonce={value}&sign={value}
  • 1
+

将返回 json 数据结果,形如:

+
{
+    "code": 200,    // 200表示请求成功,非200标识请求失败
+    "msg": "单点注销成功",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

如果单点注销失败,将返回:

+
{
+    "code": 500,    // 200表示请求成功,非200标识请求失败
+    "msg": "签名无效:xxx",  // 失败原因 
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

SSO 认证中心只有这四个接口,此部分内容先介绍至此,接下来的文章我会依次介绍在 Sa-Token SSO 中:
+同域、跨域、同Redis、跨Redis、前后端分离、前后端一体等各种不同架构下的单点登录对接流程。

+
+

参考资料

+ +
+ +Sa-Token 单点登录第三篇:跨域、同 Redis 下的 SSO 对接步骤 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/sa-token-duo-zhang-hao-ren-zheng-tong-shi-wei-xi-tong-de-admin-zhang-hao-he-user.html b/sa-token-doc/blog/essays/sa-token-duo-zhang-hao-ren-zheng-tong-shi-wei-xi-tong-de-admin-zhang-hao-he-user.html new file mode 100644 index 000000000..7d602e9a4 --- /dev/null +++ b/sa-token-doc/blog/essays/sa-token-duo-zhang-hao-ren-zheng-tong-shi-wei-xi-tong-de-admin-zhang-hao-he-user.html @@ -0,0 +1,476 @@ + + + + + Sa-Token 多账号认证:同时为系统的 Admin 账号和 User 账号提供鉴权操作 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 多账号认证:同时为系统的 Admin 账号和 User 账号提供鉴权操作

+ +

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。

+
+

Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

本篇将介绍 Sa-Token 中的多账号认证操作。

+

一、需求分析

+

有的时候,我们会在一个项目中设计两套账号体系,比如一个电商系统的 user表 和 admin表, 在这种场景下,如果两套账号我们都使用 StpUtil 类的API进行登录鉴权,那么势必会发生逻辑冲突。

+

在Sa-Token中,这个问题的模型叫做:多账号体系认证。

+

要解决这个问题,我们必须有一个合理的机制将这两套账号的授权给区分开,让它们互不干扰才行。

+

二、演进思路

+

假如说我们的 user表 和 admin表 都有一个 id=10001 的账号,它们对应的登录代码:StpUtil.login(10001) 是一样的,
+那么问题来了:在StpUtil.getLoginId()获取到的账号id如何区分它是User用户,还是Admin用户?

+

你可能会想到为他们加一个固定前缀,比如StpUtil.login("User_" + 10001)StpUtil.login("Admin_" + 10001),这样确实是可以解决问题的,
+但是同样的:你需要在StpUtil.getLoginId()时再裁剪掉相应的前缀才能获取真正的账号id,这样一增一减就让我们的代码变得无比啰嗦。

+

那么,有没有从框架层面支持的,更优雅的解决方案呢?

+

SaManager 提供了动态创建 StpLogic 的能力:

+
// 在当前会话登录 Admin 账号 10001
+SaManager.getStpLogic("admin").login(10001);
+
+// 在当前会话登录 User 账号 10001
+SaManager.getStpLogic("user").login(10001);
  • 1
  • 2
  • 3
  • 4
  • 5
+

这是一种解决方案,但仍然需要我们每次调用方法时指定账号类型参数,代码略显啰嗦,接下来我们介绍方案二:自定义 StpUtil 鉴权工具类。

+

三、自定义 StpUtil 鉴权工具类

+

前面几篇介绍的api调用,都是经过 StpUtil 类的各种静态方法进行授权认证,
+而如果我们深入它的源码,点此阅览

+就会发现,此类并没有任何代码逻辑,唯一做的事就是对成员变量stpLogic的各个API包装一下进行转发。

+

这样做有两个优点:
+- StpLogic 类的所有函数都可以被重写,按需扩展。
+- 在构造方法时随意传入一个不同的 loginType,就可以再造一套账号登录体系。

+

四、操作示例

+

比如说,对于原生StpUtil类,我们只做admin账号权限认证,而对于user账号,我们则:
+1. 新建一个新的权限认证类,比如: StpUserUtil.java
+2. 将StpUtil.java类的全部代码复制粘贴到 StpUserUtil.java里。
+3. 更改一下其 LoginType, 比如:

+
public class StpUserUtil {
+
+    /**
+     * 账号体系标识 
+     */
+    public static final String TYPE = "user";   // 将 LoginType 从`login`改为`user` 
+
+    // 其它代码 ... 
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+
    +
  1. 接下来就可以像调用StpUtil.java一样调用 StpUserUtil.java了,这两套账号认证的逻辑是完全隔离的。
  2. +
+
+

成品样例参考:码云 StpUserUtil.java

+
+

五、在多账户模式下使用注解鉴权

+

框架默认的注解鉴权 如@SaCheckLogin 只针对原生StpUtil进行鉴权。

+

例如,我们在一个方法上加上@SaCheckLogin注解,这个注解只会放行通过StpUtil.login(id)进行登录的会话,
+而对于通过StpUserUtil.login(id)进行登录的会话,则始终不会通过校验。

+

那么如何告诉@SaCheckLogin要鉴别的是哪套账号的登录会话呢?很简单,你只需要指定一下注解的type属性即可:

+
// 通过type属性指定此注解校验的是我们自定义的`StpUserUtil`,而不是原生`StpUtil`
+@SaCheckLogin(type = StpUserUtil.TYPE)
+@RequestMapping("info")
+public String info() {
+    return "查询用户信息";
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:@SaCheckRole("xxx")@SaCheckPermission("xxx")同理,亦可根据type属性指定其校验的账号体系,此属性默认为"",代表使用原生StpUtil账号体系。

+

六、使用注解合并简化代码

+

交流群里有同学反应,虽然可以根据 @SaCheckLogin(type = "user") 指定账号类型,但几十上百个注解都加上这个的话,还是有些繁琐,代码也不够优雅,有么有更简单的解决方案?

+

我们期待一种[注解继承/合并]的能力,即:自定义一个注解,标注上@SaCheckLogin(type = "user")
+然后在方法上标注这个自定义注解,效果等同于标注@SaCheckLogin(type = "user")

+

很遗憾,JDK默认的注解处理器并没有提供这种[注解继承/合并]的能力,不过好在我们可以利用 Spring 的注解处理器,达到同样的目的。

+
1、重写Sa-Token默认的注解处理器:
+
@Configuration
+public class SaTokenConfigure {
+    @Autowired
+    public void rewriteSaStrategy() {
+        // 重写Sa-Token的注解处理器,增加注解合并功能 
+        SaStrategy.me.getAnnotation = (element, annotationClass) -> {
+            return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); 
+        };
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+
2、自定义一个注解:
+
/**
+ * 登录认证(User版):只有登录之后才能进入该方法 
+ * <p> 可标注在函数、类上(效果等同于标注在此类的所有方法上) 
+ */
+@SaCheckLogin(type = "user")
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.TYPE})
+public @interface SaUserCheckLogin {
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+
3、接下来就可以使用我们的自定义注解了:
+
// 使用 @SaUserCheckLogin 的效果等同于使用:@SaCheckLogin(type = "user")
+@SaUserCheckLogin
+@RequestMapping("info")
+public String info() {
+    return "查询用户信息";
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:其它注解 @SaCheckRole("xxx")@SaCheckPermission("xxx")同理,
+完整示例参考:码云:自定义注解

+

七、同端多登陆

+

假设我们不仅需要在后台同时集成两套账号,我们还需要在一个客户端同时登陆两套账号(业务场景举例:一个APP中可以同时登陆商家账号和用户账号)。

+

如果我们不做任何特殊处理的话,在客户端会发生token覆盖,新登录的token会覆盖掉旧登录的token从而导致旧登录失效。

+

那么如何解决这个问题?

+很简单,我们只要更改一下 StpUserUtilTokenName 即可,参考示例如下:

+
public class StpUserUtil {
+
+    // 使用匿名子类 重写`stpLogic对象`的一些方法 
+    public static StpLogic stpLogic = new StpLogic("user") {
+        // 重写 StpLogic 类下的 `splicingKeyTokenName` 函数,返回一个与 `StpUtil` 不同的token名称, 防止冲突 
+        @Override
+        public String splicingKeyTokenName() {
+            return super.splicingKeyTokenName() + "-user";
+        }
+        // 同理你可以按需重写一些其它方法 ... 
+    }; 
+
+    // ... 
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

再次调用 StpUserUtil.login(10001) 进行登录授权时,token的名称将不再是 satoken,而是我们重写后的 satoken-user

+

八、不同体系不同 SaTokenConfig 配置

+

如果自定义的 StpUserUtil 需要使用不同 SaTokenConfig 对象, 也很简单,参考示例如下:

+
public class StpUserUtil {
+
+    // 使用匿名子类 重写`stpLogic对象`的一些方法 
+    public static StpLogic stpLogic = new StpLogic("user") {
+
+        // 首先自定义一个 Config 对象 
+        SaTokenConfig config = new SaTokenConfig()
+            .setTokenName("satoken")
+            .setTimeout(2592000)
+            // ... 其它set
+            ;
+
+        // 然后重写 stpLogic 配置获取方法 
+        @Override
+        public SaTokenConfig getConfig() {
+            return config;
+        }
+    };
+
+    // ... 
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
+

九、多账号体系混合鉴权

+

QQ群中有小伙伴提问:在多账号体系下,怎么在 SaInterceptor 拦截器中给一个接口登录鉴权?

+

其实这个问题,主要是靠你的业务需求来决定,以后台 Admin 账号和前台 User 账号为例:

+
// 注册 Sa-Token 拦截器
+@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    registry.addInterceptor(new SaInterceptor(handle -> {
+
+        // 如果这个接口,要求客户端登录了后台 Admin 账号才能访问:
+        SaRouter.match("/art/getInfo").check(r -> StpUtil.checkLogin());
+
+        // 如果这个接口,要求客户端登录了前台 User 账号才能访问:
+        SaRouter.match("/art/getInfo").check(r -> StpUserUtil.checkLogin());
+
+        // 如果这个接口,要求客户端同时登录 Admin 和 User 账号,才能访问:
+        SaRouter.match("/art/getInfo").check(r -> {
+            StpUtil.checkLogin();
+            StpUserUtil.checkLogin();
+        });
+
+        // 如果这个接口,要求客户端登录 Admin 和 User 账号任意一个,就能访问:
+        SaRouter.match("/art/getInfo").check(r -> {
+            if(StpUtil.isLogin() == false && StpUserUtil.isLogin() == false) {
+                throw new SaTokenException("请登录后再访问接口");
+            }
+        });
+
+    })).addPathPatterns("/**");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
+
+

参考资料

+ +
+← Sa-Token 单点登录第四篇:跨域、跨 Redis 下的 SSO 对接步骤 +Sa-Token 集成 jwt → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/sa-token-ji-cheng-jwt.html b/sa-token-doc/blog/essays/sa-token-ji-cheng-jwt.html new file mode 100644 index 000000000..1d155906a --- /dev/null +++ b/sa-token-doc/blog/essays/sa-token-ji-cheng-jwt.html @@ -0,0 +1,577 @@ + + + + + Sa-Token 集成 jwt - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 集成 jwt

+ +

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。

+
+

Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

很多小伙伴关心的一个话题,那就是 Sa-Token 和 JWT 哪个更好用一些,其实不用纠结这个问题,Sa-Token 提供了 sa-token-jwt 插件,
+可以将 Sa-Token 和 jwt 结合起来,这样我们就可以既有 Sa-Token 进行方便的鉴权,又可以得到 jwt 的 Token 扩展参数能力了。

+

下面我将介绍如何在 Sa-Token 中方便的集成 jwt。

+

一、引入依赖

+

首先在项目 pom.xml 中引入:

+
<!-- SpringBoot 集成 Sa-Token -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
+
+<!-- Sa-Token 整合 jwt -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-jwt</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

Gradle 方式:

+
// SpringBoot 集成 Sa-Token
+implementation 'cn.dev33:sa-token-spring-boot-starter:1.34.0'
+
+// Sa-Token 整合 jwt
+implementation 'cn.dev33:sa-token-jwt:1.34.0'
  • 1
  • 2
  • 3
  • 4
  • 5
+
+

注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本,意味着:你的项目中要么不引入 Hutool,要么引入版本 >= 5.7.14 的 Hutool 版本

+
+

二、配置秘钥

+

application.yml 配置文件中配置 jwt 生成秘钥:

+
sa-token:
+    # jwt秘钥 
+    jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
  • 1
  • 2
  • 3
+

注:为了安全起见请不要直接复制官网示例这个字符串(随便按几个字符就好了)

+

三、注入jwt实现

+

根据不同的整合规则,插件提供了三种不同的模式,你需要 选择其中一种 注入到你的项目中

+

Simple 简单模式:Token 风格替换

+
@Configuration
+public class SaTokenConfigure {
+    // Sa-Token 整合 jwt (Simple 简单模式)
+    @Bean
+    public StpLogic getStpLogicJwt() {
+        return new StpLogicJwtForSimple();
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

Mixin 混入模式:混入部分逻辑

+
@Configuration
+public class SaTokenConfigure {
+    // Sa-Token 整合 jwt (Mixin 混入模式)
+    @Bean
+    public StpLogic getStpLogicJwt() {
+        return new StpLogicJwtForMixin();
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

Stateless 无状态模式:服务器完全无状态

+
@Configuration
+public class SaTokenConfigure {
+    // Sa-Token 整合 jwt (Stateless 无状态模式)
+    @Bean
+    public StpLogic getStpLogicJwt() {
+        return new StpLogicJwtForStateless();
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

四、开始使用

+

然后我们就可以像之前一样使用 Sa-Token 了

+
/**
+ * 登录测试 
+ */
+@RestController
+@RequestMapping("/acc/")
+public class LoginController {
+
+    // 测试登录
+    @RequestMapping("login")
+    public SaResult login() {
+        StpUtil.login(10001);
+        return SaResult.ok("登录成功");
+    }
+
+    // 查询登录状态
+    @RequestMapping("isLogin")
+    public SaResult isLogin() {
+        return SaResult.ok("是否登录:" + StpUtil.isLogin());
+    }
+
+    // 测试注销
+    @RequestMapping("logout")
+    public SaResult logout() {
+        StpUtil.logout();
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
+

访问上述接口,观察Token生成的样式

+
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbklkIjoiMTAwMDEiLCJybiI6IjZYYzgySzBHVWV3Uk5NTTl1dFdjbnpFZFZHTVNYd3JOIn0.F_7fbHsFsDZmckHlGDaBuwDotZwAjZ0HB14DRujQfOQ
  • 1
+

五、不同模式策略对比

+

注入不同模式会让框架具有不同的行为策略,以下是三种模式的差异点(为方便叙述,以下比较以同时引入 jwt 与 Redis 作为前提):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
功能点Simple 简单模式Mixin 混入模式Stateless 无状态模式
Token风格jwt风格jwt风格jwt风格
登录数据存储Redis中存储Token中存储Token中存储
Session存储Redis中存储Redis中存储无Session
注销下线前后端双清数据前后端双清数据前端清除数据
踢人下线API支持不支持不支持
顶人下线API支持不支持不支持
登录认证支持支持支持
角色认证支持支持支持
权限认证支持支持支持
timeout 有效期支持支持支持
activity-timeout 有效期支持支持不支持
id反查Token支持支持不支持
会话管理支持部分支持不支持
注解鉴权支持支持支持
路由拦截鉴权支持支持支持
账号封禁支持支持不支持
身份切换支持支持支持
二级认证支持支持支持
模式总结Token风格替换jwt 与 Redis 逻辑混合完全舍弃Redis,只用jwt
+

六、扩展参数

+

你可以通过以下方式在登录时注入扩展参数:

+
// 登录10001账号,并为生成的 Token 追加扩展参数name
+StpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan"));
+
+// 连缀写法追加多个
+StpUtil.login(10001, SaLoginConfig
+                .setExtra("name", "zhangsan")
+                .setExtra("age", 18)
+                .setExtra("role", "超级管理员"));
+
+// 获取扩展参数 
+String name = StpUtil.getExtra("name");
+
+// 获取任意 Token 的扩展参数 
+String name = StpUtil.getExtra("tokenValue", "name");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

七、在多账户模式中集成 jwt

+

sa-token-jwt 插件默认只为 StpUtil 注入 StpLogicJwtFoxXxx 实现,自定义的 StpUserUtil 是不会自动注入的,我们需要帮其手动注入:

+
/**
+ * 为 StpUserUtil 注入 StpLogicJwt 实现 
+ */
+@Autowired
+public void setUserStpLogic() {
+    StpUserUtil.setStpLogic(new StpLogicJwtForSimple(StpUserUtil.TYPE));
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

八、自定义 SaJwtUtil 生成 token 的算法

+

如果需要自定义生成 token 的算法(例如更换sign方式),直接重写 SaJwtTemplate 对象即可:

+
/**
+ * 自定义 SaJwtUtil 生成 token 的算法 
+ */
+@Autowired
+public void setSaJwtTemplate() {
+    SaJwtUtil.setSaJwtTemplate(new SaJwtTemplate() {
+        @Override
+        public String generateToken(JWT jwt, String keyt) {
+            System.out.println("------ 自定义了 token 生成算法");
+            return super.generateToken(jwt, keyt);
+        }
+    });
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

九、注意点

+
1、使用 jwt-simple 模式后,is-share=false 恒等于 false。
+

is-share=true 的意思是每次登录都产生一样的 token,这种策略和 [ 为每个 token 单独设定 setExtra 数据 ] 不兼容的,
+为保证正确设定 Extra 数据,当使用 jwt-simple 模式后,is-share 配置项 恒等于 false

+
2、使用 jwt-mixin 模式后,is-concurrent 必须为 true。
+

is-concurrent=false 代表每次登录都把旧登录顶下线,但是 jwt-mixin 模式登录的 token 并不会记录在持久库数据中,
+技术上来讲无法将其踢下线,所以此时顶人下线和踢人下线等 API 都属于不可用状态,所以此时 is-concurrent 配置项必须配置为 true

+
+

参考资料

+ +
+← Sa-Token 多账号认证:同时为系统的 Admin 账号和 User 账号提供鉴权操作 +SpringBoot 使用 Sa-Token 完成权限认证 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/shi-yong-sa-token-shi-xian-bu-tong-de-deng-lu-mo-shi-dan-di-deng-lu-duo-di-deng.html b/sa-token-doc/blog/essays/shi-yong-sa-token-shi-xian-bu-tong-de-deng-lu-mo-shi-dan-di-deng-lu-duo-di-deng.html new file mode 100644 index 000000000..9b9b1c499 --- /dev/null +++ b/sa-token-doc/blog/essays/shi-yong-sa-token-shi-xian-bu-tong-de-deng-lu-mo-shi-dan-di-deng-lu-duo-di-deng.html @@ -0,0 +1,437 @@ + + + + + 使用 Sa-Token 实现不同的登录模式:单地登录、多地登录、同端互斥登录 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

使用 Sa-Token 实现不同的登录模式:单地登录、多地登录、同端互斥登录

+ +

一、需求分析

+

如果你经常使用腾讯QQ,就会发现它的登录有如下特点:它可以手机电脑同时在线,但是不能在两个手机上同时登录一个账号。

+

同端互斥登录,指的就是:像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线。

+

动态演示图:

+

同端互斥登录

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

本文将介绍在 Sa-Token 中,如何实现以下登录策略:

+
    +
  • 单地登录:指一个账号同一时间只能在一个地方登录,新登录会挤掉旧登录,也可以叫:单端登录。
  • +
  • 多地登录:指一个账号同一时间可以在不同地方登录,新登录会和旧登录共存,也可以叫:多端登录。
  • +
  • 同端互斥登录:在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线,参考腾讯QQ的登录模式:手机和电脑可以同时在线,但不能两个手机同时在线。
  • +
+

与之对应的,注销策略也将分为以下几种:
+- 单端注销:只在调用退出的一端注销。
+- 全端注销:一端注销,全端下线。
+- 同端注销:例如将所有手机端注销下线,PC端不受影响。

+

二、多地登录

+

此模式较为简单,Sa-Token 默认模式即为多地登录模式。

+
1、首先引入 Sa-Token 依赖:
+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+
2、在用户登录时将账号id写入会话中
+
@RestController
+@RequestMapping("/user/")
+public class UserController {
+    @RequestMapping("doLogin")
+    public SaResult doLogin(String username, String password) {
+        // 此处仅作示例模拟,真实项目需要从数据库中查询数据进行比对 
+        if("zhang".equals(username) && "123456".equals(password)) {
+            StpUtil.login(10001);
+            return SaResult.ok("登录成功");
+        }
+        return SaResult.ok("登录失败");
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

启动类:

+
@SpringBootApplication
+public class SaTokenDemoApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaTokenDemoApplication.class, args); 
+        System.out.println("\n启动成功:Sa-Roken 配置如下:" + SaManager.getConfig());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

如上代码,在多人登录同一账号时将不会对旧会话做任何处理,同一账号可以在多个地点任意登录,互不影响。

+
3、如果要全端注销,可以调用 logout 方法:
+
// 会话注销
+@RequestMapping("logout")
+public SaResult logout() {
+    StpUtil.logout();
+    return SaResult.ok("退出登录成功");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

调用如上方法注销后,当前账号所有端将一起下线。

+
4、单端注销
+

如果要只注销一端,可将配置文件中 is-share 的值配置为 false

+
sa-token:
+    is-share: false
  • 1
  • 2
+

此配置项的含义为:在多人登录同一账号时,是否共用一个 Token。

+
    +
  • 为 true 时:所有登录共用一个 Token。
  • +
  • 为 false 时:每次登录新建一个 Token。
  • +
+

此值为 false 后,每次登录都将返回不同的 Token,与之对应的,调用 StpUtil.logout() 也只会注销掉当前的 Token,其他端不受影响。

+

三、单地登录

+

单地登录的重点是需要改一下 yml 配置文件:

+
sa-token: 
+    is-concurrent: false
  • 1
  • 2
+

is-concurrent 的含义为是否允许同一账号并发登录:
+- 为 true 时:允许一起登录。
+- 为 false 时:新登录挤掉旧登录。

+

其它代码与 [多地登录] 无异,当我们在两个浏览器分别登录同一账号时,旧会话再次访问系统将会得到如下提示:

+
{
+    "code": 401,
+    "msg": "Token 已被顶下线",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

在 单地登录 模式中,不存在注销策略的问题,因为同一时间内,一个账号最多在一个设备在线,只要调用注销,就必然是全端下线。

+

四、同端互斥登录

+

好了,终于轮到主角出场,同端互斥登录可以让我们像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线。

+

那么在 Sa-Token 中如何做到同端互斥登录呢?

+

首先如 单地登录一样,在配置文件中,将 sa-token.is-concurrent 配置为false,然后调用登录等相关接口时声明设备标识即可:

+
1、指定设备标识登录
+
StpUtil.login(10001, "PC");    
  • 1
+

调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 NotLoginException 异常,场景值=-4

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
场景值对应常量含义说明
-1NotLoginException.NOT_TOKEN未能从请求中读取到 Token
-2NotLoginException.INVALID_TOKEN已读取到 Token,但是 Token无效
-3NotLoginException.TOKEN_TIMEOUT已读取到 Token,但是 Token已经过期
-4NotLoginException.BE_REPLACED已读取到 Token,但是 Token 已被顶下线
-5NotLoginException.KICK_OUT已读取到 Token,但是 Token 已被踢下线
+

如果第二个参数填写null或不填,代表将这个账号id所有在线端踢下线,被踢出者再次访问系统时会抛出 NotLoginException 异常,场景值=-5

+
2、查询当前登录的设备标识
+
StpUtil.getLoginDevice(); 
  • 1
+

如果在登录时未指定设备类型值,调用此方法将返回默认值:default-device

+
3、指定设备端类型下线
+

业务场景举例:在手机端控制PC端下线(手机端本身不受影响)

+
StpUtil.logout(10001, "PC");        
  • 1
+
4、全端下线
+

在调用 logout 方法时,不填写具体的设备端类型,将默认控制所有端一起下线。

+
StpUtil.logout(10001);      
  • 1
+

以上就是 Sa-Token 框架在处理登录问题时的各种方案,可以看出不管是简单的多地登录还是复杂的同端互斥登录,在 Sa-Token 都有完善的解决方案。

+
+

参考资料

+ +
+← 使用 Sa-Token 实现 [记住我] 模式登录、七天免登录 +别人绕过我的网关直接调用资源服务怎么办?使用 Sa-Token 解决:网关转发鉴权、RPC调用鉴权 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/shi-yong-sa-token-shi-xian-ji-zhu-wo-mo-shi-deng-lu-qi-tian-mian-deng-lu.html b/sa-token-doc/blog/essays/shi-yong-sa-token-shi-xian-ji-zhu-wo-mo-shi-deng-lu-qi-tian-mian-deng-lu.html new file mode 100644 index 000000000..7d5065ae9 --- /dev/null +++ b/sa-token-doc/blog/essays/shi-yong-sa-token-shi-xian-ji-zhu-wo-mo-shi-deng-lu-qi-tian-mian-deng-lu.html @@ -0,0 +1,419 @@ + + + + + 使用 Sa-Token 实现 [记住我] 模式登录、七天免登录 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

使用 Sa-Token 实现 [记住我] 模式登录、七天免登录

+ +

一、需求分析

+

如图所示,一般网站的登录界面都会有一个 [记住我] 按钮,当你勾选它登录后,即使你关闭浏览器再次打开网站,也依然会处于登录状态,无须重复验证密码:

+

../static/login-view.png

+

本文将详细介绍在 Sa-Token中,如何做到以下登录模式:
+- 记住我登录:登录后关闭浏览器,再次打开网站登录状态依然有效,无需重复登录。
+- 仅本次有效登录:登录后关闭浏览器,再次打开网站登录状态将失效,需要再次登录。
+- 七天免登录:为登录状态设定一个详细的有效期,在这个期限内无需重复登录,过了期限后需要再次登录。

+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。

+
+

Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+

二、在 Sa-Token 中实现记住我功能

+

Sa-Token的登录授权,默认就是[记住我]模式,为了实现[非记住我]模式,你需要在登录时如下设置:

+
// 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录
+StpUtil.login(10001, false);
  • 1
  • 2
+

那么,Sa-Token实现[记住我]的具体原理是?

+

三、实现原理

+

Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是:
+- 临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失。
+- 持久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失。

+

利用Cookie的此特性,我们便可以轻松实现 [记住我] 模式:
+- 勾选 [记住我] 按钮时:调用StpUtil.login(10001, true),在浏览器写入一个持久Cookie储存 Token,此时用户即使重启浏览器 Token 依然有效。
+- 不勾选 [记住我] 按钮时:调用StpUtil.login(10001, false),在浏览器写入一个临时Cookie储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。

+

动态演示图:

+

记住我登录

+

四、前后端分离模式下如何实现[记住我]?

+

此时机智的你😏很快发现一个问题,Cookie虽好,却无法在前后端分离环境下使用,那是不是代表上述方案在APP、小程序等环境中无效?

+

准确的讲,答案是肯定的,任何基于Cookie的认证方案在前后端分离环境下都会失效(原因在于这些客户端默认没有实现Cookie功能),不过好在,这些客户端一般都提供了替代方案,
+唯一遗憾的是,此场景中token的生命周期需要我们在前端手动控制:

+

以经典跨端框架 uni-app 为例,我们可以使用如下方式达到同样的效果:

+
// 使用本地存储保存token,达到 [持久Cookie] 的效果
+uni.setStorageSync("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
+
+// 使用globalData保存token,达到 [临时Cookie] 的效果
+getApp().globalData.satoken = "xxxx-xxxx-xxxx-xxxx-xxx";
  • 1
  • 2
  • 3
  • 4
  • 5
+

如果你决定在PC浏览器环境下进行前后端分离模式开发,那么更加简单:

+
// 使用 localStorage 保存token,达到 [持久Cookie] 的效果
+localStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
+
+// 使用 sessionStorage 保存token,达到 [临时Cookie] 的效果
+sessionStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
  • 1
  • 2
  • 3
  • 4
  • 5
+

Remember me, it's too easy!

+

五、登录时指定 Token 有效期

+

登录时不仅可以指定是否为[记住我]模式,还可以指定一个特定的时间作为 Token 有效时长,如下示例:

+
// 示例1:
+// 指定token有效期(单位: 秒),如下所示token七天有效
+StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));
+
+// ----------------------- 示例2:所有参数
+// `SaLoginModel`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如:
+StpUtil.login(10001, new SaLoginModel()
+            .setDevice("PC")                 // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型
+            .setIsLastingCookie(true)        // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
+            .setTimeout(60 * 60 * 24 * 7)    // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值)
+            .setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token 
+            .setIsWriteHeader(false)         // 是否在登录后将 Token 写入到响应头
+            );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

注:如果在登录时未指定 new SaLoginModel().setTimeout(604800) 那么框架将采用全局配置的 sa-token.timeout 值作为 Token 的有效期。

+

六、不同登录策略的代码对比

+

以下是三种登录策略的代码差异:

+
package com.pj.cases.up;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * Sa-Token 记住我模式登录 
+ * 
+ * @author kong
+ * @since 2022-10-17 
+ */
+@RestController
+@RequestMapping("/RememberMe/")
+public class RememberMeController {
+
+    // 记住我登录    ---- http://localhost:8081/RememberMe/doLogin?name=zhang&pwd=123456
+    @RequestMapping("doLogin")
+    public SaResult doLogin(String name, String pwd) {
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+            StpUtil.login(10001, true);
+            return SaResult.ok("登录成功");
+        }
+        return SaResult.error("登录失败");
+    }
+
+    // 不记住我登录    ---- http://localhost:8081/RememberMe/doLogin2?name=zhang&pwd=123456
+    @RequestMapping("doLogin2")
+    public SaResult doLogin2(String name, String pwd) {
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+            StpUtil.login(10001, false);
+            return SaResult.ok("登录成功");
+        }
+        return SaResult.error("登录失败");
+    }
+
+    // 七天免登录    ---- http://localhost:8081/RememberMe/doLogin3?name=zhang&pwd=123456
+    @RequestMapping("doLogin3")
+    public SaResult doLogin3(String name, String pwd) {
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+            StpUtil.login(10001, 60 * 60 * 24 * 7);
+            return SaResult.ok("登录成功");
+        }
+        return SaResult.error("登录失败");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
+

可依次访问注释中提供的测试链接,观察不同登录策略带来的会话有效期差异。

+
+

参考资料

+ +
+← 使用 Sa-Token 完成踢人下线功能 +使用 Sa-Token 实现不同的登录模式:单地登录、多地登录、同端互斥登录 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/shi-yong-sa-token-wan-cheng-ti-ren-xia-xian-gong-neng.html b/sa-token-doc/blog/essays/shi-yong-sa-token-wan-cheng-ti-ren-xia-xian-gong-neng.html new file mode 100644 index 000000000..e9315c2f3 --- /dev/null +++ b/sa-token-doc/blog/essays/shi-yong-sa-token-wan-cheng-ti-ren-xia-xian-gong-neng.html @@ -0,0 +1,504 @@ + + + + + 使用 Sa-Token 完成踢人下线功能 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

使用 Sa-Token 完成踢人下线功能

+ +

一、需求

+

在企业级项目中,踢人下线是一个很常见的需求,如果要设计比较完善的话,至少需要以下功能点:

+
    +
  • 可以根据用户 userId 踢出指定会话,对方再次访问系统会被提示:您已被踢下线,请重新登录。
  • +
  • 可以查询出一个账号共在几个设备端登录,并返回其对应的 Token 凭证,以便后续操作。
  • +
  • 可以只踢出一个账号某一个端的会话,其他端不受影响。例如在某电商APP上可以看到当前账号共在几个手机上登录,并注销指定端的会话,当前端不受影响。
  • +
+

手动从零开始设计满足需求的会话架构,还是需要一定的代码量的。本篇将介绍如何使用 Sa-Token 方便的完成上述需求,
+Sa-Token 框架对踢人下线做了较为完整的封装,我们可以使用极少的代码就完成踢人下线功能。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+

二、踢人下线 API 一览

+

先看看 Sa-Token 为我们提供的与踢人下线有关的API。

+

强制注销:

+
StpUtil.logout(10001);                    // 强制指定账号注销下线 
+StpUtil.logout(10001, "PC");              // 强制指定账号指定端注销下线 
+StpUtil.logoutByTokenValue("token");      // 强制指定 Token 注销下线 
  • 1
  • 2
  • 3
+

踢人下线:

+
StpUtil.kickout(10001);                    // 将指定账号踢下线 
+StpUtil.kickout(10001, "PC");              // 将指定账号指定端踢下线
+StpUtil.kickoutByTokenValue("token");      // 将指定 Token 踢下线
  • 1
  • 2
  • 3
+

强制注销 和 踢人下线 的区别在于:
+- 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
+- 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。

+

动态图演示:

+

强制注销 和 踢人下线

+

下面开始进行代码实战。

+

三、根据账号踢人下线

+

在完成踢人下线之前,我们需要先让会话完成登录。正常的登录需要根据 username + password 判断账号合法性,由于我们本篇的重点是 踢人下线
+所以此处简化一下登录操作,直接填入 userId 进行登录。

+
package com.pj;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 测试踢人下线
+ */
+@RestController
+@RequestMapping("/kick/")
+public class KickController {
+
+    // 会话登录接口  ---- http://localhost:8081/kick/doLogin?id=10001
+    @RequestMapping("doLogin")
+    public SaResult doLogin(long userId) {
+        StpUtil.login(userId);
+        return SaResult.ok("登录成功,Token 凭证为:" + StpUtil.getTokenValue());
+    }
+
+    // 验证当前客户端是否登录  ---- http://localhost:8081/kick/checkLogin
+    @RequestMapping("checkLogin")
+    public SaResult checkLogin() {
+        StpUtil.checkLogin();
+        // 下面是登录后才会返回的数据
+        return SaResult.ok("您已登录成功,userId=" + StpUtil.getLoginId());
+    }
+
+    // 根据账号Id踢人下线  ---- http://localhost:8081/kick/kickout
+    @RequestMapping("kickout")
+    public SaResult kickout(long userId) {
+        StpUtil.kickout(userId);
+        return SaResult.ok("将账号 " + userId + " 踢下线成功");
+    }
+
+    // 全局异常拦截
+    @ExceptionHandler
+    public SaResult handlerException(Exception e) {
+        e.printStackTrace();
+        return SaResult.error(e.getMessage());
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
+

运行代码,分别用三个独立的浏览器测试登录:

+
// 使用浏览器 1 测试登录账号 10001
+http://localhost:8081/kick/doLogin?userId=10001
+
+// 使用浏览器 2 测试登录账号 10002
+http://localhost:8081/kick/doLogin?userId=10002
+
+// 使用浏览器 3 测试登录账号 10003
+http://localhost:8081/kick/doLogin?userId=10003
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

之所以使用三个独立的浏览器来测试,是为了避免会话的相互覆盖,造成逻辑不可控。访问成功的话,服务端的返回信息会类似如下:

+
{
+    "code": 200,
+    "msg": "登录成功,Token 凭证为:f53ac098-aed4-4de2-9223-8c3f1dab656d",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

然后使用三个浏览器分别访问登录验证接口:

+
http://localhost:8081/kick/checkLogin
  • 1
+

返回信息如下:

+
{
+    "code": 200,
+    "msg": "您已登录成功,userId=10001",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

现在开始测试踢人下线,使用任意浏览器访问:

+
http://localhost:8081/kick/kickout?userId=10002
  • 1
+

返回信息:

+
{
+    "code": 200,
+    "msg": "将账号 10002 踢下线成功",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

账号 10002 将被踢下线成功,现在我们再使用浏览器2 测试一下 10002 是否仍然在线:

+
{
+    "code": 500,
+    "msg": "Token已被踢下线:aa5911a6-3623-4fdb-98d0-055c46353981",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

可以看到,10002会话已失效,无法通过登录校验。

+

四、根据 Token 踢人下线

+

业务场景举例:我要在APP上查看我的账号共在几个设备登录,并且将除我之外的设备全部踢下线。

+

首先我们需要在 application.yml 中添加配置:

+
sa-token: 
+    is-share: false
  • 1
  • 2
+

is-share 的含义是:在多人登录同一账号时,是否共用同一个 Token:
+- 此值为 true 时,所有登录共用一个Token。
+- 此值为 false 时,每次登录新建一个Token。

+

在以上 KickController 的基础上,继续添加接口:

+
/**
+ * 测试踢人下线
+ */
+@RestController
+@RequestMapping("/kick/")
+public class KickController {
+
+    // 其他代码...
+
+    // 以下是需要新添加的代码
+
+    // 查询我的账号已经在几个设备登录  ---- http://localhost:8081/kick/tokenList
+    @RequestMapping("tokenList")
+    public SaResult tokenList() {
+        long currUserId = StpUtil.getLoginIdAsLong();
+        List<String> tokenList = StpUtil.getTokenValueListByLoginId(currUserId);
+        return SaResult.data(tokenList);
+    }
+
+    // 根据 Token 踢人下线  ---- http://localhost:8081/kick/kickoutToken?token=xxxx
+    @RequestMapping("kickoutToken")
+    public SaResult kickoutToken(String token) {
+        StpUtil.kickoutByTokenValue(token);
+        return SaResult.ok("将Token: " + token + " 踢下线成功");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
+

重启项目(如果集成 Redis 了就清空 Redis数据一下),分别从三个独立的浏览器测试访问:

+
http://localhost:8081/kick/doLogin?userId=10001
  • 1
+

返回如下:

+
{
+    "code": 200,
+    "msg": "登录成功,Token 凭证为:450b8b73-f52d-4496-b67e-bdd579c8708a",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

仔细观察三个浏览器返回的信息,虽然三个浏览器都是登录账号 10001,但是每次返回的 Token 凭证都是不一样的。

+

现在查询一下当前账号一共在几个设备完成了登录:

+
http://localhost:8081/kick/tokenList
  • 1
+

返回如下:

+
{
+    "code": 200,
+    "msg": "ok",
+    "data": [
+        "450b8b73-f52d-4496-b67e-bdd579c8708a",
+        "39d7974b-327d-4aea-a0b7-d90ab47caf0c",
+        "d73c1bc5-d04f-4dc2-81ee-42c9438f9d78"
+    ]
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
+

现在选一个 Token,将其踢下线:

+
http://localhost:8081/kick/kickoutToken?token=d73c1bc5-d04f-4dc2-81ee-42c9438f9d78
  • 1
+

返回信息如下:

+
{
+    "code": 200,
+    "msg": "将Token: d73c1bc5-d04f-4dc2-81ee-42c9438f9d78 踢下线成功",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

然后在对应的浏览器,验证一下登录状态:

+
http://localhost:8081/kick/checkLogin
  • 1
+

返回如下:

+
{
+    "code": 500,
+    "msg": "Token已被踢下线:d73c1bc5-d04f-4dc2-81ee-42c9438f9d78",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

可以看到,该设备登录的会话已被踢下线。那么同账号的其他设备有没有受到影响呢,我们从其他浏览器验证一下:

+
http://localhost:8081/kick/checkLogin
  • 1
+

返回如下:

+
{
+    "code": 200,
+    "msg": "您已登录成功,userId=10001",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

可以看到,只有踢出的 Token 被强制下线了,其他端并没有受到影响。

+
+

参考资料

+ +
+← 不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现 +使用 Sa-Token 实现 [记住我] 模式登录、七天免登录 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-shi-xian-zhang-hao-feng-jin-fen-lei-feng-jin-jie-ti.html b/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-shi-xian-zhang-hao-feng-jin-fen-lei-feng-jin-jie-ti.html new file mode 100644 index 000000000..026922b6a --- /dev/null +++ b/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-shi-xian-zhang-hao-feng-jin-fen-lei-feng-jin-jie-ti.html @@ -0,0 +1,567 @@ + + + + + SpringBoot 使用 Sa-Token 实现账号封禁、分类封禁、阶梯封禁 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

SpringBoot 使用 Sa-Token 实现账号封禁、分类封禁、阶梯封禁

+ +

一、需求分析

+

之前的章节中,我们学习了 踢人下线 和 强制注销 功能,用于清退违规账号。在部分场景下,我们还需要将其 账号封禁,以防止其再次登录。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

Sa-Token 提供的封禁操作有三种:

+
    +
  • 账号封禁:封禁掉一个账号的登录能力,使其无法登录。
  • +
  • 分类封禁:封禁掉一个账号的部分业务操作权限,不影响账号的整体登录等基础功能。
  • +
  • 阶梯封禁:按照不同的违规程度,给与其不同的封禁力度。
  • +
+

本篇文章将介绍在 Sa-Token 中如何完成上述三种封禁操作。

+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+

二、账号封禁

+

对指定账号进行封禁:

+
// 封禁指定账号 
+StpUtil.disable(10001, 86400); 
  • 1
  • 2
+

参数含义:
+- 参数1:要封禁的账号id。
+- 参数2:封禁时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。

+

注意点:对于正在登录的账号,将其封禁并不会使它立即掉线,如果我们需要它即刻下线,可采用先踢再封禁的策略,例如:

+
// 先踢下线
+StpUtil.kickout(10001); 
+// 再封禁账号
+StpUtil.disable(10001, 86400); 
  • 1
  • 2
  • 3
  • 4
+

待到下次登录时,我们先校验一下这个账号是否已被封禁:

+
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
+StpUtil.checkDisable(10001); 
+
+// 通过校验后,再进行登录:
+StpUtil.login(10001); 
  • 1
  • 2
  • 3
  • 4
  • 5
+
+

旧版本在 StpUtil.login() 时会自动校验账号是否被封禁,v1.31.0 之后将 校验封禁 和 登录 两个动作分离成两个方法,不再自动校验,请注意其中的逻辑更改。

+
+

此模块所有方法:

+
// 封禁指定账号 
+StpUtil.disable(10001, 86400); 
+
+// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁) 
+StpUtil.isDisable(10001); 
+
+// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
+StpUtil.checkDisable(10001); 
+
+// 获取指定账号剩余封禁时间,单位:秒,如果该账号未被封禁,则返回-2 
+StpUtil.getDisableTime(10001); 
+
+// 解除封禁
+StpUtil.untieDisable(10001); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

三、分类封禁

+

有的时候,我们并不需要将整个账号禁掉,而是只禁止其访问部分服务。

+

假设我们在开发一个电商系统,对于违规账号的处罚,我们设定三种分类封禁:

+
    +
  • 1、封禁评价能力:账号A 因为多次虚假好评,被限制订单评价功能。
  • +
  • 2、封禁下单能力:账号B 因为多次薅羊毛,被限制下单功能。
  • +
  • 3、封禁开店能力:账号C 因为店铺销售假货,被限制开店功能。
  • +
+

相比于封禁账号的一刀切处罚,这里的关键点在于:每一项能力封禁的同时,都不会对其它能力造成影响。

+

也就是说我们需要一种只对部分服务进行限制的能力,对应到代码层面,就是只禁止部分接口的调用。

+
// 封禁指定用户评论能力,期限为 1天
+StpUtil.disable(10001, "comment", 86400);
  • 1
  • 2
+

参数释义:
+- 参数1:要封禁的账号id。
+- 参数2:针对这个账号,要封禁的服务标识(可以是任意的自定义字符串)。
+- 参数3:要封禁的时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。

+

分类封禁模块所有可用API:

+
/*
+ * 以下示例中:"comment"=评论服务标识、"place-order"=下单服务标识、"open-shop"=开店服务标识
+ */
+
+// 封禁指定用户评论能力,期限为 1天
+StpUtil.disable(10001, "comment", 86400);
+
+// 在评论接口,校验一下,会抛出异常:`DisableServiceException`,使用 e.getService() 可获取业务标识 `comment` 
+StpUtil.checkDisable(10001, "comment");
+
+// 在下单时,我们校验一下 下单能力,并不会抛出异常,因为我们没有限制其下单功能
+StpUtil.checkDisable(10001, "place-order");
+
+// 现在我们再将其下单能力封禁一下,期限为 7天 
+StpUtil.disable(10001, "place-order", 86400 * 7);
+
+// 然后在下单接口,我们添加上校验代码,此时用户便会因为下单能力被封禁而无法下单(代码抛出异常)
+StpUtil.checkDisable(10001, "place-order");
+
+// 但是此时,用户如果调用开店功能的话,还是可以通过,因为我们没有限制其开店能力 (除非我们再调用了封禁开店的代码)
+StpUtil.checkDisable(10001, "open-shop");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
+

通过以上示例,你应该大致可以理解 业务封禁 -> 业务校验 的处理步骤。

+

有关分类封禁的所有方法:

+
// 封禁:指定账号的指定服务 
+StpUtil.disable(10001, "<业务标识>", 86400); 
+
+// 判断:指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁) 
+StpUtil.isDisable(10001, "<业务标识>"); 
+
+// 校验:指定账号的指定服务 是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
+StpUtil.checkDisable(10001, "<业务标识>"); 
+
+// 获取:指定账号的指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁)
+StpUtil.getDisableTime(10001, "<业务标识>"); 
+
+// 解封:指定账号的指定服务
+StpUtil.untieDisable(10001, "<业务标识>"); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

四、阶梯封禁

+

对于多次违规的用户,我们常常采取阶梯处罚的策略,这种 “阶梯” 一般有两种形式:

+
    +
  • 处罚时间阶梯:首次违规封禁 1 天,第二次封禁 7 天,第三次封禁 30 天,依次顺延……
  • +
  • 处罚力度阶梯:首次违规消息提醒、第二次禁言禁评论、第三次禁止账号登录,等等……
  • +
+

基于处罚时间的阶梯,我们只需在封禁时 StpUtil.disable(10001, 86400) 传入不同的封禁时间即可,下面我们着重探讨一下基于处罚力度的阶梯形式。

+

假设我们在开发一个论坛系统,对于违规账号的处罚,我们设定三种力度:

+
    +
  • 1、轻度违规:封禁其发帖、评论能力,但允许其点赞、关注等操作。
  • +
  • 2、中度违规:封禁其发帖、评论、点赞、关注等一切与别人互动的能力,但允许其浏览帖子、浏览评论。
  • +
  • 3、重度违规:封禁其登录功能,限制一切能力。
  • +
+

解决这种需求的关键在于,我们需要把不同处罚力度,量化成不同的处罚等级,比如上述的 轻度中度重度 3 个力度,
+我们将其量化为一级封禁二级封禁三级封禁 3个等级,数字越大代表封禁力度越高。

+

然后我们就可以使用阶梯封禁的API,进行鉴权了:

+
// 阶梯封禁,参数:封禁账号、封禁级别、封禁时间 
+StpUtil.disableLevel(10001, 3, 10000);
+
+// 获取:指定账号封禁的级别 (如果此账号未被封禁则返回 -2)
+StpUtil.getDisableLevel(10001);
+
+// 判断:指定账号是否已被封禁到指定级别,返回 true 或 false
+StpUtil.isDisableLevel(10001, 3);
+
+// 校验:指定账号是否已被封禁到指定级别,如果已达到此级别(例如已被3级封禁,这里校验是否达到2级),则抛出异常 `DisableServiceException`
+StpUtil.checkDisableLevel(10001, 2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

注意点:DisableServiceException 异常代表当前账号未通过封禁校验,可以:
+- 通过 e.getLevel() 获取这个账号实际被封禁的等级。
+- 通过 e.getLimitLevel() 获取这个账号在校验时要求低于的等级。当 Level >= LimitLevel 时,框架就会抛出异常。

+

如果业务足够复杂,我们还可能将 分类封禁 和 阶梯封禁 组合使用:

+
// 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间 
+StpUtil.disableLevel(10001, "comment", 3, 10000);
+
+// 获取:指定账号的指定服务 封禁的级别 (如果此账号未被封禁则返回 -2)
+StpUtil.getDisableLevel(10001, "comment");
+
+// 判断:指定账号的指定服务 是否已被封禁到指定级别,返回 true 或 false
+StpUtil.isDisableLevel(10001, "comment", 3);
+
+// 校验:指定账号的指定服务 是否已被封禁到指定级别(例如 comment服务 已被3级封禁,这里校验是否达到2级),如果已达到此级别,则抛出异常 
+StpUtil.checkDisableLevel(10001, "comment", 2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

五、使用注解完成封禁校验

+

首先我们需要注册 Sa-Token 全局拦截器:

+
@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");    
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

然后我们就可以使用以下注解校验账号是否封禁:

+
// 校验当前账号是否被封禁,如果已被封禁会抛出异常,无法进入方法 
+@SaCheckDisable
+@PostMapping("send")
+public SaResult send() {
+    // ... 
+    return SaResult.ok(); 
+}
+
+// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 
+@SaCheckDisable("comment")
+@PostMapping("send")
+public SaResult send() {
+    // ... 
+    return SaResult.ok(); 
+}
+
+// 校验当前账号是否被封禁 comment、place-order、open-shop 等服务,指定多个值,只要有一个已被封禁,就无法进入方法 
+@SaCheckDisable({"comment", "place-order", "open-shop"})
+@PostMapping("send")
+public SaResult send() {
+    // ... 
+    return SaResult.ok(); 
+}
+
+// 阶梯封禁,校验当前账号封禁等级是否达到5级,如果达到则抛出异常 
+@SaCheckDisable(level = 5)
+@PostMapping("send")
+public SaResult send() {
+    // ... 
+    return SaResult.ok(); 
+}
+
+// 分类封禁 + 阶梯封禁 校验:校验当前账号的 comment 服务,封禁等级是否达到5级,如果达到则抛出异常 
+@SaCheckDisable(value = "comment", level = 5)
+@PostMapping("send")
+public SaResult send() {
+    // ... 
+    return SaResult.ok(); 
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
+

六、来个小示例,加深理解

+
package com.pj.cases.up;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * Sa-Token 账号封禁示例 
+ * 
+ * @author kong
+ * @since 2022-10-17 
+ */
+@RestController
+@RequestMapping("/disable/")
+public class DisableController {
+
+    // 会话登录接口  ---- http://localhost:8081/disable/login?userId=10001
+    @RequestMapping("login")
+    public SaResult login(long userId) {
+        // 1、先检查此账号是否已被封禁 
+        StpUtil.checkDisable(userId);
+        // 2、检查通过后,再登录 
+        StpUtil.login(userId);
+        return SaResult.ok("账号登录成功");
+    }
+
+    // 会话注销接口  ---- http://localhost:8081/disable/logout
+    @RequestMapping("logout")
+    public SaResult logout() {
+        StpUtil.logout();
+        return SaResult.ok("账号退出成功");
+    }
+
+    // 封禁指定账号  ---- http://localhost:8081/disable/disable?userId=10001
+    @RequestMapping("disable")
+    public SaResult disable(long userId) {
+        /*
+         * 账号封禁:
+         *  参数1:要封禁的账号id
+         *  参数2:要封禁的时间,单位:秒,86400秒=1天
+         */
+        StpUtil.disable(userId, 86400);
+        return SaResult.ok("账号 " + userId + " 封禁成功");
+    }
+
+    // 解封指定账号  ---- http://localhost:8081/disable/untieDisable?userId=10001
+    @RequestMapping("untieDisable")
+    public SaResult untieDisable(long userId) {
+        StpUtil.untieDisable(userId);
+        return SaResult.ok("账号 " + userId + " 解封成功");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
+

测试步骤:
+1. 访问登录接口,可以正常登录 ---- http://localhost:8081/disable/login?userId=10001
+2. 注销登录 ---- http://localhost:8081/disable/logout
+3. 禁用账号 ---- http://localhost:8081/disable/disable?userId=10001
+4. 再次访问登录接口,登录失败 ---- http://localhost:8081/disable/login?userId=10001
+5. 解封账号 ---- http://localhost:8081/disable/untieDisable?userId=10001
+6. 再次访问登录接口,登录成功 ---- http://localhost:8081/disable/login?userId=10001

+
+

参考资料

+ +
+← SpringBoot 使用 Sa-Token 完成路由拦截鉴权 +SpringBoot 项目使用 Sa-Token 完成登录认证 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-lu-you-lan-jie-jian-quan.html b/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-lu-you-lan-jie-jian-quan.html new file mode 100644 index 000000000..b037d3958 --- /dev/null +++ b/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-lu-you-lan-jie-jian-quan.html @@ -0,0 +1,490 @@ + + + + + SpringBoot 使用 Sa-Token 完成路由拦截鉴权 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

SpringBoot 使用 Sa-Token 完成路由拦截鉴权

+ +

一、需求分析

+

在前文,我们详细的讲述了在 Sa-Token 如何使用注解进行权限认证,注解鉴权虽然方便,却并不适合所有鉴权场景。

+

假设有如下需求:项目中所有接口均需要登录认证校验,只有 “登录接口” 本身对外开放。

+

如果我们对项目所有接口都加上 @SaCheckLogin 注解,会显得非常冗余且没有必要,在这个需求中我们真正需要的是一种基于路由拦截的鉴权模式,那么在 Sa-Token 怎么实现路由拦截鉴权呢?

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

二、注册 Sa-Token 路由拦截器

+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+

新建配置类SaTokenConfigure.java

+
@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    // 注册拦截器
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
+        registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
+                .addPathPatterns("/**")
+                .excludePathPatterns("/user/doLogin"); 
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

以上代码,我们注册了一个基于 StpUtil.checkLogin() 的登录校验拦截器,并且排除了/user/doLogin接口用来开放登录(除了/user/doLogin以外的所有接口都需要登录才能访问)。

+
+

SaInterceptor 是新版本提供的拦截器,点此 查看旧版本代码迁移示例

+
+

三、校验函数详解

+

自定义认证规则:new SaInterceptor(handle -> StpUtil.checkLogin()) 是最简单的写法,代表只进行登录校验功能。

+

我们可以往构造函数塞一个完整的 lambda 表达式,来定义详细的校验规则,例如:

+
@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册 Sa-Token 拦截器,定义详细认证规则 
+        registry.addInterceptor(new SaInterceptor(handler -> {
+            // 指定一条 match 规则
+            SaRouter
+                .match("/**")   // 拦截的 path 列表,可以写多个 */
+                .notMatch("/user/doLogin")      // 排除掉的 path 列表,可以写多个 
+                .check(r -> StpUtil.checkLogin());      // 要执行的校验动作,可以写完整的 lambda 表达式
+
+            // 根据路由划分模块,不同模块不同鉴权 
+            SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
+            SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
+            SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
+            SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
+            SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
+            SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
+        })).addPathPatterns("/**");
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
+

SaRouter.match() 匹配函数有两个参数:
+- 参数一:要匹配的path路由。
+- 参数二:要执行的校验函数。

+

在校验函数内不只可以使用 StpUtil.checkPermission("xxx") 进行权限校验,你还可以写任意代码,例如:

+
@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    // 注册 Sa-Token 的拦截器
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册路由拦截器,自定义认证规则 
+        registry.addInterceptor(new SaInterceptor(handler -> {
+
+            // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 
+            SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
+
+            // 角色校验 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证 
+            SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));
+
+            // 权限校验 -- 不同模块校验不同权限 
+            SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
+            SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
+            SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
+            SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
+            SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
+            SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
+
+            // 甚至你可以随意的写一个打印语句
+            SaRouter.match("/**", r -> System.out.println("----啦啦啦----"));
+
+            // 连缀写法
+            SaRouter.match("/**").check(r -> System.out.println("----啦啦啦----"));
+
+        })).addPathPatterns("/**");
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
+

四、匹配特征详解

+

除了上述示例的 path 路由匹配,还可以根据很多其它特征进行匹配,以下是所有可匹配的特征:

+
// 基础写法样例:匹配一个path,执行一个校验函数 
+SaRouter.match("/user/**").check(r -> StpUtil.checkLogin());
+
+// 根据 path 路由匹配   ——— 支持写多个path,支持写 restful 风格路由 
+// 功能说明: 使用 /user , /goods 或者 /art/get 开头的任意路由都将进入 check 方法
+SaRouter.match("/user/**", "/goods/**", "/art/get/{id}").check( /* 要执行的校验函数 */ );
+
+// 根据 path 路由排除匹配 
+// 功能说明: 使用 .html , .css 或者 .js 结尾的任意路由都将跳过, 不会进入 check 方法
+SaRouter.match("/**").notMatch("*.html", "*.css", "*.js").check( /* 要执行的校验函数 */ );
+
+// 根据请求类型匹配 
+SaRouter.match(SaHttpMethod.GET).check( /* 要执行的校验函数 */ );
+
+// 根据一个 boolean 条件进行匹配 
+SaRouter.match( StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );
+
+// 根据一个返回 boolean 结果的lambda表达式匹配 
+SaRouter.match( r -> StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );
+
+// 多个条件一起使用 
+// 功能说明: 必须是 Get 请求 并且 请求路径以 `/user/` 开头 
+SaRouter.match(SaHttpMethod.GET).match("/user/**").check( /* 要执行的校验函数 */ );
+
+// 可以无限连缀下去 
+// 功能说明: 同时满足 Get 方式请求, 且路由以 /admin 开头, 路由中间带有 /send/ 字符串, 路由结尾不能是 .js 和 .css
+SaRouter
+    .match(SaHttpMethod.GET)
+    .match("/admin/**")
+    .match("/**/send/**") 
+    .notMatch("/**/*.js")
+    .notMatch("/**/*.css")
+    // ....
+    .check( /* 只有上述所有条件都匹配成功,才会执行最后的check校验函数 */ );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
+

五、提前退出匹配链

+

使用 SaRouter.stop() 可以提前退出匹配链,例:

+
registry.addInterceptor(new SaInterceptor(handler -> {
+    SaRouter.match("/**").check(r -> System.out.println("进入1"));
+    SaRouter.match("/**").check(r -> System.out.println("进入2")).stop();
+    SaRouter.match("/**").check(r -> System.out.println("进入3"));
+    SaRouter.match("/**").check(r -> System.out.println("进入4"));
+    SaRouter.match("/**").check(r -> System.out.println("进入5"));
+})).addPathPatterns("/**");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

如上示例,代码运行至第2条匹配链时,会在stop函数处提前退出整个匹配函数,从而忽略掉剩余的所有match匹配

+

除了stop()函数,SaRouter还提供了 back() 函数,用于:停止匹配,结束执行,直接向前端返回结果

+
// 执行back函数后将停止匹配,也不会进入Controller,而是直接将 back参数 作为返回值输出到前端
+SaRouter.match("/user/back").back("要返回到前端的内容");
  • 1
  • 2
+

stop() 与 back() 函数的区别在于:
+- SaRouter.stop() 会停止匹配,进入Controller。
+- SaRouter.back() 会停止匹配,直接返回结果到前端。

+

六、使用free打开一个独立的作用域

+
// 进入 free 独立作用域 
+SaRouter.match("/**").free(r -> {
+    SaRouter.match("/a/**").check(/* --- */);
+    SaRouter.match("/b/**").check(/* --- */).stop();
+    SaRouter.match("/c/**").check(/* --- */);
+});
+// 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配 
+SaRouter.match("/**").check(/* --- */);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

free() 的作用是:打开一个独立的作用域,使内部的 stop() 不再一次性跳出整个 Auth 函数,而是仅仅跳出当前 free 作用域。

+

七、使用注解忽略掉路由拦截校验

+

我们可以使用 @SaIgnore 注解,忽略掉路由拦截认证:

+

1、先配置好了拦截规则:

+
@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    registry.addInterceptor(new SaInterceptor(handler -> {
+        // 根据路由划分模块,不同模块不同鉴权 
+        SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
+        SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
+        SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
+        // ... 
+    })).addPathPatterns("/**");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

2、然后在 Controller 里又添加了忽略校验的注解

+
@SaIgnore
+@RequestMapping("/user/getList")
+public SaResult getList() {
+    System.out.println("------------ 访问进来方法"); 
+    return SaResult.ok(); 
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

请求将会跳过拦截器的校验,直接进入 Controller 的方法中。

+

注意点:此注解的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效,对自定义拦截器与过滤器不生效。

+

八、关闭注解校验

+

SaInterceptor 只要注册到项目中,默认就会打开注解校验,如果要关闭此能力,需要:

+
@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    registry.addInterceptor(
+        new SaInterceptor(handle -> {
+            SaRouter.match("/**").check(r -> StpUtil.checkLogin());
+        }).isAnnotation(false)  // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了 
+    ).addPathPatterns("/**");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+
+

参考资料

+ +
+← SpringBoot 使用 Sa-Token 完成注解鉴权功能 +SpringBoot 使用 Sa-Token 实现账号封禁、分类封禁、阶梯封禁 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-quan-xian-ren-zheng.html b/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-quan-xian-ren-zheng.html new file mode 100644 index 000000000..eba10e170 --- /dev/null +++ b/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-quan-xian-ren-zheng.html @@ -0,0 +1,576 @@ + + + + + SpringBoot 使用 Sa-Token 完成权限认证 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

SpringBoot 使用 Sa-Token 完成权限认证

+ +

一、设计思路

+

所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限:

+
    +
  • 有,就让你通过。
  • +
  • 没有?那么禁止访问!
  • +
+

深入到底层数据中,就是每个账号都会拥有一个权限码集合,框架来校验这个集合中是否包含指定的权限码。

+

例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"],这时候我来校验权限 "user-update",则其结果就是:验证失败,禁止访问

+

动态演示图:

+

权限认证

+

所以现在问题的核心就是:
+1. 如何获取一个账号所拥有的的权限码集合?
+2. 本次操作需要验证的权限码是哪个?

+

接下来,我们将介绍在 SpringBoot 中如何使用 Sa-Token 完成权限认证操作。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+

二、获取当前账号权限码集合

+

因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中,
+所以 Sa-Token 将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。

+

你需要做的就是新建一个类,实现 StpInterface接口,例如以下代码:

+
/**
+ * 自定义权限验证接口扩展
+ */
+@Component  // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展 
+public class StpInterfaceImpl implements StpInterface {
+
+    /**
+     * 返回一个账号所拥有的权限码集合 
+     */
+    @Override
+    public List<String> getPermissionList(Object loginId, String loginType) {
+        // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
+        List<String> list = new ArrayList<String>();    
+        list.add("101");
+        list.add("user.add");
+        list.add("user.update");
+        list.add("user.get");
+        // list.add("user.delete");
+        list.add("art.*");
+        return list;
+    }
+
+    /**
+     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
+     */
+    @Override
+    public List<String> getRoleList(Object loginId, String loginType) {
+        // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
+        List<String> list = new ArrayList<String>();    
+        list.add("admin");
+        list.add("super-admin");
+        return list;
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
+

参数解释:
+- loginId:账号id,即你在调用 StpUtil.login(id) 时写入的标识值。
+- loginType:账号体系标识,此处可以暂时忽略,在 [ 多账户认证 ] 章节下会对这个概念做详细的解释。

+

注意点:
+类上一定要加上 @Component 注解,保证组件被 Springboot 扫描到,成功注入到 Sa-Token 框架内。

+

三、权限校验

+

启动类:

+
@SpringBootApplication
+public class SaTokenCaseApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaTokenCaseApplication.class, args); 
+        System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig());
+    }   
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

然后就可以用以下api来鉴权了

+
// 获取:当前账号所拥有的权限集合
+StpUtil.getPermissionList();
+
+// 判断:当前账号是否含有指定权限, 返回 true 或 false
+StpUtil.hasPermission("user.add");      
+
+// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException 
+StpUtil.checkPermission("user.add");        
+
+// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
+StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");      
+
+// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
+StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

扩展:NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

+

四、角色校验

+

在Sa-Token中,角色和权限可以独立验证

+
// 获取:当前账号所拥有的角色集合
+StpUtil.getRoleList();
+
+// 判断:当前账号是否拥有指定角色, 返回 true 或 false
+StpUtil.hasRole("super-admin");     
+
+// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
+StpUtil.checkRole("super-admin");       
+
+// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
+StpUtil.checkRoleAnd("super-admin", "shop-admin");      
+
+// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] 
+StpUtil.checkRoleOr("super-admin", "shop-admin");       
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

扩展:NotRoleException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

+

五、拦截全局异常

+

有同学要问,鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?当然不可以!

+

你可以创建一个全局异常拦截器,统一返回给前端的格式,参考:

+
@RestControllerAdvice
+public class GlobalExceptionHandler {
+    // 全局异常拦截 
+    @ExceptionHandler
+    public SaResult handlerException(Exception e) {
+        e.printStackTrace(); 
+        return SaResult.error(e.getMessage());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
+

六、权限通配符

+

Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有art.*的权限时,art.addart.deleteart.update都将匹配通过

+
// 当拥有 art.* 权限时
+StpUtil.hasPermission("art.add");        // true
+StpUtil.hasPermission("art.update");     // true
+StpUtil.hasPermission("goods.add");      // false
+
+// 当拥有 *.delete 权限时
+StpUtil.hasPermission("art.delete");      // true
+StpUtil.hasPermission("user.delete");     // true
+StpUtil.hasPermission("user.update");     // false
+
+// 当拥有 *.js 权限时
+StpUtil.hasPermission("index.js");        // true
+StpUtil.hasPermission("index.css");       // false
+StpUtil.hasPermission("index.html");      // false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+
+

上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码 (角色认证同理)

+
+

七、如何把权限精确到按钮级?

+

权限精确到按钮级的意思就是指:权限范围可以控制到页面上的每一个按钮是否显示

+

思路:如此精确的范围控制只依赖后端已经难以完成,此时需要前端进行一定的逻辑判断。

+

如果是前后端一体项目,可以参考:Thymeleaf 标签方言,如果是前后端分离项目,则:

+
    +
  1. 在登录时,把当前账号拥有的所有权限码一次性返回给前端。
  2. +
  3. 前端将权限码集合保存在localStorage或其它全局状态管理对象中。
  4. +
  5. 在需要权限控制的按钮上,使用 js 进行逻辑判断,例如在Vue框架中我们可以使用如下写法:
  6. +
+
<button v-if="arr.indexOf('user.delete') > -1">删除按钮</button>
  • 1
+

其中:arr是当前用户拥有的权限码数组,user.delete是显示按钮需要拥有的权限码,删除按钮是用户拥有权限码才可以看到的内容。

+

注意:以上写法只为提供一个参考示例,不同框架有不同写法,大家可根据项目技术栈灵活封装进行调用。

+

八、前端有了鉴权后端还需要鉴权吗?

+

需要!

+

前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全,无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验!

+

九、来个小示例,加深一下印象

+

新建 JurAuthController,复制以下代码

+
package com.pj.cases.use;
+
+import java.util.List;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * Sa-Token 权限认证示例 
+ * 
+ * @author kong
+ * @since 2022-10-13
+ */
+@RestController
+@RequestMapping("/jur/")
+public class JurAuthController {
+
+    /*
+     * 前提1:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 
+     *      ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
+     * 
+     * 前提2:项目实现 StpInterface 接口,代码在  com.pj.satoken.StpInterfaceImpl
+     *      Sa-Token 将从此实现类获取 每个账号拥有哪些权限。
+     * 
+     * 然后我们就可以使用以下示例中的代码进行鉴权了 
+     */
+
+    // 查询权限   ---- http://localhost:8081/jur/getPermission
+    @RequestMapping("getPermission")
+    public SaResult getPermission() {
+        // 查询权限信息 ,如果当前会话未登录,会返回一个空集合 
+        List<String> permissionList = StpUtil.getPermissionList();
+        System.out.println("当前登录账号拥有的所有权限:" + permissionList);
+
+        // 查询角色信息 ,如果当前会话未登录,会返回一个空集合 
+        List<String> roleList = StpUtil.getRoleList();
+        System.out.println("当前登录账号拥有的所有角色:" + roleList);
+
+        // 返回给前端 
+        return SaResult.ok()
+                .set("roleList", roleList)
+                .set("permissionList", permissionList);
+    }
+
+    // 权限校验  ---- http://localhost:8081/jur/checkPermission
+    @RequestMapping("checkPermission")
+    public SaResult checkPermission() {
+
+        // 判断:当前账号是否拥有一个权限,返回 true 或 false
+        //      如果当前账号未登录,则永远返回 false 
+        StpUtil.hasPermission("user.add");
+        StpUtil.hasPermissionAnd("user.add", "user.delete", "user.get");  // 指定多个,必须全部拥有才会返回 true 
+        StpUtil.hasPermissionOr("user.add", "user.delete", "user.get");  // 指定多个,只要拥有一个就会返回 true 
+
+        // 校验:当前账号是否拥有一个权限,校验不通过时会抛出 `NotPermissionException` 异常 
+        //      如果当前账号未登录,则永远校验失败 
+        StpUtil.checkPermission("user.add");
+        StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");  // 指定多个,必须全部拥有才会校验通过 
+        StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");  // 指定多个,只要拥有一个就会校验通过 
+
+        return SaResult.ok();
+    }
+
+    // 角色校验  ---- http://localhost:8081/jur/checkRole
+    @RequestMapping("checkRole")
+    public SaResult checkRole() {
+
+        // 判断:当前账号是否拥有一个角色,返回 true 或 false
+        //      如果当前账号未登录,则永远返回 false 
+        StpUtil.hasRole("admin");
+        StpUtil.hasRoleAnd("admin", "ceo", "cfo");  // 指定多个,必须全部拥有才会返回 true 
+        StpUtil.hasRoleOr("admin", "ceo", "cfo");     // 指定多个,只要拥有一个就会返回 true 
+
+        // 校验:当前账号是否拥有一个角色,校验不通过时会抛出 `NotRoleException` 异常 
+        //      如果当前账号未登录,则永远校验失败 
+        StpUtil.checkRole("admin");
+        StpUtil.checkRoleAnd("admin", "ceo", "cfo");  // 指定多个,必须全部拥有才会校验通过 
+        StpUtil.checkRoleOr("admin", "ceo", "cfo");  // 指定多个,只要拥有一个就会校验通过 
+
+        return SaResult.ok();
+    }
+
+    // 权限通配符  ---- http://localhost:8081/jur/wildcardPermission
+    @RequestMapping("wildcardPermission")
+    public SaResult wildcardPermission() {
+
+        // 前提条件:在 StpInterface 实现类中,为账号返回了 "art.*" 泛权限
+        StpUtil.hasPermission("art.add");  // 返回 true 
+        StpUtil.hasPermission("art.delete");  // 返回 true 
+        StpUtil.hasPermission("goods.add");  // 返回 false,因为前缀不符合  
+
+        // * 符合可以出现在任意位置,比如权限码的开头,当账号拥有 "*.delete" 时  
+        StpUtil.hasPermission("goods.add");        // false
+        StpUtil.hasPermission("goods.delete");     // true
+        StpUtil.hasPermission("art.delete");      // true
+
+        // 也可以出现在权限码的中间,比如当账号拥有 "shop.*.user" 时  
+        StpUtil.hasPermission("shop.add.user");  // true
+        StpUtil.hasPermission("shop.delete.user");  // true
+        StpUtil.hasPermission("shop.delete.goods");  // false,因为后缀不符合 
+
+        // 注意点:
+        // 1、上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码
+        // 2、角色校验也可以加 * ,指定泛角色,例如: "*.admin",暂不赘述 
+
+        return SaResult.ok();
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
+

代码注释已针对每一步操作做出详细解释,大家可根据可参照注释中的访问链接进行逐步测试。

+
+

参考资料

+ +
+← Sa-Token 集成 jwt +SpringBoot 使用 Sa-Token 完成注解鉴权功能 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-zhu-jie-jian-quan-gong-neng.html b/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-zhu-jie-jian-quan-gong-neng.html new file mode 100644 index 000000000..44e5bd95f --- /dev/null +++ b/sa-token-doc/blog/essays/springboot-shi-yong-sa-token-wan-cheng-zhu-jie-jian-quan-gong-neng.html @@ -0,0 +1,588 @@ + + + + + SpringBoot 使用 Sa-Token 完成注解鉴权功能 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

SpringBoot 使用 Sa-Token 完成注解鉴权功能

+ +

注解鉴权 —— 优雅的将鉴权与业务代码分离。本篇我们将介绍在 Sa-Token 中如何通过注解完成权限校验。

+
+

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

一、Sa-Token 鉴权注解一览

+

Sa-Token 为我们提供的鉴权注解包括但不限于以下:

+
    +
  • @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
  • +
  • @SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
  • +
  • @SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
  • +
  • @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
  • +
  • @SaCheckBasic: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。
  • +
  • @SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
  • +
  • @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
  • +
+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+

二、登录认证

+

Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态

+因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中

+

SpringBoot2.0为例,新建配置类SaTokenConfigure.java

+
@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
+        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");    
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
+

保证此类被springboot启动类扫描到即可。

+

新建 LoginController,添加以下代码:

+
/**
+ * 登录认证注解测试
+ */
+@RestController
+public class LoginController {
+
+    // 访问 home 页,登录后才能访问  ---- http://localhost:8081/home
+    @SaCheckLogin
+    @RequestMapping("home")
+    public SaResult home() {
+        return SaResult.ok("访问成功,此处为登录后才能看到的信息");
+    }
+
+    // 登录接口  ---- http://localhost:8081/doLogin?name=zhang&pwd=123456
+    @RequestMapping("doLogin")
+    public SaResult doLogin(String name, String pwd) {
+        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+            StpUtil.login(10001);
+            return SaResult.ok("登录成功");
+        }
+        return SaResult.error("登录失败");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
+

启动项目,首次访问资源接口:

+
http://localhost:8081/home
  • 1
+

返回如下:

+
{
+    "code": 500,
+    "msg": "未能读取到有效Token",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

会话尚未登录,因此无法访问资源。

+

现在我们再去访问一下登录接口:

+
http://localhost:8081/doLogin?name=zhang&pwd=123456
  • 1
+

返回如下:

+
{
+    "code": 200,
+    "msg": "登录成功",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

登录成功,我们再去访问资源接口:

+
http://localhost:8081/home
  • 1
+

返回如下:

+
{
+    "code": 200,
+    "msg": "访问成功,此处为登录后才能看到的信息",
+    "data": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

通过登录认证校验,成功获取到信息!

+

三、权限认证 & 角色认证

+

首先我们需要实现 StpInterface 接口,告诉框架指定账号拥有哪些权限码。

+
/**
+ * 自定义权限认证接口扩展,Sa-Token 将从此实现类获取每个账号拥有的权限码 
+ * 
+ * @author kong
+ * @since 2022-10-13
+ */
+@Component  // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 
+public class StpInterfaceImpl implements StpInterface {
+
+    /**
+     * 返回一个账号所拥有的权限码集合 
+     */
+    @Override
+    public List<String> getPermissionList(Object loginId, String loginType) {
+        // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
+        List<String> list = new ArrayList<String>();    
+        list.add("101");
+        list.add("user.add");
+        list.add("user.update");
+        list.add("user.get");
+        // list.add("user.delete");
+        list.add("art.*");
+        return list;
+    }
+
+    /**
+     * 返回一个账号所拥有的角色标识集合 
+     */
+    @Override
+    public List<String> getRoleList(Object loginId, String loginType) {
+        // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
+        List<String> list = new ArrayList<String>();    
+        list.add("admin");
+        list.add("super-admin");
+        return list;
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
+

使用以下两个注解完成校验:
+- @SaCheckPermission("user.add"):校验当前会话是否具有某个权限。
+- @SaCheckRole("super-admin"):校验当前会话是否具有某个角色。

+
/**
+ * Sa-Token 注解鉴权示例 
+ * 
+ * @author kong
+ * @since 2022-10-13
+ */
+@RestController
+@RequestMapping("/at-check/")
+public class AtCheckController {
+
+    /*
+     * 前提1:首先调用登录接口进行登录
+     *      ---- http://localhost:8081/doLogin?name=zhang&pwd=123456
+     * 
+     * 前提2:项目在配置类中注册拦截器 SaInterceptor ,此拦截器将打开注解鉴权功能 
+     * 
+     * 前提3:项目实现了 StpInterface 接口,此接口会告诉框架指定账号拥有哪些权限码
+     * 
+     * 然后我们就可以使用以下示例中的代码进行注解鉴权了 
+     */
+
+    // 权限校验   ---- http://localhost:8081/at-check/checkPermission
+    //      只有具有 user.add 权限的账号才可以进入方法 
+    @SaCheckPermission("user.add")
+    @RequestMapping("checkPermission")
+    public SaResult checkPermission() {
+        // ... 
+        return SaResult.ok();
+    }
+
+    // 角色校验   ---- http://localhost:8081/at-check/checkRole
+    //      只有具有 super-admin 角色的账号才可以进入方法 
+    @SaCheckRole("super-admin")
+    @RequestMapping("checkRole")
+    public SaResult checkRole() {
+        // ... 
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
+

可根据代码注释提供的链接进行测试访问。

+

四、设定校验模式

+

@SaCheckRole@SaCheckPermission注解可设置校验模式,例如:

+
// 注解式鉴权:只要具有其中一个权限即可通过校验 
+@RequestMapping("atJurOr")
+@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)       
+public SaResult atJurOr() {
+    return SaResult.data("用户信息");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

mode有两种取值:
+- SaMode.AND, 标注一组权限,会话必须全部具有才可通过校验。
+- SaMode.OR, 标注一组权限,会话只要具有其一即可通过校验。

+

五、角色权限双重 “or校验”

+

假设有以下业务场景:一个接口在具有权限 user.add 或角色 admin 时可以调通。怎么写?

+
// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
+@RequestMapping("userAdd")
+@SaCheckPermission(value = "user.add", orRole = "admin")        
+public SaResult userAdd() {
+    return SaResult.data("用户信息");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

orRole 字段代表权限认证未通过时的次要选择,两者只要其一认证成功即可通过校验,其有三种写法:
+- 写法一:orRole = "admin",代表需要拥有角色 admin 。
+- 写法二:orRole = {"admin", "manager", "staff"},代表具有三个角色其一即可。
+- 写法三:orRole = {"admin, manager, staff"},代表必须同时具有三个角色。

+

六、二级认证

+
@RestController
+@RequestMapping("/at/")
+public class AtController {
+
+    // 在当前会话完成二级认证  ---- http://localhost:8081/at/openSafe 
+    @RequestMapping("openSafe")
+    public SaResult openSafe() {
+        StpUtil.openSafe(200); // 打开二级认证,有效期为200秒
+        return SaResult.ok();
+    }
+
+    // 通过二级认证后,才可以进入  ---- http://localhost:8081/at/checkSafe 
+    @SaCheckSafe
+    @RequestMapping("checkSafe")
+    public SaResult checkSafe() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
+

必须先经过 StpUtil.openSafe(1200) 打开二级认证(参数为指定认证有效期,单位:秒),才可以通过 @SaCheckSafe 的检查。

+

七、HttpBasic认证:

+
@RestController
+@RequestMapping("/at/")
+public class AtController {
+
+    // 通过Basic认证后才可以进入  ---- http://localhost:8081/at/checkBasic 
+    @SaCheckBasic(account = "sa:123456")
+    @RequestMapping("checkBasic")
+    public SaResult checkBasic() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+

当我们访问这个接口时,浏览器会强制弹出一个表单:

+

img

+

当我们输入账号密码后 (sa / 123456),才可以继续访问数据:

+

img

+

八、服务禁用性校验

+
@RestController
+@RequestMapping("/at/")
+public class AtController {
+
+    // 只有当前服务没有禁用 comment 服务时,才能够进入方法  ---- http://localhost:8081/at/comment 
+    @SaCheckDisable("comment")
+    @RequestMapping("comment")
+    public SaResult comment() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+

@SaCheckDisable 注解的作用是检测当前账号是否被禁用了指定服务,如果已被禁用则无法进入指定方法,
+在之后的章节我们会详细讲述服务禁用的相关代码,此处先稍作了解即可。

+

九、忽略认证

+

使用 @SaIgnore 可表示一个接口忽略认证:

+
@SaCheckLogin
+@RestController
+public class TestController {
+
+    // ... 其它方法 
+
+    // 此接口加上了 @SaIgnore 可以游客访问 
+    @SaIgnore
+    @RequestMapping("getList")
+    public SaResult getList() {
+        // ... 
+        return SaResult.ok(); 
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

如上代码表示:TestController 中的所有方法都需要登录后才可以访问,但是 getList 接口可以匿名游客访问。

+
    +
  • @SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。
  • +
  • @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。
  • +
  • @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权,在下面的 [路由拦截鉴权] 章节中我们会讲到。
  • +
+

十、在业务逻辑层使用注解鉴权

+

疑问:我能否将注解写在其它架构层呢,比如业务逻辑层?

+

使用拦截器模式,只能在Controller层进行注解鉴权,如需在任意层级使用注解鉴权,可使用 AOP注解鉴权 插件。

+
<!-- Sa-Token 整合 SpringAOP 实现注解鉴权 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-aop</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

集成此插件后,便可以在任意层使用 Sa-Token 的注解鉴权了(例如业务逻辑层),不过需要注意的是:
+- 拦截器模式和AOP模式不可同时集成,否则会在 Controller 层发生一个注解校验两次的bug。

+
+

参考资料

+ +
+← SpringBoot 使用 Sa-Token 完成权限认证 +SpringBoot 使用 Sa-Token 完成路由拦截鉴权 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/springboot-xiang-mu-shi-yong-sa-token-wan-cheng-deng-lu-ren-zheng.html b/sa-token-doc/blog/essays/springboot-xiang-mu-shi-yong-sa-token-wan-cheng-deng-lu-ren-zheng.html new file mode 100644 index 000000000..1ed7531bd --- /dev/null +++ b/sa-token-doc/blog/essays/springboot-xiang-mu-shi-yong-sa-token-wan-cheng-deng-lu-ren-zheng.html @@ -0,0 +1,633 @@ + + + + + SpringBoot 项目使用 Sa-Token 完成登录认证 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

SpringBoot 项目使用 Sa-Token 完成登录认证

+ +

一、设计思路

+

对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:

+
    +
  • 如果校验通过,则:正常返回数据。
  • +
  • 如果校验未通过,则:抛出异常,告知其需要先进行登录。
  • +
+

那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:

+
    +
  1. 用户提交 name + password 参数,调用登录接口。
  2. +
  3. 登录成功,返回这个用户的 Token 会话凭证。
  4. +
  5. 用户后续的每次请求,都携带上这个 Token。
  6. +
  7. 服务器根据 Token 判断此会话是否登录成功。
  8. +
+

所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。

+

动态图演示:

+

登录认证

+

接下来,我们将介绍在 SpringBoot 中如何使用 Sa-Token 完成登录认证操作。

+
+

Sa-Token 是一个 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

首先在项目中引入 Sa-Token 依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

+

二、登录与注销

+

根据以上思路,我们需要一个会话登录的函数:

+
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
+StpUtil.login(Object id);    
  • 1
  • 2
+

只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:

+
    +
  1. 检查此账号是否之前已有登录
  2. +
  3. 为账号生成 Token 凭证与 Session 会话
  4. +
  5. 通知全局侦听器,xx 账号登录成功
  6. +
  7. Token 注入到请求上下文
  8. +
  9. 等等其它工作……
  10. +
+

你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端

+

所以一般情况下,我们的登录接口代码,会大致类似如下:

+
// 会话登录接口 
+@RequestMapping("doLogin")
+public SaResult doLogin(String name, String pwd) {
+    // 第一步:比对前端提交的账号名称、密码
+    if("zhang".equals(name) && "123456".equals(pwd)) {
+        // 第二步:根据账号id,进行登录 
+        StpUtil.login(10001);
+        return SaResult.ok("登录成功");
+    }
+    return SaResult.error("登录失败");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。
+是因为不需要吗?严格来讲是需要的,只不过 StpUtil.login(id) 方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。

+

如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点:

+
    +
  • Cookie 可以从后端控制往浏览器中写入 Token 值。
  • +
  • Cookie 会在前端每次发起请求时自动提交 Token 值。
  • +
+

因此,在 Cookie 功能的加持下,我们可以仅靠 StpUtil.login(id) 一句代码就完成登录认证。

+

除了登录方法,我们还需要:

+
// 当前会话注销登录
+StpUtil.logout();
+
+// 获取当前会话是否已经登录,返回true=已登录,false=未登录
+StpUtil.isLogin();
+
+// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
+StpUtil.checkLogin();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

异常 NotLoginException 代表当前会话暂未登录,可能的原因有很多:
+前端没有提交 Token、前端提交的 Token 是无效的、前端提交的 Token 已经过期 …… 等等。

+

Sa-Token 未登录场景值参照表:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
场景值对应常量含义说明
-1NotLoginException.NOT_TOKEN未能从请求中读取到 Token
-2NotLoginException.INVALID_TOKEN已读取到 Token,但是 Token无效
-3NotLoginException.TOKEN_TIMEOUT已读取到 Token,但是 Token已经过期
-4NotLoginException.BE_REPLACED已读取到 Token,但是 Token 已被顶下线
-5NotLoginException.KICK_OUT已读取到 Token,但是 Token 已被踢下线
+

那么,如何获取场景值呢?废话少说直接上代码:

+
// 全局异常拦截(拦截项目中的NotLoginException异常)
+@ExceptionHandler(NotLoginException.class)
+public SaResult handlerNotLoginException(NotLoginException nle)
+        throws Exception {
+
+    // 打印堆栈,以供调试
+    nle.printStackTrace(); 
+
+    // 判断场景值,定制化异常信息 
+    String message = "";
+    if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
+        message = "未提供token";
+    }
+    else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
+        message = "token无效";
+    }
+    else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
+        message = "token已过期";
+    }
+    else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
+        message = "token已被顶下线";
+    }
+    else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
+        message = "token已被踢下线";
+    }
+    else {
+        message = "当前会话未登录";
+    }
+
+    // 返回给前端
+    return SaResult.error(message);
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
+



+注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理

+

三、会话查询

+
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
+StpUtil.getLoginId();
+
+// 类似查询API还有:
+StpUtil.getLoginIdAsString();    // 获取当前会话账号id, 并转化为`String`类型
+StpUtil.getLoginIdAsInt();       // 获取当前会话账号id, 并转化为`int`类型
+StpUtil.getLoginIdAsLong();      // 获取当前会话账号id, 并转化为`long`类型
+
+// ---------- 指定未登录情形下返回的默认值 ----------
+
+// 获取当前会话账号id, 如果未登录,则返回null 
+StpUtil.getLoginIdDefaultNull();
+
+// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
+StpUtil.getLoginId(T defaultValue);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

四、Token 查询

+
// 获取当前会话的token值
+StpUtil.getTokenValue();
+
+// 获取当前`StpLogic`的token名称
+StpUtil.getTokenName();
+
+// 获取指定token对应的账号id,如果未登录,则返回 null
+StpUtil.getLoginIdByToken(String tokenValue);
+
+// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
+StpUtil.getTokenTimeout();
+
+// 获取当前会话的token信息参数
+StpUtil.getTokenInfo();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

TokenInfo 是 Token 信息 Model,用来描述一个 Token 的常用参数:

+
{
+    "tokenName": "satoken",           // token名称
+    "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633",      // token值
+    "isLogin": true,                  // 此token是否已经登录
+    "loginId": "10001",               // 此token对应的LoginId,未登录时为null
+    "loginType": "login",              // 账号类型标识
+    "tokenTimeout": 2591977,          // token剩余有效期 (单位: 秒)
+    "sessionTimeout": 2591977,        // User-Session剩余有效时间 (单位: 秒)
+    "tokenSessionTimeout": -2,        // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
+    "tokenActivityTimeout": -1,       // token剩余无操作有效时间 (单位: 秒)
+    "loginDevice": "default-device"   // 登录设备类型 
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+

五、来个小测试,加深一下理解

+

新建 LoginAuthController,复制以下代码

+
package com.pj.cases.use;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.dev33.satoken.stp.SaTokenInfo;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+
+/**
+ * Sa-Token 登录认证示例 
+ * 
+ * @author kong
+ * @since 2022-10-13
+ */
+@RestController
+@RequestMapping("/acc/")
+public class LoginAuthController {
+
+    // 会话登录接口  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
+    @RequestMapping("doLogin")
+    public SaResult doLogin(String name, String pwd) {
+
+        // 第一步:比对前端提交的 账号名称 & 密码 是否正确,比对成功后开始登录 
+        //      此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+
+            // 第二步:根据账号id,进行登录 
+            //      此处填入的参数应该保持用户表唯一,比如用户id,不可以直接填入整个 User 对象 
+            StpUtil.login(10001);
+
+            // SaResult 是 Sa-Token 中对返回结果的简单封装,下面的示例将不再赘述 
+            return SaResult.ok("登录成功");
+        }
+
+        return SaResult.error("登录失败");
+    }
+
+    // 查询当前登录状态  ---- http://localhost:8081/acc/isLogin
+    @RequestMapping("isLogin")
+    public SaResult isLogin() {
+        // StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false 
+        boolean isLogin = StpUtil.isLogin();
+        return SaResult.ok("当前客户端是否登录:" + isLogin);
+    }
+
+    // 校验当前登录状态  ---- http://localhost:8081/acc/checkLogin
+    @RequestMapping("checkLogin")
+    public SaResult checkLogin() {
+        // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
+        StpUtil.checkLogin();
+
+        // 抛出异常后,代码将走入全局异常处理(GlobalException.java),如果没有抛出异常,则代表通过了登录校验,返回下面信息 
+        return SaResult.ok("校验登录成功,这行字符串是只有登录后才会返回的信息");
+    }
+
+    // 获取当前登录的账号是谁  ---- http://localhost:8081/acc/getLoginId
+    @RequestMapping("getLoginId")
+    public SaResult getLoginId() {
+        // 需要注意的是,StpUtil.getLoginId() 自带登录校验效果
+        // 也就是说如果在未登录的情况下调用这句代码,框架就会抛出 `NotLoginException` 异常,效果和 StpUtil.checkLogin() 是一样的 
+        Object userId = StpUtil.getLoginId();
+        System.out.println("当前登录的账号id是:" + userId);
+
+        // 如果不希望 StpUtil.getLoginId() 触发登录校验效果,可以填入一个默认值
+        // 如果会话未登录,则返回这个默认值,如果会话已登录,将正常返回登录的账号id 
+        Object userId2 = StpUtil.getLoginId(0);
+        System.out.println("当前登录的账号id是:" + userId2);
+
+        // 或者使其在未登录的时候返回 null 
+        Object userId3 = StpUtil.getLoginIdDefaultNull();
+        System.out.println("当前登录的账号id是:" + userId3);
+
+        // 类型转换:
+        // StpUtil.getLoginId() 返回的是 Object 类型,你可以使用以下方法指定其返回的类型 
+        int userId4 = StpUtil.getLoginIdAsInt();  // 将返回值转换为 int 类型 
+        long userId5 = StpUtil.getLoginIdAsLong();  // 将返回值转换为 long 类型 
+        String userId6 = StpUtil.getLoginIdAsString();  // 将返回值转换为 String 类型 
+
+        // 疑问:数据基本类型不是有八个吗,为什么只封装以上三种类型的转换?
+        // 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的,实在没见过哪个项目用 double、float、boolean 之类来声明 UserId 
+        System.out.println("当前登录的账号id是:" + userId4 + " --- " + userId5 + " --- " + userId6);
+
+        // 返回给前端 
+        return SaResult.ok("当前客户端登录的账号id是:" + userId);
+    }
+
+    // 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo
+    @RequestMapping("tokenInfo")
+    public SaResult tokenInfo() {
+        // TokenName 是 Token 名称的意思,此值也决定了前端提交 Token 时应该使用的参数名称 
+        String tokenName = StpUtil.getTokenName();
+        System.out.println("前端提交 Token 时应该使用的参数名称:" + tokenName);
+
+        // 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值 
+        // 框架默认前端可以从以下三个途径中提交 Token:
+        //      Cookie      (浏览器自动提交)
+        //      Header头 (代码手动提交)
+        //      Query 参数    (代码手动提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx 
+        // 读取顺序为: Query 参数 --> Header头 -- > Cookie 
+        // 以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token 
+        String tokenValue = StpUtil.getTokenValue();
+        System.out.println("前端提交的Token值为:" + tokenValue);
+
+        // TokenInfo 包含了此 Token 的大多数信息 
+        SaTokenInfo info = StpUtil.getTokenInfo();
+        System.out.println("Token 名称:" + info.getTokenName());
+        System.out.println("Token 值:" + info.getTokenValue());
+        System.out.println("当前是否登录:" + info.getIsLogin());
+        System.out.println("当前登录的账号id:" + info.getLoginId());
+        System.out.println("当前登录账号的类型:" + info.getLoginType());
+        System.out.println("当前登录客户端的设备类型:" + info.getLoginDevice());
+        System.out.println("当前 Token 的剩余有效期:" + info.getTokenTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
+        System.out.println("当前 Token 的剩余临时有效期:" + info.getTokenActivityTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
+        System.out.println("当前 User-Session 的剩余有效期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
+        System.out.println("当前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
+
+        // 返回给前端 
+        return SaResult.data(StpUtil.getTokenInfo());
+    }
+
+    // 会话注销  ---- http://localhost:8081/acc/logout
+    @RequestMapping("logout")
+    public SaResult logout() {
+        // 退出登录会清除三个地方的数据:
+        //      1、Redis中保存的 Token 信息
+        //      2、当前请求上下文中保存的 Token 信息 
+        //      3、Cookie 中保存的 Token 信息(如果未使用Cookie模式则不会清除)
+        StpUtil.logout();
+
+        // StpUtil.logout() 在未登录时也是可以调用成功的,
+        // 也就是说,无论客户端有没有登录,执行完 StpUtil.logout() 后,都会处于未登录状态 
+        System.out.println("当前是否处于登录状态:" + StpUtil.isLogin());
+
+        // 返回给前端 
+        return SaResult.ok("退出登录成功");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
+

代码注释已针对每一步操作做出详细解释,大家可根据可参照注释中的访问链接进行逐步测试。

+

本示例代码已上传至 Gitee,可参考:
+Sa-Token 登录认证示例

+
+

参考资料

+ +
+← SpringBoot 使用 Sa-Token 实现账号封禁、分类封禁、阶梯封禁 +一文详解 Sa-Token 中的 SaSession 对象 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/yi-wen-xiang-jie-sa-token-zhong-de-sasession-dui-xiang.html b/sa-token-doc/blog/essays/yi-wen-xiang-jie-sa-token-zhong-de-sasession-dui-xiang.html new file mode 100644 index 000000000..0810d6cc4 --- /dev/null +++ b/sa-token-doc/blog/essays/yi-wen-xiang-jie-sa-token-zhong-de-sasession-dui-xiang.html @@ -0,0 +1,440 @@ + + + + + 一文详解 Sa-Token 中的 SaSession 对象 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

一文详解 Sa-Token 中的 SaSession 对象

+ +

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。

+
+

Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

本文将详细介绍 Sa-Token 中的不同 SaSession 对象的区别,以及各种方便的存取值的方法。

+

一、Session 是什么?

+

Session 是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据,提高程序性能,例如:

+
// 在登录时缓存user对象 
+StpUtil.getSession().set("user", user);
+
+// 然后我们就可以在任意处使用这个user对象
+SysUser user = (SysUser) StpUtil.getSession().get("user");
  • 1
  • 2
  • 3
  • 4
  • 5
+

在 Sa-Token 中,SaSession 分为三种,分别是:

+
    +
  • User-Session: 指的是框架为每个 账号id 分配的 SaSession。
  • +
  • Token-Session: 指的是框架为每个 token 分配的 SaSession。
  • +
  • Custom-Session: 指的是以一个 特定的值 作为SessionId,来分配的 Session。
  • +
+

假设三个客户端登录同一账号,且配置了不共享token,那么此时的Session模型是:

+

session-model

+

简而言之:
+- User-Session 以UserId为主,只要token指向的UserId一致,那么对应的Session对象就一致。
+- Token-Session 以token为主,只要token不同,那么对应的Session对象就不同。
+- Custom-Session 以特定的key为主,不同key对应不同的Session对象,同样的key指向同一个Session对象。

+

二、获取 User-Session

+

有关账号Session的API如下:

+
// 获取当前账号id的Session (必须是登录后才能调用)
+StpUtil.getSession();
+
+// 获取当前账号id的Session, 并决定在Session尚未创建时,是否新建并返回
+StpUtil.getSession(true);
+
+// 获取账号id为10001的Session
+StpUtil.getSessionByLoginId(10001);
+
+// 获取账号id为10001的Session, 并决定在Session尚未创建时,是否新建并返回
+StpUtil.getSessionByLoginId(10001, true);
+
+// 获取SessionId为xxxx-xxxx的Session, 在Session尚未创建时, 返回null 
+StpUtil.getSessionBySessionId("xxxx-xxxx");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

三、获取 Token-Session

+

有关令牌Session的API如下:

+
// 获取当前 Token 的 Token-Session 对象
+StpUtil.getTokenSession();
+
+// 获取指定 Token 的 Token-Session 对象
+StpUtil.getTokenSessionByToken(token);
  • 1
  • 2
  • 3
  • 4
  • 5
+

四、获取自定义Session

+

自定义Session指的是以一个特定的值作为SessionId来分配的Session, 借助自定义Session,你可以为系统中的任意元素分配相应的session

+例如以商品id作为key为每个商品分配一个Session,以便于缓存和商品相关的数据,其相关API如下:

+
// 查询指定key的Session是否存在
+SaSessionCustomUtil.isExists("goods-10001");
+
+// 获取指定key的Session,如果没有,则新建并返回
+SaSessionCustomUtil.getSessionById("goods-10001");
+
+// 获取指定key的Session,如果没有,第二个参数决定是否新建并返回  
+SaSessionCustomUtil.getSessionById("goods-10001", false);   
+
+// 删除指定key的Session
+SaSessionCustomUtil.deleteSessionById("goods-10001");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

五、在 Session 上存取值

+
// 写值 
+session.set("name", "zhang"); 
+
+// 写值 (只有在此key原本无值的时候才会写入)
+session.setDefaultValue("name", "zhang");
+
+// 取值
+session.get("name");
+
+// 取值 (指定默认值)
+session.get("name", "<defaultValue>"); 
+
+// 取值 (若无值则执行参数方法, 之后将结果保存到此键名下,并返回此结果   若有值则直接返回, 无需执行参数方法)
+session.get("name", () -> {
+            return ...;
+        });
+
+// ---------- 数据类型转换: ----------
+session.getInt("age");         // 取值 (转int类型)
+session.getLong("age");        // 取值 (转long类型)
+session.getString("name");     // 取值 (转String类型)
+session.getDouble("result");   // 取值 (转double类型)
+session.getFloat("result");    // 取值 (转float类型)
+session.getModel("key", Student.class);     // 取值 (指定转换类型)
+session.getModel("key", Student.class, <defaultValue>);  // 取值 (指定转换类型, 并指定值为Null时返回的默认值)
+
+// 是否含有某个key (返回true或false)
+session.has("key"); 
+
+// 删值 
+session.delete('name');          
+
+// 清空所有值 
+session.clear();                 
+
+// 获取此 Session 的所有key (返回Set<String>)
+session.keys();      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
+

六、其它操作

+
// 返回此 Session 的id 
+session.getId();                          
+
+// 返回此 Session 的创建时间 (时间戳) 
+session.getCreateTime();                  
+
+// 返回此 Session 会话上的底层数据对象(如果更新map里的值,请调用session.update()方法避免产生脏数据)
+session.getDataMap();                     
+
+// 将这个 Session 从持久库更新一下
+session.update();                         
+
+// 注销此 Session 会话 (从持久库删除此Session)
+session.logout();                         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

七、SaSession 环境隔离说明

+

有同学经常会把 SaSessionHttpSession 进行混淆,例如:

+
@PostMapping("/resetPoints")
+public void reset(HttpSession session) {
+    // 在 HttpSession 上写入一个值 
+    session.setAttribute("name", 66);
+    // 在 SaSession 进行取值
+    System.out.println(StpUtil.getSession().get("name"));   // 输出null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

要点:
+1. SaSessionHttpSession 没有任何关系,在HttpSession上写入的值,在SaSession中无法取出。
+2. HttpSession并未被框架接管,在使用Sa-Token时,请在任何情况下均使用SaSession,不要使用HttpSession

+

八、未登录场景下获取 Token-Session

+

默认场景下,只有登录后才能通过 StpUtil.getTokenSession() 获取 Token-Session

+

如果想要在未登录场景下获取 Token-Session ,有两种方法:

+
    +
  • 方法一:将全局配置项 tokenSessionCheckLogin 改为 false。
  • +
  • 方法二:使用匿名 Token-Session
  • +
+
// 获取当前 Token 的匿名 Token-Session (可在未登录情况下使用的 Token-Session)
+StpUtil.getAnonTokenSession();
  • 1
  • 2
+

注意点:如果前端没有提交 Token ,或者提交的 Token 是一个无效 Token 的话,框架将不会根据此 Token 创建 Token-Session 对象,
+而是随机一个新的 Token 值来创建 Token-Session 对象,此 Token 值可以通过 StpUtil.getTokenValue() 获取到。

+
+

参考资料

+ +
+← SpringBoot 项目使用 Sa-Token 完成登录认证 +不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/essays/zai-wei-fu-wu-zhong-shi-yong-sa-token-fen-bu-shi-session-hui-hua-wang-guan-tong.html b/sa-token-doc/blog/essays/zai-wei-fu-wu-zhong-shi-yong-sa-token-fen-bu-shi-session-hui-hua-wang-guan-tong.html new file mode 100644 index 000000000..f6b6b0f42 --- /dev/null +++ b/sa-token-doc/blog/essays/zai-wei-fu-wu-zhong-shi-yong-sa-token-fen-bu-shi-session-hui-hua-wang-guan-tong.html @@ -0,0 +1,506 @@ + + + + + 在微服务中使用 Sa-Token:分布式Session会话、网关统一鉴权 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

在微服务中使用 Sa-Token:分布式Session会话、网关统一鉴权

+ +

Sa-Token 是一个轻量级 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。

+
+

Gitee 开源地址:https://gitee.com/dromara/sa-token

+
+

本篇将介绍在微服务中使用 Sa-Token 的两种常见技术:
+- 分布式 Session 会话。
+- 网关统一鉴权。

+

一、分布式 Session 会话

+

微服务架构下的第一个难题便是数据同步,单机版的Session在分布式环境下一般不能正常工作,为此我们需要对框架做一些特定的处理。

+

首先我们要明白,分布式环境下为什么Session会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点,
+这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。

+

要怎么解决这个问题呢?目前的主流方案有四种:
+1. Session同步:只要一个节点的数据发生了改变,就强制同步到其它所有节点
+2. Session粘滞:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务
+3. 建立会话中心:将Session存储在专业的缓存中间件上,使每个节点都变成了无状态服务,例如:Redis
+4. 颁发无状态token:放弃Session机制,将用户数据直接写入到令牌本身上,使会话数据做到令牌自解释,例如:jwt

+

使用 Sa-Token 可以为项目构建一个会话中心模式,所有的数据都统一放在 Redis 中,每个服务都是无状态服务,保证会话数据的一致性。

+

动态演示图:

+

会话中心

+

在每个服务中引入以下依赖(或根pom统一引入)

+
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-dao-redis-jackson</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.commons</groupId>
+    <artifactId>commons-pool2</artifactId>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

如上述依赖所示,只要所有客户端配置一样的 Redis 链接,他们的会话就可以互通。

+

如果你的业务无法统一配置一个 Redis,也可以使用 Alone-Redis 插件,为 Sa-Token 单独配置一个Redis 依赖,做到权限缓存与业务缓存分离:

+
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-alone-redis</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

然后在application.yml中增加配置

+
# Sa-Token 配置
+sa-token: 
+    # Token名称
+    token-name: satoken
+    # Token有效期
+    timeout: 2592000
+    # Token风格
+    token-style: uuid
+
+    # 配置 Sa-Token 单独使用的 Redis 连接 
+    alone-redis: 
+        # Redis数据库索引(默认为0)
+        database: 2
+        # Redis服务器地址
+        host: 127.0.0.1
+        # Redis服务器连接端口
+        port: 6379
+        # Redis服务器连接密码(默认为空)
+        password: 
+        # 连接超时时间
+        timeout: 10s
+
+spring: 
+    # 配置业务使用的 Redis 连接 
+    redis: 
+        # Redis数据库索引(默认为0)
+        database: 0
+        # Redis服务器地址
+        host: 127.0.0.1
+        # Redis服务器连接端口
+        port: 6379
+        # Redis服务器连接密码(默认为空)
+        password: 
+        # 连接超时时间
+        timeout: 10s
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
+

新建Controller测试一下:

+
@RestController
+@RequestMapping("/test/")
+public class TestController {
+
+    @Autowired
+    StringRedisTemplate stringRedisTemplate;
+
+    // 测试Sa-Token缓存
+    @RequestMapping("login")
+    public SaResult login(@RequestParam(defaultValue="10001") String id) {
+        System.out.println("--------------- 测试Sa-Token缓存");
+        StpUtil.login(id);    
+        return SaResult.ok();
+    }
+
+    // 测试业务缓存
+    @RequestMapping("test")
+    public SaResult test() {
+        System.out.println("--------------- 测试业务缓存");
+        stringRedisTemplate.opsForValue().set("hello", "Hello World");
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
+

分别访问两个接口,观察Redis中增加的数据:
+Alone-Redis

+

二、网关统一鉴权

+

微服务架构下的鉴权一般分为两种:

+
    +
  • 每个服务各自鉴权。
  • +
  • 网关统一鉴权。
  • +
+

方案一和传统单体鉴权差别不大,不再过多赘述,下面介绍方案二的整合步骤:

+

1、引入正确的依赖:

+

在交流群里不少小伙伴提出bug解决到最后发现都是因为依赖引入错误导致的,此处重点强调一下:

+
+

在微服务架构中使用Sa-Token时,网关和内部服务要分开引入Sa-Token依赖(不要直接在顶级父pom中引入Sa-Token)

+
+

总体来讲,我们需要关注的依赖就是两个:sa-token-spring-boot-startersa-token-reactor-spring-boot-starter

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+
<!-- Sa-Token 权限认证(Reactor响应式集成) -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
+    <version>1.34.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

至于怎么分辨我们需要引入哪个呢?这个要看你使用的基础框架:

+

对于内部基础服务来讲,我们一般都是使用SpringBoot默认的web模块:SpringMVC,
+因为这个SpringMVC是基于Servlet模型的,在这里我们需要引入的是sa-token-spring-boot-starter

+

对于网关服务,大体来讲分为两种:
+- 一种是基于Servlet模型的,如:Zuul,我们需要引入的是:sa-token-spring-boot-starter,详细戳:在SpringBoot环境集成
+- 一种是基于Reactor模型的,如:SpringCloud Gateway,我们需要引入的是:sa-token-reactor-spring-boot-starter

+

注:切不可直接在一个项目里同时引入这两个依赖,否则会造成项目无法启动。

+

另外,我们需要引入Redis集成包,因为我们的网关和子服务主要通过Redis来同步数据

+
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-dao-redis-jackson</artifactId>
+    <version>1.34.0</version>
+</dependency>
+<dependency>
+    <groupId>org.apache.commons</groupId>
+    <artifactId>commons-pool2</artifactId>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

2、实现鉴权接口

+
/**
+ * 自定义权限验证接口扩展 
+ */
+@Component   
+public class StpInterfaceImpl implements StpInterface {
+
+    @Override
+    public List<String> getPermissionList(Object loginId, String loginType) {
+        // 返回此 loginId 拥有的权限列表 
+        return ...;
+    }
+
+    @Override
+    public List<String> getRoleList(Object loginId, String loginType) {
+        // 返回此 loginId 拥有的角色列表
+        return ...;
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
+

关于数据的获取,建议以下方案三选一:
+1. 在网关处集成ORM框架,直接从数据库查询数据
+2. 先从Redis中获取数据,获取不到时走ORM框架查询数据库
+3. 先从Redis中获取缓存数据,获取不到时走RPC调用子服务 (专门的权限数据提供服务) 获取

+

3、注册全局过滤器

+

然后我们就可以在网关处注册全局过滤器进行鉴权操作了:

+
/**
+ * [Sa-Token 权限认证] 配置类 
+ * @author kong
+ */
+@Configuration
+public class SaTokenConfigure {
+    // 注册 Sa-Token全局过滤器 
+    @Bean
+    public SaReactorFilter getSaReactorFilter() {
+        return new SaReactorFilter()
+            // 拦截地址 
+            .addInclude("/**")    /* 拦截全部path */
+            // 开放地址 
+            .addExclude("/favicon.ico")
+            // 鉴权方法:每次访问进入 
+            .setAuth(obj -> {
+                // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 
+                SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
+
+                // 权限认证 -- 不同模块, 校验不同权限 
+                SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
+                SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
+                SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
+                SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
+
+                // 更多匹配 ...  */
+            })
+            // 异常处理方法:每次setAuth函数出现异常时进入 
+            .setError(e -> {
+                return SaResult.error(e.getMessage());
+            })
+            ;
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
+

在此处,你可以按照 路由拦截鉴权 模式进行鉴权。

+

Sa-Token 认证流程图

+
+

参考资料

+ +
+← 前后端分离架构下使用 Sa-Token 完成登录认证 +对敏感操作的二次认证 —— 详解 Sa-Token 二级认证 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/featured/qian-tan-ti-ren-xia-xian-de-she-ji-si-lu-fu-dai-ma-shi-xian-fang-an-2.html b/sa-token-doc/blog/featured/qian-tan-ti-ren-xia-xian-de-she-ji-si-lu-fu-dai-ma-shi-xian-fang-an-2.html new file mode 100644 index 000000000..bd6f5c202 --- /dev/null +++ b/sa-token-doc/blog/featured/qian-tan-ti-ren-xia-xian-de-she-ji-si-lu-fu-dai-ma-shi-xian-fang-an-2.html @@ -0,0 +1,394 @@ + + + + + 浅谈踢人下线的设计思路!(附代码实现方案) - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

浅谈踢人下线的设计思路!(附代码实现方案)

+ +

前言

+

前两天写了一篇文章,主要讲了下java中如何实现踢人下线,原文链接:java中如何踢人下线?封禁某个账号后使其会话立即掉线!

+

本来只是简单阐述一下踢人下线的业务场景和实现方案,没想到引出那么多大佬把小弟喷的睁不开眼睛,为了避免大家继续喷我,特再写下此篇文章,彻底讲清楚各种场景下踢人下线的设计思路,如有不足之处还请各位大佬轻喷!

+

好了废话不多说,正文开始

+

正文

+

如果把踢人下线比喻成拆房子,那么在学会拆房之前,我们必须要了解这座房子是怎么盖起来的,不同的盖法对应不同的拆法,不能混为一谈

+

对于目前大多数系统来讲,登录主要有两种方式,一是传统Session模式,二是jwt令牌模式

+

传统Session模式

+

我们先以Session模式为例,这种模式是怎么登录的呢?

+

(注:此处的Session不单指HttpSession,指一切使用服务端控制会话的手段)

+

这里我们不使用任何框架,从底层逻辑开始说起。

+

首先,你需要一个全局拦截器,拦截所有会话请求,如果此会话已经登录,那么拦截器放行,如果未登录,直接将此会话强制重定向到登录接口

+
    +
  1. 在登录接口,我们需要接受两个参数:username + password, 拿这两个参数去数据库中获取数据
  2. +
  3. 如果查不到数据,直接返回用户名或密码错误,如果可以查找到数据,那么开始登录
  4. +
  5. 利用一定的算法(例如uuid),生成一个随机字符串,就像这样子:623368f0-ae5e-4475-a53f-93e4225f16ae, 这就是我们的token
  6. +
  7. 现在我们需要做两件事,一是建立此tokenUserId的映射关系,二是把这个token返回给前端
      +
    1. 建立映射:在Redis中添加一条数据,假如userId=10001,那么我们需要RedisUtil.set("623368f0-ae5e-4475-a53f-93e4225f16ae", 10001)
    2. +
    3. token传递给前台,你可以放到Cookie里,或者直接放到返回体body
    4. +
    +
  8. +
  9. 大工告成,会话登录完毕!在全局拦截器里,我们不认userId只认token,谁持有623368f0-ae5e-4475-a53f-93e4225f16ae这个令牌,谁就是用户10001
  10. +
  11. 一个会话访问进来,有token且token有效,那么会话放行!没有?乖乖滚去登录!
  12. +
+

此时不难看出,一个客户端要保持会话登录的两个必要条件:
+1. 此客户端持有token
+2. 这个token是一个有效token,即:可以从Redis中找到对应的UserId

+

而我们要做踢人下线,就必须从这两点至少选择其一开始下手

+

首先我们先明确一点:除非客户端主动注销,否则我们是无法清除一个已经颁发到客户端的token的。

+

(除了Cookie清除技术WebSocket实时推送技术可以做到,但是这两种技术都需要客户端主动配合,我们现在的假设是客户端拒不配合,我们需要将它强制清退下线。)

+

现在,我们只能从第二点下手,即:清除此tokenUserId的映射关系

+

你可能会想,这不简单?Redis清除一个键值,还不是一行代码就能解决的事情?

+

此时你可能漏掉了关键的一点,那就是,我们只在Redis中存储了token -> UserId的映射关系,如果我们要踢出用户10001,正常情况下,我们无法只根据10001找到它对应的token是哪个键值

+

要解决这个问题,我们就必须把UserId -> token的映射关系也存储一份,你可以存储在数据库中,也可以存储在Redis中,为了性能考虑,我们使用Redis

+

现在事情变得简单起来,要踢人下线,我们只需要两步:
+1. 找到账号10001对应的token键值
+2. 删除这个键值

+

OK,踢出成功,待到此账号下一次访问系统时,虽然他携带了token,但是此token已成为无效token,乖乖去登陆吧!

+

此时你可能会说:

+

就这?我创建个集合保存所有要踢出下线的账号,每次拦截器里判断这个会话是否在这个集合中不就OK了?

+

大佬请慢喷!这就是我要说的第二种模式————黑名单机制,且往下看

+

jwt模式

+

jwt模式的登陆步骤与传统Session模式区别不大,在此暂不赘述

+

不同点在于,jwt登陆时,不会在服务器保存任何会话信息,所有的用户参数都被写进了jwt生成的token中

+

(所以jwttoken才会长的那么长!通常两三百字符长度起步)

+

一个会话是否有效,只看这个会话携带的token能不能正常解析出数据!

+

这也就意味着令牌的合法性是令牌自解释的,而不是服务器说了算!

+

所以,相比于传统Session模式jwt对令牌的可控性就弱了很多,无法做到主动清除token -> UserId 映射关系的操作

+

除非你手动更换jwt令牌生成的算法秘钥,但是这样会造成系统中所有令牌全部失效,全部用户集体下线!这是万万不行的。

+

那怎么办?难道我就不能做到踢人下线的操作吗?

+

其实办法肯定是有的,只要思想不滑坡,方法总比困难多!

+

那就是利用黑名单机制:我们要踢出哪个用户,只需要将他的UserId或者jwt-token放进一个黑名单里,然后我们在拦截器里检查每个请求的token或者UserId是否存在于这个黑名单里即可!

+

这种方式和传统Session模式孰优孰劣呢?只能说各有千秋!

+

黑名单机制在存储时节省性能,在拦截器里多了一步黑名单检测的步骤,浪费性能!

+

不过坦白了讲,这丁点的性能的浪费对于现在的CPU来说都是毛毛雨,可以直接忽略!

+

题外话

+

在我一位同事的项目中,给我提供了jwt踢人下线的另一种实现思路:

+

那就是在生成jwt令牌时,加入一个固定的参数当做令牌生成因子,如果要将一个用户踢出下线,只需要修改一下这个因子的值,然后在拦截器里每次校验这个因子生成的令牌是否与客户端传递的令牌一致!即可判断出这个token是否已被拉黑!

+

这种模式提供了一个比较新颖的逻辑算法,但是严格来讲,还是借助服务器存储一定的数据完成的会话验证,仍然属于Session模式。在此暂不展开细讲。

+

代码实现方案?

+

说了这么多理论,总归是要上代码的,由于笔者除了sa-token框架以外没有找到任何一个框架对踢人下线有直接现成的解决方案,所以在此暂以sa-token框架为例

+
    +
  1. 首先添加pom.xml依赖
  2. +
+
<!-- sa-token 权限认证, 在线文档:http://sa-token.com/ -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.12.1</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+
    +
  1. 在用户登录时将账号id写入会话中
  2. +
+
@RestController
+@RequestMapping("user")
+public class UserController {
+    @RequestMapping("doLogin")
+    public String doLogin(String username, String password) {
+        // 此处仅作示例模拟,真实项目需要从数据库中查询数据进行比对 
+        if("zhang".equals(username) && "123456".equals(password)) {
+            StpUtil.setLoginId(10001);
+            return "登录成功";
+        }
+        return "登录失败";
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+
    +
  1. 将指定id的账号踢出在线
  2. +
+
// 使指定id账号的会话注销登录,对方再次访问系统时会抛出`NotLoginException`异常,场景值为-5
+@RequestMapping("kickout")
+public String kickout(long userId) {
+    StpUtil.logoutByLoginId(userId);
+    return "踢出成功";
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

对框架感兴趣的同学可以查看官网:sa-token 一个java轻量级权限认证框架

+

后话

+

文章写的再详细也难免会有遗漏之处,在此还求大家轻喷,可以在评论出留言指出不足之处

+

如果觉得文章写得不错还请大家不要吝惜为文章点个赞,您的支持是我更新的最大动力!

+





+
+← 使用 Sa-Token 解决 WebSocket 握手身份认证 +使用 Sa-Token CORS 策略处理跨域问题(三种方式全版) → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/featured/shi-yong-sa-token-cors-ce-lve-chu-li-kua-yu-wen-ti-san-zhong-fang-shi-quan-ban.html b/sa-token-doc/blog/featured/shi-yong-sa-token-cors-ce-lve-chu-li-kua-yu-wen-ti-san-zhong-fang-shi-quan-ban.html new file mode 100644 index 000000000..fdf0fb34c --- /dev/null +++ b/sa-token-doc/blog/featured/shi-yong-sa-token-cors-ce-lve-chu-li-kua-yu-wen-ti-san-zhong-fang-shi-quan-ban.html @@ -0,0 +1,480 @@ + + + + + 使用 Sa-Token CORS 策略处理跨域问题(三种方式全版) - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

使用 Sa-Token CORS 策略处理跨域问题(三种方式全版)

+ +

前言

+

之前写了一篇 使用 Sa-Token 的全局过滤器解决跨域问题(三种方式全版)
+详解了如何使用 Sa-Token 的全局过滤器解决各种跨域问题,正好最近 Sa-Token 更新了版本,最新版本简化了跨域处理方案,
+本文章将介绍一下最新版本的写法,以便给正在使用新版本的同学做个参考。

+

注:本文章仅针对新版本(v1.42.0 及以上版本)提供参考,低于 v1.42.0 版本可以移步上述文章进行了解。

+
+

在 web 开发中,跨域绝对是比较折磨新同学的一个问题,本文将讲解三种常见的跨域情形,并讲解如何使用 Sa-Token 框架解决跨域问题。

+

什么情况下会发生跨域

+

简单理解,就是你在 A 域名下的页面,去调用 B 域名的接口,浏览器感觉你这次调用可能是不安全的请求行为,于是它需要用 cors 安全策略来确认一下这个请求是由用户真实的意愿发出的,而不是被 csrf 伪造请求攻击偷偷发送的。(这么说只是为了方便大家理解,不是特别严谨,实际上同域名下部分情形也会出现跨域问题)

+

请仔细理解上面这段话,因为它说明了两点:
+- 跨域不是后端接口对前端浏览器的限制,而是前端浏览器对用户的限制。
+- 跨域不是在保护后端接口免受攻击,而是浏览器在保护用户,避免用户发送自己不想发送的请求。

+

请一定要记住上面跨域的本质,明白了症状和原因,我们才能对症下药。

+

一般情况下,我们会碰到三种跨域场景:
+- 1、本地页面调用测试服务器,只在项目开发阶段会有跨域问题。(比较简单)
+- 2、使用 header 头提交 token,产生的跨域问题。(比较常见+通用,推荐使用)
+- 3、使用第三方 Cookie 提交 token,产生的跨域问题。(最古老的方案,目前新版浏览器对此方案限制越来越严格,非必要不选择此方案,如果对此方案不是很熟悉就贸然使用也容易出现安全问题)

+

跨域情形一:只在项目开发阶段会有跨域问题

+

有些公司项目的开发方式为:
+- 在项目开发时:使用本地页面调用测试服务器接口。(域名不同,存在跨域问题)
+- 在项目部署时:将后端接口和前端页面部署在同一域名下。(域名一致,不存在跨域问题)

+

这种情况下比较好解决,在代码层面我们无需任何更改,只在前端客户端做出一定的更改就行了。比如说:在前端配置一个代理服务器,或者修改一下 Chrome 客户端使其去除跨域限制。

+

具体的方案有很多,大家可参考这篇博客:手把手教你解决web前端跨域问题

+

上面是说的普通前后端分离开发,而在APP、小程序 开发中,其天然就是个没有跨域限制的客户端,我们什么都不用做就能解决跨域问题。

+

跨域情形二:使用 header 头提交 token,产生的跨域问题(比较常见+通用,推荐使用)

+

当你使用 header 头提交 token 时,会产生跨域问题。此方案比较常见+通用,推荐使用。

+

jquery 代码示例:

+
    $.ajax({
+        url: "/user/getInfo",
+        type: "post", 
+        data: {},
+        dataType: 'json',
+        headers: {
+            "X-Requested-With": "XMLHttpRequest",
+            // 重点处:请求的 header 头里塞入自定义参数
+            "satoken": localStorage.getItem("satoken")
+        },
+        success: function(res){
+            console.log(res);
+        },
+        error: function(xhr, type, errorThrown){
+            return alert("异常:" + JSON.stringify(xhr));
+        }
+    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
+

Axios 代码示例:

+
    axios({
+        url: "/user/getInfo",
+        method: 'post',
+        data: {},
+        headers: {
+            "Content-Type": "application/x-www-form-urlencoded",
+            // 重点处:请求的 header 头里塞入自定义参数
+            "satoken": localStorage.getItem("satoken")
+        }
+    }).
+    then(function (response) { // 成功时执行
+        const res = response.data;
+        console.log(res);
+    }).
+    catch(function (error) {
+        return alert("异常:" + JSON.stringify(error));
+    })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
+

此时在后端,我们应该添加以下响应头:

+
/**
+ * [Sa-Token 权限认证] 配置类 
+ *
+ * @author click33
+ */
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+
+    /**
+     * CORS 跨域处理策略
+     */
+    @Bean
+    public SaCorsHandleFunction corsHandle() {
+        return (req, res, sto) -> {
+            res.
+                // 允许指定域访问跨域资源
+                setHeader("Access-Control-Allow-Origin", "*")
+                // 允许所有请求方式
+                .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
+                // 有效时间
+                .setHeader("Access-Control-Max-Age", "3600")
+                // 允许的header参数
+                .setHeader("Access-Control-Allow-Headers", "*");
+
+            // 如果是预检请求,则立即返回到前端
+            SaRouter.match(SaHttpMethod.OPTIONS)
+                .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
+                .back();
+        };
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
+

WebFlux 同理,此处理策略全环境通用。

+ +

这是最古老的方案,目前新版浏览器对此方案限制越来越严格,非必要不选择此方案,如果对此方案不是很熟悉就贸然使用也容易出现安全问题。

+

jquery 代码示例:

+
    $.ajax({
+        url: "/user/getInfo",
+        type: "post", 
+        data: {},
+        dataType: 'json',
+        // 重点处:指定是跨域模式,需要提交第三方 Cookie 
+        crossDomain: true,
+        xhrFields:{
+            withCredentials: true
+        },
+        headers: {
+            "X-Requested-With": "XMLHttpRequest"
+        },
+        success: function(res){
+            console.log(res);
+        },
+        error: function(xhr, type, errorThrown){
+            return alert("异常:" + JSON.stringify(xhr));
+        }
+    });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
+

Axios 代码示例:

+
    axios({
+        url: "/user/getInfo",
+        method: 'post',
+        data: {},
+        // 重点处:开启第三方 Cookie 
+        withCredentials: true,
+        headers: {
+            "Content-Type": "application/x-www-form-urlencoded"
+        }
+    }).
+    then(function (response) { // 成功时执行
+        console.log(res);
+    }).
+    catch(function (error) {
+        return alert("异常:" + JSON.stringify(error));
+    })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
+

此时在后端,我们应该添加以下响应头:

+
/**
+ * [Sa-Token 权限认证] 配置类 
+ *
+ * @author click33
+ */
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+
+    /**
+     * CORS 跨域处理策略
+     */
+    @Bean
+    public SaCorsHandleFunction corsHandle() {
+        return (req, res, sto) -> {
+            // 获得客户端domain
+            String origin = req.getHeader("Origin");
+            if (origin == null) {
+                origin = req.getHeader("Referer");
+            }
+
+            // ---------- 设置跨域响应头 ----------
+            res
+                // 允许第三方 Cookie
+                .setHeader("Access-Control-Allow-Credentials", "true")
+                // 允许指定域访问跨域资源
+                .setHeader("Access-Control-Allow-Origin", origin)
+                // 允许所有请求方式
+                .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
+                // 允许的header参数
+                .setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken")
+                // 有效时间
+                .setHeader("Access-Control-Max-Age", "3600")
+            ;
+
+            // 如果是预检请求,则立即返回到前端
+            SaRouter.match(SaHttpMethod.OPTIONS)
+                .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
+                .back();
+        };
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
+

WebFlux 同理,此处理策略全环境通用。

+

注意:根据浏览器最新标准,Cookie 模式认证需要做到以下两点:

+

1、Cookie 属性指定 secure=true 以及 sameSite=None

+
sa-token:
+    is-log: true
+    cookie:
+        # 指明当前为 https 安全连接
+        secure: true
+        # 指明第三方 Cookie 限制级别为:不限制
+        sameSite: None
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

2、后端的服务必须是 https 形式,前端无要求。

+
+← 浅谈踢人下线的设计思路!(附代码实现方案) +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/featured/shi-yong-sa-token-jie-jue-websocket-wo-shou-shen-fen-ren-zheng-2.html b/sa-token-doc/blog/featured/shi-yong-sa-token-jie-jue-websocket-wo-shou-shen-fen-ren-zheng-2.html new file mode 100644 index 000000000..a24f9bd5f --- /dev/null +++ b/sa-token-doc/blog/featured/shi-yong-sa-token-jie-jue-websocket-wo-shou-shen-fen-ren-zheng-2.html @@ -0,0 +1,668 @@ + + + + + 使用 Sa-Token 解决 WebSocket 握手身份认证 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

使用 Sa-Token 解决 WebSocket 握手身份认证

+ +

前言

+

相比于 Http 的单项通信方式,WebSocket 可以从服务器向浏览器主动推送消息,这一特性可以帮助我们完成诸如 订单消息推送、IM实时聊天 等一些特定业务。

+

然而 WebSocket 本身对“身份认证”并没有提供直接的支持,对客户端的连接默认是“来者不拒”,所以认证授权这个事,得我们自己动手。

+

Sa-Token 是一个 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
+GitHub 开源地址:https://github.com/dromara/sa-token

+

下面我们介绍一下如何在 WebSocket 中集成 Sa-Token 身份认证,保证连接的安全性。

+

两种集成方式

+

我们将依次介绍目前最常见的两种集成 WebSocket 方式:

+
    +
  • Java 原生版:javax.websocket.Session
  • +
  • Spring 封装版:WebSocketSession
  • +
+

废话不多说,直接开搞:

+

方式一:Java 原生版 javax.websocket.Session

+
1、首先是引入 pom.xml 依赖
+
<!-- SpringBoot依赖 -->
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-web</artifactId>
+</dependency>
+
+<!-- WebScoket 依赖 -->
+<dependency>  
+    <groupId>org.springframework.boot</groupId>  
+    <artifactId>spring-boot-starter-websocket</artifactId>  
+</dependency>
+
+<!-- Sa-Token 权限认证, 在线文档:http://sa-token.com/ -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.29.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
+
2、登录接口,用于获取会话token
+
/**
+ * 登录测试 
+ */
+@RestController
+@RequestMapping("/acc/")
+public class LoginController {
+
+    // 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
+    @RequestMapping("doLogin")
+    public SaResult doLogin(String name, String pwd) {
+        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+            StpUtil.login(10001);
+            return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue());
+        }
+        return SaResult.error("登录失败");
+    }
+
+    // ... 
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
+
3、WebSocket连接处理
+
@Component
+@ServerEndpoint("/ws-connect/{satoken}")
+public class WebSocketConnect {
+
+    /**
+     * 固定前缀 
+     */
+    private static final String USER_ID = "user_id_";
+
+     /** 
+      * 存放Session集合,方便推送消息 (javax.websocket.Session)  
+      */
+    private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
+
+    // 监听:连接成功
+    @OnOpen
+    public void onOpen(Session session, @PathParam("satoken") String satoken) throws IOException {
+
+        // 根据 token 获取对应的 userId 
+        Object loginId = StpUtil.getLoginIdByToken(satoken);
+        if(loginId == null) {
+            session.close();
+            throw new SaTokenException("连接失败,无效Token:" + satoken);
+        }
+
+        // put到集合,方便后续操作 
+        long userId = SaFoxUtil.getValueByType(loginId, long.class);
+        sessionMap.put(USER_ID + userId, session);
+
+        // 给个提示 
+        String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId;
+        System.out.println(tips);
+        sendMessage(session, tips);
+    }
+
+    // 监听: 连接关闭
+    @OnClose
+    public void onClose(Session session) {
+        System.out.println("连接关闭,sid=" + session.getId());
+        for (String key : sessionMap.keySet()) {
+            if(sessionMap.get(key).getId().equals(session.getId())) {
+                sessionMap.remove(key);
+            }
+        }
+    }
+
+    // 监听:收到客户端发送的消息 
+    @OnMessage
+    public void onMessage(Session session, String message) {
+        System.out.println("sid为:" + session.getId() + ",发来:" + message);
+    }
+
+    // 监听:发生异常 
+    @OnError
+    public void onError(Session session, Throwable error) {
+        System.out.println("sid为:" + session.getId() + ",发生错误");
+        error.printStackTrace();
+    }
+
+    // ---------
+
+    // 向指定客户端推送消息 
+    public static void sendMessage(Session session, String message) {
+        try {
+            System.out.println("向sid为:" + session.getId() + ",发送:" + message);
+            session.getBasicRemote().sendText(message);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 向指定用户推送消息 
+    public static void sendMessage(long userId, String message) {
+        Session session = sessionMap.get(USER_ID + userId);
+        if(session != null) {
+            sendMessage(session, message);
+        }
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
+
4、WebSocket配置
+
/**
+ * 开启WebSocket支持
+ */
+@Configuration  
+public class WebSocketConfig { 
+
+    @Bean  
+    public ServerEndpointExporter serverEndpointExporter() {  
+        return new ServerEndpointExporter();  
+    }
+
+} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+
5、启动类
+
@SpringBootApplication
+public class SaTokenWebSocketApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(SaTokenWebSocketApplication.class, args); 
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

搭建完毕,启动项目

+
6、测试
+

1、首先我们访问登录接口,拿到会话token

+
http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
  • 1
+

如图所示:

+

令牌

+

2、然后我们随便找一个WebSocket在线测试页面进行连接
+,例如:https://www.bejson.com/httputil/websocket/

+

连接地址:

+
ws://localhost:8081/ws-connect/302ee2f8-60aa-42aa-8ecb-eeae5ba57015
  • 1
+

如图所示:

+

测试连接

+

3、如果我们输入一个错误的token,会怎样呢?

+

连接失败

+

可以看到,连接会被立即断开!

+

方式二:Spring 封装版:WebSocketSession

+
1、同上:首先是引入 pom.xml 依赖
+
<!-- SpringBoot依赖 -->
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-web</artifactId>
+</dependency>
+
+<!-- WebScoket 依赖 -->
+<dependency>  
+    <groupId>org.springframework.boot</groupId>  
+    <artifactId>spring-boot-starter-websocket</artifactId>  
+</dependency>
+
+<!-- Sa-Token 权限认证, 在线文档:http://sa-token.com/ -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.29.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
+
2、登录接口,用于获取会话token
+
/**
+ * 登录测试 
+ */
+@RestController
+@RequestMapping("/acc/")
+public class LoginController {
+
+    // 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
+    @RequestMapping("doLogin")
+    public SaResult doLogin(String name, String pwd) {
+        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+            StpUtil.login(10001);
+            return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue());
+        }
+        return SaResult.error("登录失败");
+    }
+
+    // ... 
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
+
3、WebSocket 连接处理
+
/**
+ * 处理 WebSocket 连接 
+ */
+public class MyWebSocketHandler extends TextWebSocketHandler {
+
+    /**
+     * 固定前缀 
+     */
+    private static final String USER_ID = "user_id_";
+
+    /**
+     * 存放Session集合,方便推送消息
+     */
+    private static ConcurrentHashMap<String, WebSocketSession> webSocketSessionMaps = new ConcurrentHashMap<>();
+
+    // 监听:连接开启 
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+
+        // put到集合,方便后续操作 
+        String userId = session.getAttributes().get("userId").toString();
+        webSocketSessionMaps.put(USER_ID + userId, session);
+
+
+        // 给个提示 
+        String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId;
+        System.out.println(tips);
+        sendMessage(session, tips);
+    }
+
+    // 监听:连接关闭 
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+        // 从集合移除 
+        String userId = session.getAttributes().get("userId").toString();
+        webSocketSessionMaps.remove(USER_ID + userId);
+
+        // 给个提示 
+        String tips = "Web-Socket 连接关闭,sid=" + session.getId() + ",userId=" + userId;
+        System.out.println(tips);
+    }
+
+    // 收到消息 
+    @Override
+    public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
+        System.out.println("sid为:" + session.getId() + ",发来:" + message);
+    }
+
+    // ----------- 
+
+    // 向指定客户端推送消息 
+    public static void sendMessage(WebSocketSession session, String message) {
+        try {
+            System.out.println("向sid为:" + session.getId() + ",发送:" + message);
+            session.sendMessage(new TextMessage(message));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 向指定用户推送消息 
+    public static void sendMessage(long userId, String message) {
+        WebSocketSession session = webSocketSessionMaps.get(USER_ID + userId);
+        if(session != null) {
+            sendMessage(session, message);
+        }
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
+
4、WebSocket 前置拦截器
+
/**
+ * WebSocket 握手的前置拦截器 
+ */
+public class WebSocketInterceptor implements HandshakeInterceptor {
+
+    // 握手之前触发 (return true 才会握手成功 )
+    @Override
+    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler,
+            Map<String, Object> attr) {
+
+        System.out.println("---- 握手之前触发 " + StpUtil.getTokenValue());
+
+        // 未登录情况下拒绝握手 
+        if(StpUtil.isLogin() == false) {
+            System.out.println("---- 未授权客户端,连接失败");
+            return false;
+        }
+
+        // 标记 userId,握手成功 
+        attr.put("userId", StpUtil.getLoginIdAsLong());
+        return true;
+    }
+
+    // 握手之后触发 
+    @Override
+    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
+            Exception exception) {
+        System.out.println("---- 握手之后触发 ");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
+
5、WebSocket 配置
+
/**
+ * WebSocket 相关配置 
+ */
+@Configuration
+@EnableWebSocket
+public class WebSocketConfig implements WebSocketConfigurer {
+
+    // 注册 WebSocket 处理器 
+    @Override
+    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
+        webSocketHandlerRegistry
+                // WebSocket 连接处理器 
+                .addHandler(new MyWebSocketHandler(), "/ws-connect")
+                // WebSocket 拦截器 
+                .addInterceptors(new WebSocketInterceptor())
+                // 允许跨域 
+                .setAllowedOrigins("*");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
+
6、启动类
+
/**
+ * Sa-Token 整合 WebSocket 鉴权示例 
+ */
+@SpringBootApplication
+public class SaTokenWebSocketSpringApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(SaTokenWebSocketSpringApplication.class, args); 
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

启动项目,开始测试

+
7、测试
+

1、首先访问登录接口,拿到会话token

+
http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
  • 1
+

如图所示:

+

令牌

+

2、然后打开WebSocket在线测试页面进行连接
+,例如:https://www.bejson.com/httputil/websocket/

+

连接地址:

+
ws://localhost:8081/ws-connect?satoken=fe6e7dbd-38b8-4de2-ae05-cda7e36bf2f7
  • 1
+

如图所示:

+

测试连接

+

注:这里采用 url 传递 Token 是因为在第三方测试页面上这样比较方便,真实项目中可以从Cookie、Header参数、url参数 三种方式任选其一传递会话令牌,效果同等

+

3、如果输入一个错误的 Token

+

连接失败

+

连接失败!

+

示例地址

+

以上代码已经上传git,示例地址:
+码云:sa-token-demo-websocket

+

参考资料

+ +
+← 一个登录功能也能玩出这么多花样?sa-token带你轻松搞定多地登录、单地登录、同端互斥登录 +浅谈踢人下线的设计思路!(附代码实现方案) → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/featured/yi-ge-deng-lu-gong-neng-ye-neng-wan-chu-zhe-me-duo-hua-yang-sa-token-dan-di-deng.html b/sa-token-doc/blog/featured/yi-ge-deng-lu-gong-neng-ye-neng-wan-chu-zhe-me-duo-hua-yang-sa-token-dan-di-deng.html new file mode 100644 index 000000000..da12343c6 --- /dev/null +++ b/sa-token-doc/blog/featured/yi-ge-deng-lu-gong-neng-ye-neng-wan-chu-zhe-me-duo-hua-yang-sa-token-dan-di-deng.html @@ -0,0 +1,399 @@ + + + + + 一个登录功能也能玩出这么多花样?sa-token带你轻松搞定多地登录、单地登录、同端互斥登录 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

一个登录功能也能玩出这么多花样?sa-token带你轻松搞定多地登录、单地登录、同端互斥登录

+ +

需求场景

+

说起登录,你可能会不屑一顾,还有比这更简单的功能吗?

+

获取一下用户提交参数 username + password 和数据库中一比对,有记录返回[登录成功],无记录返回[用户名或密码错误]

+

什么,就这?

+

当你熟练的打包、部署、启动项目开始了一天的摸鱼之后,产品经理开始坐不住了

+

“小顺子啊,你看咱们的APP登录能不能加一个功能,就是那种......那个......一个用户登录之后,能把上一个登录的自动挤下线”

+

此时的你陷入了沉思,怎么让他在登录之后,把上一个登录者的会话给挤下线呢?

+

难道说要在每次登录之后循环一遍Session列表,找到与此用户同账号的会话将其注销,聪明如你马上想到了这种方案将会给服务器带来巨大的性能压力!

+

那怎么办?难道要建个Map以userId做keySession做value,建立起映射关系,然后手动取出Session做上标记[已被挤下线]?

+

说干就干,当你撸起袖子,噼里啪啦敲好上述逻辑之后,然后测试、打包、部署、上传一气呵成,又开始了一天的摸鱼......

+

然而你还是低估了产品经理的脑洞能力

+

“小顺子,你看你写的功能有点小问题啊,我每次一登录,就会把其它登录地给挤掉线啊。”

+

此时的你下意识反驳到: "有什么问题?这难道不就是你想要的效果吗?"

+

“en....就是....咱们能不能这样,我在手机上登录,能不能只把别的手机上给挤下线,但是我电脑上已经登录的不受影响”

+

“挤掉肯定是全部挤掉啊,怎么可能只留下你电脑端不挤掉呢?你要的功能不可能做到”

+

只见此时产品经理嘴角轻轻一笑,放出了大招:

+

“那人家腾讯QQ是怎么做到的呢?”

+

一句话暴击99999+,顿时你哑口无言,是呀,腾讯QQ怎么做到这种功能的呢?一个QQ号可以在手机和电脑上同时在线,但是却不能两个手机同时在线

+

难道说在登录时再记录时每次登录的设备标识?循环检测登录列表的设备名称,同设备挤下线,不同设备保持登录?

+

产品经理冲上咖啡,带着胜利的微笑离开了房间,只留下一脸愁容的你,冥思苦想着实现方案……

+
+

正题

+

好了,说了这么多,下面进入今天的主题————sa-token,一个可以让你轻松解决各种登录问题的权限认证框架!

+

如上述场景所言,你遇到的问题不过是三个典型的登录模型:多地登录、单地登录、同端互斥登录

+
    +
  • 多地登录:指同一账号可以在任意地点同时登录,互不影响
  • +
  • 单地登录:在同一时间一个账号只能在一个地点登录,新登录会挤掉旧登录者
  • +
  • 同端互斥登录:在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线
  • +
+

接下来让我们看看使用sa-token是如何轻松处理这三种登录问题的

+

多地登录

+

此模式较为简单,sa-token默认模式即为多地登录模式

+
    +
  1. 首先添加pom.xml框架
  2. +
+
<!-- sa-token 权限认证, 在线文档:http://sa-token.com/ -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.12.1</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+
    +
  1. 在用户登录时将账号id写入会话中
  2. +
+
@RestController
+@RequestMapping("user")
+public class UserController {
+    @RequestMapping("doLogin")
+    public String doLogin(String username, String password) {
+        // 此处仅作示例模拟,真实项目需要从数据库中查询数据进行比对 
+        if("zhang".equals(username) && "123456".equals(password)) {
+            StpUtil.setLoginId(10001);
+            return "登录成功";
+        }
+        return "登录失败";
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+
    +
  1. 新建启动类启动
  2. +
+
@SpringBootApplication
+public class SaTokenDemoApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaTokenDemoApplication.class, args); 
+        System.out.println("\n启动成功:sa-token配置如下:" + SaTokenManager.getConfig());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

至此,我们已经完成了多地点登录的全部代码,上述代码在多人登录同一账号时将不会对旧会话做任何处理,同一账号可以在多个地点任意登录,互不影响

+

单地登录

+

单地登录多地登录唯一的差异就是, 需要改一下yml配置文件

+
spring: 
+    # sa-token配置
+    sa-token: 
+        allow-concurrent-login: false
  • 1
  • 2
  • 3
  • 4
+

配置项 allow-concurrent-login 的含义为:是否允许同一账号并发登录 (此值为true时允许一起登录, 为false时新登录挤掉旧登录)

+

其它代码与[多地登录]无异,当我们在两个浏览器分别登录同一账号时,旧会话再次访问系统将会得到如下提示:

+
{
+    "code": 401,
+    "msg": "token已被顶下线",
+    "data": null,
+    "dataCount": null
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

同端互斥登录

+

好了,终于到了最终难题,同端互斥登录可以让我们像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线

+

那么在sa-token中如何做到同端互斥登录呢?

+首先如单地登录一样,在配置文件中,将 allowConcurrentLogin 配置为false,然后调用登录等相关接口时声明设备标识即可:

+
指定设备标识登录
+
StpUtil.setLoginId(10001, "PC");    
  • 1
+

调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 NotLoginException 异常,场景值=-4

+
指定设备标识强制注销(踢人下线)
+
StpUtil.logoutByLoginId(10001, "PC");       
  • 1
+

如果第二个参数填写null或不填,代表将这个账号id所有在线端踢下线,被踢出者再次访问系统时会抛出 NotLoginException 异常,场景值=-5

+
查询当前登录的设备标识
+
StpUtil.getLoginDevice(); 
  • 1
+

结尾

+

以上就是sa-token框架在处理登录问题时的各种技巧,可以看出不管是简单的多地登录还是复杂的同端互斥登录,在sa-token都有完成的解决方案

+

sa-token是近期开源的国产优秀权限认证框架,除了各种登录认证,sa-token还可以轻松解决项目中的各种权限认证问题,
+比如:踢人下线、自动续签、身份临时切换等常见业务均可以一行代码调用实现,接下来的文章我会逐一介绍这些特性,让大家对sa-token有一个全面的了解

+

如果觉得文章写得不错还请大家不要吝惜为文章点个赞,您的支持是我更新的最大动力!

+

最后附上项目链接:

+ +





+
+ +使用 Sa-Token 解决 WebSocket 握手身份认证 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/index.html b/sa-token-doc/blog/index.html new file mode 100644 index 000000000..022837b6f --- /dev/null +++ b/sa-token-doc/blog/index.html @@ -0,0 +1,446 @@ + + + + + Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+

太强了!Sa-Token 的 NodeJS 版本!

2026-05-16特殊文章

这是由 Sa-Token 社区成员发起的项目 xlt-token:把 sa-token 的鉴权思路带到 NodeJS。本文介绍项目背景、能力概览与仓库地址,方便前端或全栈同学在 Node 技术栈里复用熟悉的权限认证体验,降低跨语言学习成本,也欢迎参与社区共建。

+ + +

使用 Sa-Token CORS 策略处理跨域问题(三种方式全版)

2025-04-11精选

前后端分离几乎绕不开跨域。本文基于 Sa-Token 新版本(v1.42.0+)讲解 CORS 处理的三种方式全版:全局过滤器、策略配置与常见坑,帮你简化跨域方案;低于该版本可参考此前过滤器专题文章,避免照搬过时写法导致联调失败,文内含示例,可直接对照实践。

+ +

Sa-Token v1.41.0 发布 🚀,来看看有没有令你心动的功能!

2025-03-21版本更新

Sa-Token v1.41.0 版本更新发布。本版继续完善登录认证、权限认证、单点登录、OAuth2.0 与微服务网关鉴权等能力,并增强 API 参数签名等特性。文内给出详细更新说明,方便升级评估与测试用例补齐,降低上线后的兼容风险,升级前建议先对照变更做回归。

+

Sa-Token v1.40.0 发布 🚀,来看看有没有令你心动的功能!

2025-02-01版本更新

Sa-Token v1.40.0 版本更新发布。作为轻量级 Java 权限认证框架,本版在登录认证、权限认证、单点登录、OAuth2.0、微服务网关鉴权等方向持续迭代。文内整理完整更新日志,便于对照升级与排查兼容性变化,建议升级前先看清破坏性改动。

+

Sa-Token v1.39.0 更新,大幅重构 OAuth2 模块

2024-08-28版本更新

Sa-Token v1.39.0 版本更新说明:在登录认证、权限认证、分布式 Session、单点登录与 OAuth2.0 等能力上继续打磨。本文汇总本版新增特性、优化细节与问题修复,适合升级前快速浏览,并对照自家项目依赖做回归,避免漏测关键路径。

+

Sa-Token v1.35.0 更新,新增动态 active-timeout 能力

2023-06-27版本更新

Sa-Token v1.35.0 重点新增动态 active-timeout 能力,可按账号或会话灵活调整活跃超时。同时继续完善登录认证、权限认证、分布式 Session、单点登录与 OAuth2.0。文内附完整更新说明,方便针对性试用新能力,并评估对现有超时策略的影响。

+

浅谈踢人下线的设计思路!(附代码实现方案)

2023-04-24精选

踢人下线不只是改库字段那么简单:封禁账号后如何让已在线会话立即掉线?本文顺着上一篇「如何踢人下线」继续深挖设计思路,并附上可参考的代码实现方案,帮你把会话治理做完整,避免「封了还能用」的尴尬,真正做到立刻生效,适合做账号治理与在线会话管控时对照。

+

对敏感操作的二次认证 —— 详解 Sa-Token 二级认证

2023-04-24随笔

删库、改密、转账等敏感操作,即便已登录也常需再次输入密码。本文介绍 Sa-Token 二级认证:在已登录会话上再开一段短时安全窗口,校验通过后才能执行高危操作,在安全性与操作体验之间取得平衡,适合后台高危能力防护,文内含示例,可直接对照实践。

+

在微服务中使用 Sa-Token:分布式Session会话、网关统一鉴权

2023-04-24随笔

微服务里会话与鉴权往往是难点。本文介绍在 Sa-Token 中落地分布式 Session、网关统一鉴权:共享 Redis 会话、Gateway 路由拦截,以及子服务如何复用登录态,减少重复鉴权代码,让整条调用链权限口径一致,降低微服务鉴权维护成本。

+

前后端分离架构下使用 Sa-Token 完成登录认证

2023-04-24随笔

前后端分离架构下,传统 Cookie-Session 往往不够好用。本文讲解如何用 Sa-Token 完成分离架构登录认证:Token 提交方式、前端缓存、跨域携带凭证,以及和 Cookie 模式的取舍,帮你少写一堆兼容适配代码,快速打通前后端登录闭环。

+

别人绕过我的网关直接调用资源服务怎么办?使用 Sa-Token 解决:网关转发鉴权、RPC调用鉴权

2023-04-24随笔

微服务子服务通常不能被外网直连,需经网关转发才算合法请求。若有人绕过网关直调资源服务会怎样?本文用 Sa-Token 讲清网关转发鉴权与 RPC 调用鉴权,堵住「绕过网关」缺口,加固内网服务边界安全,适合已有网关但仍担心直连风险的团队,文内含示例,可直接对照实践。

+

初识sa-token,一行代码搞定登录授权!

2023-04-24其它

Java 界有 Shiro、Spring Security 等老牌权限框架,生态齐全但有时偏重。本文带你初识 Sa-Token:用极简 API、甚至一行代码完成登录授权,快速体验轻量级权限认证框架的开发手感,适合作为接入权限体系的第一篇入门文,先跑通再深入。

+

公众号首宣

2023-04-24其它

JavaWeb 项目离不开认证授权。相较 Shiro、SpringSecurity,在前后端分离主流下往往要写不少兼容代码。本文首宣推荐 Sa-Token:轻量优雅完成登录认证、权限认证、SSO、OAuth2、网关鉴权等能力,适合想换更轻方案的同学快速上手体验。

+

使用 Sa-Token 解决 WebSocket 握手身份认证

2023-04-24精选

WebSocket 可做订单推送、IM 聊天等实时业务,但握手阶段同样需要身份认证。本文讲解如何用 Sa-Token 在 WebSocket 握手时校验登录态,避免未登录连接长连,并给出可落地的接入思路,让实时通道也纳入统一鉴权体系,减少安全盲区。

+ +

使用 Sa-Token 实现 [记住我] 模式登录、七天免登录

2023-04-24随笔

登录页常见「记住我」:勾选后即使关闭浏览器再打开,仍保持登录状态。本文介绍如何用 Sa-Token 实现记住我模式与七天免登录,说明 timeout、Cookie 持久化等配置要点,让长期免登录既好用又可控,兼顾安全与用户体验,文内含示例,可直接对照实践。

+

使用 Sa-Token 完成踢人下线功能

2023-04-24随笔

企业级项目里,踢人下线很常见:按账号 id 或 Token 强制下线、区分强制注销与踢人、并给出友好提示。本文演示如何用 Sa-Token 一行代码完成踢人下线,说明两者差异,以及如何在后台管理系统里做会话治理与在线用户清退,文内含示例,可直接对照实践。

+

不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现

2023-04-24随笔

只想快速挂一个可用登录页,不想自己写前端?本文介绍 Sa-Token-Quick-Login:引入依赖后即可获得开箱即用的登录界面与基础鉴权能力,适合演示项目、后台管理与快速原型,把时间留给真正的业务功能,也能先把登录链路跑通再迭代,文内含示例,可直接对照实践。

+

一文详解 Sa-Token 中的 SaSession 对象

2023-04-24随笔

SaSession 是 Sa-Token 的会话对象,可在 Token 或账号维度存取业务数据。本文详解 Account-Session、Token-Session、自定义 Session 的使用场景与 API,对比传统 HttpSession,帮助你在分布式环境里正确管理会话数据,避免存错维度。

+ +

java中如何踢人下线?封禁某个账号后使其会话立即掉线!

2023-04-24其它

论坛社区常有封禁需求:只改用户表 status 不够,已在线会话可能仍有效。本文讲解 Java 中如何真正踢人下线——封禁账号后让会话立即掉线,并给出用 Sa-Token 落地的思路与示例,把账号治理落到「立刻生效」,而不是只改数据库看着像封了。

+

SpringBoot 项目使用 Sa-Token 完成登录认证

2023-04-24随笔

登录后才能访问的接口(如查询账号资料)通常要加一层登录校验。本文讲解如何在 SpringBoot 中接入 Sa-Token,用极简 API 完成登录、注销与登录态判断,快速搭好权限认证第一步,少写重复样板代码,适合鉴权入门,文内含示例,可直接对照实践。

+

SpringBoot 使用 Sa-Token 实现账号封禁、分类封禁、阶梯封禁

2023-04-24随笔

踢人下线与强制注销用于清退违规账号,但有时还要封禁以防再次登录。本文介绍 Sa-Token 的账号封禁、分类封禁与阶梯封禁:指定天数、永久封禁、按服务禁用,以及解封时机,适合社区、内容平台与后台治理等账号管控场景,文内含示例,可直接对照实践。

+

SpringBoot 使用 Sa-Token 完成路由拦截鉴权

2023-04-24随笔

注解鉴权虽方便,却不适合所有场景。本文讲解如何用 Sa-Token 做路由拦截式鉴权:按 URL 规则统一校验登录与权限,适配 REST 风格接口,并可与注解鉴权搭配使用,在全局拦截与局部声明之间灵活取舍,覆盖更多网关式鉴权需求,文内含示例,可直接对照实践。

+

SpringBoot 使用 Sa-Token 完成注解鉴权功能

2023-04-24随笔

注解鉴权可以把鉴权逻辑与业务代码优雅分离。本文介绍在 Sa-Token 中如何通过 @SaCheckLogin、@SaCheckPermission、@SaCheckRole 等注解完成登录与权限校验,并说明拦截器注册方式,让鉴权声明式、可读性更好,维护成本更低。

+

SpringBoot 使用 Sa-Token 完成权限认证

2023-04-24随笔

权限认证的核心,是判断账号是否拥有指定权限:每个账号持有一组权限码,框架校验集合里是否包含目标权限码。本文介绍在 SpringBoot 中用 Sa-Token 完成权限认证与角色认证,并给出常用校验 API 与注解写法,方便直接落地,文内含示例,可直接对照实践。

+

Sa-Token之注解鉴权:优雅的将鉴权与业务代码分离!

2023-04-24其它

Sa-Token 是轻量级 Java 权限认证框架。本文重点介绍注解鉴权:用注解优雅地把鉴权与业务代码分离,少写重复 if 判断,让登录校验、权限校验、角色校验写起来更干净,也更易维护,适合快速提升项目鉴权可读性与团队协作效率,文内含示例,可直接对照实践。

+

Sa-Token 集成 jwt

2023-04-24随笔

无状态 JWT 在分布式系统中很常见。本文介绍如何用 sa-token-jwt 插件把 Sa-Token 与 JWT 整合:签发与解析、登录态校验,以及相对传统共享 Session 方案的优劣与适用场景,帮你按业务选型而不是盲目跟风,减少后期架构返工。

+ + +

Sa-Token 单点登录第四篇:跨域、跨 Redis 下的 SSO 对接步骤

2023-04-24随笔

本篇是 Sa-Token SSO 系列第四篇:跨域且不共享 Redis 的完整模式。介绍通过 HTTP 校验 Ticket、拉取用户信息的对接流程,适合各子系统独立部署、数据中心隔离的单点登录场景,也是生产环境常见架构,配置步骤可直接照着落地。

+

Sa-Token 单点登录第六篇:定制化登录页面

2023-04-24随笔

本篇是 Sa-Token SSO 系列第六篇:定制化登录页面。介绍如何替换默认登录页、接入自有 UI 与品牌样式,并保证认证中心跳转、Ticket 下发与回调流程仍然完整可用,让统一认证中心既好用又好看,满足企业品牌与交互定制需求,建议结合本系列前后篇一起看。

+

Sa-Token 单点登录第五篇:前后端分离架构下的 SSO 对接步骤

2023-04-24随笔

本篇是 Sa-Token SSO 系列第五篇:前后端分离架构下的单点登录对接。讲解无 Cookie 场景如何携带 Token、前端跳转登录页与回调处理,以及与传统同域 Cookie 方案的差异,方便 SPA、小程序等终端接入 SSO,打通前后端分离登录链路。

+

Sa-Token 单点登录第二篇:同域、同 Redis 下的 SSO 对接步骤

2023-04-24随笔

本篇是 Sa-Token SSO 系列第二篇:同域且共享 Redis 场景下的客户端对接。讲解最简模式的配置步骤、登录跳转与会话共享原理,适合同一顶级域名下的多系统快速打通单点登录,上手成本最低,也是理解后续复杂模式的最佳起点,建议结合本系列前后篇一起看。

+

Sa-Token 单点登录第三篇:跨域、同 Redis 下的 SSO 对接步骤

2023-04-24随笔

本篇是 Sa-Token SSO 系列第三篇:跨域但共享 Redis 的对接方式。说明 Ticket 换取、跨域 Cookie 限制与解决思路,帮助不同域名系统在共享缓存前提下完成单点登录,兼顾架构约束与对接复杂度,适合子系统域名拆分但仍共用 Redis 的团队。

+

Sa-Token 单点登录第一篇:搭建 SSO-Server 统一认证中心

2023-04-24随笔

本篇是 Sa-Token SSO 系列第一篇:从零搭建 SSO-Server 统一认证中心。介绍单点登录整体架构、三种模式差异,以及认证中心的核心接口与配置,为后续同域、跨域、跨 Redis、前后端分离等对接文章打好基础,建议按系列顺序阅读。

+

Sa-Token v1.34.0 更新,适配 SpringBoot3

2023-04-24版本更新

Sa-Token v1.34.0 版本更新说明:继续增强登录认证、权限认证、分布式 Session、单点登录与 OAuth2.0 能力。本文整理更新日志中的新增、优化与修复内容,便于版本升级时查阅,减少漏看关键变更的风险,也方便写进内部升级说明。

+

Sa-Token v1.33.0 更新,新增全局 Log 模块

2023-04-24版本更新

Sa-Token v1.33.0 版本更新说明:围绕登录认证、权限认证、分布式 Session、单点登录与 OAuth2.0 等核心场景持续迭代。文内汇总本版变更清单,方便对照官方文档完成平滑升级,并同步团队内的版本策略与发布窗口安排,升级前建议先对照变更做回归。

+

Sa-Token v1.32.0 更新,新增账号分类封禁、阶梯封禁功能

2023-04-24版本更新

Sa-Token v1.32.0 版本更新说明:持续完善登录认证、权限认证、分布式 Session、单点登录与 OAuth2.0 等能力。本文列出本版本新增功能、优化项与问题修复,帮助你快速判断是否需要升级,以及升级后要注意什么,减少盲目跟版本带来的风险。

+

Sa-Token v1.31.0 更新,新增账号分类封禁、阶梯封禁功能

2023-04-24版本更新

Sa-Token v1.31.0 版本更新说明:在登录认证、权限认证、分布式 Session、单点登录、OAuth2.0 等能力之上,本版对拦截器与相关 API 做了重要调整。文内整理新增、优化与修复项,便于升级前评估影响范围,并提前准备兼容改造与回归测试。

+

Sa-Token v1.28.0更新,并荣获开源中国“码云GVP认证”

2023-04-24特殊文章

Sa-Token v1.28.0 更新,并荣获开源中国「码云 GVP」认证。本版重点完善与 JWT、Dubbo 的整合,新增插件与文档章节。同时回顾框架在登录认证、权限认证、SSO、OAuth2、网关鉴权等方面的能力版图,见证社区里程碑,感谢一路同行的开源伙伴。

+

Sa-Token v1.23.0 更新, 正式支持OAuth2.0授权认证

2023-04-24版本更新

Sa-Token v1.23.0 正式支持 OAuth2.0 授权认证,在登录认证、权限认证、Session、单点登录等能力之外补齐标准授权流程。本文汇总本版本更新点与 OAuth2 相关能力说明,方便你对照升级并快速验证新特性,评估是否满足开放平台对接需求。

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
博客说明
+
+

本站官方博客开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+

社区博客和推荐博客版权属于其作者本人,如需转载请联系对应作者申请授权。

+
+
+
+ + +
+

如果 Sa-Token 帮助到了你,希望你可以向同事、朋友推荐了解本框架,这对我们非常重要,感谢支持!

+

加油,工程师!

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + diff --git a/sa-token-doc/blog/other/chu-shi-sa-token-yi-xing-dai-ma-gao-ding-deng-lu-shou-quan.html b/sa-token-doc/blog/other/chu-shi-sa-token-yi-xing-dai-ma-gao-ding-deng-lu-shou-quan.html new file mode 100644 index 000000000..3889c682a --- /dev/null +++ b/sa-token-doc/blog/other/chu-shi-sa-token-yi-xing-dai-ma-gao-ding-deng-lu-shou-quan.html @@ -0,0 +1,335 @@ + + + + + 初识sa-token,一行代码搞定登录授权! - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

初识sa-token,一行代码搞定登录授权!

+ +

前言

+

在java的世界里,有很多优秀的权限认证框架,如Apache ShiroSpring Security 等等。这些框架背景强大,历史悠久,其生态也比较齐全。

+

但同时这些框架也并非十分完美,在前后台分离已成标配的互联网时代,这些老牌框架的很多设计理念已经相当滞后,无法与我们的项目完美契合。

+

而今天我要介绍的这款框架,专门为前后台分离架构而生, 功能强大, 上手简单 —— sa-token。

+

sa-token 是什么?

+

sa-token是一个轻量级Java权限认证框架,主要解决: 登录认证、权限认证、Session会话 等一系列权限相关问题

+

sa-token的API调用非常简单,一行代码即可搞定登录授权,废话不多说,我们直接上示例:

+
1. 添加pom依赖
+
    <!-- sa-token 权限认证, 在线文档:http://sa-token.com/ -->
+    <dependency>
+        <groupId>cn.dev33</groupId>
+        <artifactId>sa-token-spring-boot-starter</artifactId>
+        <version>1.12.0</version>
+    </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+
2. 调用框架API进行登录
+
// 在用户账号密码验证成功后,直接调用以下API进行登录授权
+StpUtil.setLoginId(10001); 
  • 1
  • 2
+

至此,我们已经借助sa-token框架完成登录授权!

+

此时的你的小脑袋可能飘满了问号,就这么简单?自定义Realm呢?全局过滤器呢?我不用写各种配置文件吗?

+

事实上在此我可以负责的告诉你,在sa-token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权!

+

当你受够Shiro、Security等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,sa-token的API设计是多么的清爽!

+

拒绝引入复杂的概念,以实际业务需求为第一目标进行定向突破,业务上需要什么,sa-token就做什么,不搞各种云里雾里的高大上概念,以化繁为简为第一目标!

+

除了以上的登录授权,sa-token还可以一行代码完成以下功能:

+
StpUtil.setLoginId(10001);          // 标记当前会话登录的账号id
+StpUtil.getLoginId();               // 获取当前会话登录的账号id
+StpUtil.isLogin();                  // 获取当前会话是否已经登录, 返回true或false
+StpUtil.logout();                   // 当前会话注销登录
+StpUtil.logoutByLoginId(10001);     // 让账号为10001的会话注销登录(踢人下线)
+StpUtil.hasRole("super-admin");     // 查询当前账号是否含有指定角色标识, 返回true或false
+StpUtil.hasPermission("user:add");  // 查询当前账号是否含有指定权限, 返回true或false
+StpUtil.getSession();               // 获取当前账号id的Session 
+StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
+StpUtil.getTokenValueByLoginId(10001);  // 获取账号id为10001的token令牌值
+StpUtil.setLoginId(10001, "PC");        // 指定设备标识登录
+StpUtil.logoutByLoginId(10001, "PC");   // 指定设备标识进行强制注销 (不同端不受影响)
+StpUtil.switchTo(10044);                // 将当前会话身份临时切换为其它账号 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

sa-token的API众多,请恕此处无法逐一为您展示,以上示例只是框架能力的很小一部分能力展示。

+

sa-token中,各种与登录鉴权相关功能:踢人下线、自动续签、同端互斥登录等常见业务均可以一行代码调用实现

+

关于sa-token的授权登录先介绍至此,接下来的章节我会陆续介绍sa-token框架的其它各种强大能力

+

如果觉得文章写得不错还请大家不要吝惜为文章点个赞,您的支持是我更新的最大动力!

+

最后附上项目链接:

+ +





+
+← 公众号首宣 +
+
+
+
+
+
+
章节目录
+
+
+
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/other/gong-zhong-hao-shou-xuan-2.html b/sa-token-doc/blog/other/gong-zhong-hao-shou-xuan-2.html new file mode 100644 index 000000000..13de4715f --- /dev/null +++ b/sa-token-doc/blog/other/gong-zhong-hao-shou-xuan-2.html @@ -0,0 +1,406 @@ + + + + + 公众号首宣 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

公众号首宣

+ +

前言:

+
+

在 JavaWeb 项目中,认证授权是必不可少的功能,比较传统的框架有 Shiro、SpringSecurity 等等,这些框架在如今“前后端分离” 已成主流的情况下,并不是十分好用,需要写不少兼容性代码才能集成。


+今天给大家推荐的这个框架是 Sa-Token,可以让我们非常优雅的集成鉴权功能!


+GitHub开源地址:https://github.com/dromara/sa-token

+
+

Sa-Token 介绍

+

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证Session会话单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。

+

Sa-Token 的 API 设计非常简单,有多简单呢?以登录认证为例,你只需要:

+
// 在登录时写入当前会话的账号id
+StpUtil.login(10001);
+
+// 然后在需要校验登录处调用以下方法:
+// 如果当前会话未登录,这句代码会抛出 `NotLoginException` 异常
+StpUtil.checkLogin();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

至此,我们已经借助 Sa-Token 完成登录认证!

+

此时的你小脑袋可能飘满了问号,就这么简单?自定义 Realm 呢?全局过滤器呢?我不用写各种配置文件吗?

+

没错,在 Sa-Token 中,登录认证就是如此简单,不需要任何的复杂前置工作,只需这一行简单的API调用,就可以完成会话登录认证!

+

当你受够 SpringSecurity、Shiro 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅!

+

权限认证示例(只有具备 user:add 权限的会话才可以进入请求)

+
@SaCheckPermission("user:add")
+@RequestMapping("/user/insert")
+public String insert(SysUser user) {
+    // ...
+    return "用户增加";
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

将某个账号踢下线(待到对方再次访问系统时会抛出NotLoginException异常)

+
// 使账号id为 10001 的会话强制注销登录
+StpUtil.logoutByLoginId(10001);
  • 1
  • 2
+

在 Sa-Token 中,绝大多数功能都可以 一行代码 完成:

+
StpUtil.login(10001);   // 标记当前会话登录的账号id
+StpUtil.getLoginId();   // 获取当前会话登录的账号id
+StpUtil.isLogin();   // 获取当前会话是否已经登录, 返回true或false
+StpUtil.logout();   // 当前会话注销登录
+StpUtil.logoutByLoginId(10001);   // 让账号为10001的会话注销登录(踢人下线)
+StpUtil.hasRole("super-admin");   // 查询当前账号是否含有指定角色标识, 返回true或false
+StpUtil.hasPermission("user:add");   // 查询当前账号是否含有指定权限, 返回true或false
+StpUtil.getSession();   // 获取当前账号id的Session
+StpUtil.getSessionByLoginId(10001);   // 获取账号id为10001的Session
+StpUtil.getTokenValueByLoginId(10001);   // 获取账号id为10001的token令牌值
+StpUtil.login(10001, "PC");   // 指定设备标识登录,常用于“同端互斥登录”
+StpUtil.logoutByLoginId(10001, "PC");   // 指定设备标识进行强制注销 (不同端不受影响)
+StpUtil.openSafe(120);   // 在当前会话开启二级认证,有效期为120秒
+StpUtil.checkSafe();   // 校验当前会话是否处于二级认证有效期内,校验失败会抛出异常
+StpUtil.switchTo(10044);   // 将当前会话身份临时切换为其它账号
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

即使不运行测试,相信您也能意会到绝大多数 API 的用法。

+

Sa-Token 功能一览

+
    +
  • 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录
  • +
  • 权限认证 —— 权限认证、角色认证、会话二级认证
  • +
  • Session会话 —— 全端共享Session、单端独享Session、自定义Session
  • +
  • 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线
  • +
  • 账号封禁 —— 指定天数封禁、永久封禁、设定解封时间
  • +
  • 持久层扩展 —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
  • +
  • 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案
  • +
  • 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
  • +
  • 单点登录 —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
  • +
  • OAuth2.0认证 —— 基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式
  • +
  • 二级认证 —— 在已登录的基础上再次认证,保证安全性
  • +
  • 独立Redis —— 将权限缓存与业务缓存分离
  • +
  • 临时Token验证 —— 解决短时间的Token授权问题
  • +
  • 模拟他人账号 —— 实时操作任意用户状态数据
  • +
  • 临时身份切换 —— 将会话身份临时切换为其它账号
  • +
  • 前后台分离 —— APP、小程序等不支持Cookie的终端
  • +
  • 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
  • +
  • 多账号认证体系 —— 比如一个商城项目的user表和admin表分开鉴权
  • +
  • 花式token生成 —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀
  • +
  • 注解式鉴权 —— 优雅的将鉴权与业务代码分离
  • +
  • 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配restful模式
  • +
  • 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签
  • +
  • 会话治理 —— 提供方便灵活的会话查询接口
  • +
  • 记住我模式 —— 适配[记住我]模式,重启浏览器免验证
  • +
  • 密码加密 —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
  • +
  • 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作
  • +
  • 开箱即用 —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用
  • +
+

单点登录 & OAuth2.0

+

通过 Sa-Token ,我们可以很方便的搭建单点登录认证中心:

+
/**
+ * Sa-Token-SSO Server端 Controller 
+ */
+@RestController
+public class SsoServerController {
+    // SSO-Server端:处理所有SSO相关请求 
+    @RequestMapping("/sso/*")
+    public Object ssoRequest() {
+        return SaSsoHandle.serverRequest();
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

同样,我们也可以很方便的搭建一个 OAuth2.0 认证中心:

+
/**
+ * Sa-Token-OAuth2.0 Server端 Controller 
+ */
+@RestController
+public class SaOAuth2ServerController {
+    // SSO-Server端:处理所有SSO相关请求 
+    @RequestMapping("/oauth2/*")
+    public Object request() {
+        return SaSsoHandle.serverRequest();
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

详细代码请参考框架官网介绍。

+

总结

+

通过以上示例我们可以看出,Sa-Token 同时兼顾 简单全面 两个优点,是一个非常值得上手的优秀框架。

+

同时,其官网非常文档也写的非常详细,不光介绍了 Sa-Token 框架,还介绍了非常多的权限设计思想,是真正的授人以渔。

+

官方文档

+

参考资料

+
    +
  • 开源地址:https://github.com/dromara/sa-token
  • +
  • 官方文档:http://sa-token.com/
  • +
  • 框架官方讨论群非常活跃,长按扫码即可加入:
  • +
+

加入群聊

+
+← java中如何踢人下线?封禁某个账号后使其会话立即掉线! +初识sa-token,一行代码搞定登录授权! → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/other/java-feng-jin-mou-ge-zhang-hao-hou-shi-qi-hui-hua-li-ji-diao-xian.html b/sa-token-doc/blog/other/java-feng-jin-mou-ge-zhang-hao-hou-shi-qi-hui-hua-li-ji-diao-xian.html new file mode 100644 index 000000000..f402f2dd6 --- /dev/null +++ b/sa-token-doc/blog/other/java-feng-jin-mou-ge-zhang-hao-hou-shi-qi-hui-hua-li-ji-diao-xian.html @@ -0,0 +1,347 @@ + + + + + java中如何踢人下线?封禁某个账号后使其会话立即掉线! - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

java中如何踢人下线?封禁某个账号后使其会话立即掉线!

+ +

需求场景

+

封禁账号是一个比较常见的业务需求,尤其是在论坛、社区类型的项目中,当出现了违规用户时我们需要将其账号立即封禁。

+

常规的设计思路是:在设计用户表时增加一个状态字段,例如:status,其值为1时代表账号正常,其值为0时代表账号已被封禁。

+

当我们需要封禁一个账号时,只需要将其账号的status值修改为0即可,对方再次登录系统时,我们便可以检测到status值不为1禁止登录。

+

这种模式虽然思路简单,但也有一个不小的问题,那就是: 如果对方一直在线不注销登录呢?

+

由于我们只在登录时检测status值,这也就代表:如果对方不主动注销账号,他的会话还是会一直存在且有效。

+

那怎么才可以做到在封禁账号后立即生效?

+

你可能会想到使用拦截器,拦截用户的所有请求检测账号状态:status=0时禁止访问,status=1时再对请求放行

+

此方式虽然解决了问题,但是如果每次请求都要进行数据库查询的话,数据库表示你如此扫荡我你就没有一点心理压力吗?

+

那怎么办?上缓存?虽然可以缓解性能压力,但似乎总感觉没有完美解决问题。

+

真正的问题点在于:一个正常的系统只有0.01%的用户是需要封禁的,我们对其它99.99%用户的实时检测都是不必要的性能浪费。

+

在如上场景中,我们真正需要的是一个踢人下线的操作,即:我们需要在封禁某个用户后,使他的会话立即掉线,即时他重新登录也会被禁止登录

+

那么,怎么做到实时踢人下线呢?

+

笔者使用的是sa-token权限认证框架,这个框架封装了踢人下线操作调用非常方便,不用像其它框架一样还需要我自己再封装一层才能做到。

+

具体代码

+
    +
  1. 首先添加pom.xml框架
  2. +
+
<!-- sa-token 权限认证, 在线文档:http://sa-token.com/ -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.12.1</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+
    +
  1. 在用户登录时将账号id写入会话中
  2. +
+
@RestController
+@RequestMapping("user")
+public class UserController {
+    @RequestMapping("doLogin")
+    public String doLogin(String username, String password) {
+        // 此处仅作示例模拟,真实项目需要从数据库中查询数据进行比对 
+        if("zhang".equals(username) && "123456".equals(password)) {
+            StpUtil.setLoginId(10001);
+            return "登录成功";
+        }
+        return "登录失败";
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+
    +
  1. 将指定id的账号
  2. +
+
// 使指定id账号的会话注销登录,对方再次访问系统时会抛出`NotLoginException`异常,场景值为-5
+@RequestMapping("kickout")
+public String kickout(long userId) {
+    StpUtil.logoutByLoginId(userId);
+    return "剔出成功";
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

关键代码就在 StpUtil.logoutByLoginId(userId) 这一句,使指定id的账号注销登录 (踢人下线)

+

如果觉得文章写得不错还请大家不要吝惜为文章点个赞,您的支持是我更新的最大动力!

+

最后附上项目链接:

+ +





+
+← Sa-Token之注解鉴权:优雅的将鉴权与业务代码分离! +公众号首宣 → +
+
+
+
+
+
+
章节目录
+
+
+
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/other/sa-token-you-ya-de-jiang-jian-quan-yu-ye-wu-dai-ma-fen-li.html b/sa-token-doc/blog/other/sa-token-you-ya-de-jiang-jian-quan-yu-ye-wu-dai-ma-fen-li.html new file mode 100644 index 000000000..238d45be2 --- /dev/null +++ b/sa-token-doc/blog/other/sa-token-you-ya-de-jiang-jian-quan-yu-ye-wu-dai-ma-fen-li.html @@ -0,0 +1,483 @@ + + + + + Sa-Token之注解鉴权:优雅的将鉴权与业务代码分离! - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token之注解鉴权:优雅的将鉴权与业务代码分离!

+ +

Sa-Token 介绍:

+

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证Session会话单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。

+

今天,我们主要介绍一下 Sa-Token 中的注解鉴权,它可以让我们:优雅的将鉴权与业务代码分离

+

GitHub 开源地址:https://github.com/dromara/sa-token

+

前置工作:

+

1、首先我们在 pom.xml 引入依赖:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.26.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

2、注册 Sa-Token 的注解拦截器

+
/**
+ * Sa-Token 配置类 
+ */
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**").excludePathPatterns("");
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

3、实现 StpInterface 接口,返回每个账号拥有的权限码集合

+
/**
+ * 自定义权限验证接口扩展 
+ */
+@Component
+public class StpInterfaceImpl implements StpInterface {
+
+    // 返回一个账号所拥有的权限码集合 
+    @Override
+    public List<String> getPermissionList(Object loginId, String loginType) {
+        return Arrays.asList("101", "user-add", "user-delete", "user-update", "user-get", "article-get");
+    }
+
+    // 返回一个账号所拥有的角色标识集合 
+    @Override
+    public List<String> getRoleList(Object loginId, String loginType) {
+        return Arrays.asList("admin", "super-admin");
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
+

4、然后创建登录接口:

+
/**
+ * 登录测试 
+ */
+@RestController
+public class LoginController {
+
+    // 测试登录  ---- http://localhost:8081/doLogin?name=zhang&pwd=123456
+    @RequestMapping("doLogin")
+    public SaResult doLogin(String name, String pwd) {
+        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 
+        if("zhang".equals(name) && "123456".equals(pwd)) {
+            StpUtil.login(10001);
+            return SaResult.ok("登录成功");
+        }
+        return SaResult.error("登录失败");
+    }
+
+    // 测试注销  ---- http://localhost:8081/logout
+    @RequestMapping("logout")
+    public SaResult logout() {
+        StpUtil.logout();
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
+

5、启动类

+
@SpringBootApplication
+public class SaTokenDemoApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaTokenDemoApplication.class, args);
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

OK,前置工作完毕,现在我们就可以开始使用 Sa-Token 注解鉴权了!

+
    +
  • @SaCheckLogin: 登录认证 —— 只有登录之后才能进入该方法
  • +
  • @SaCheckRole("admin"): 角色认证 —— 必须具有指定角色标识才能进入该方法
  • +
  • @SaCheckPermission("user:add"): 权限认证 —— 必须具有指定权限才能进入该方法
  • +
  • @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法
  • +
  • @SaCheckBasic: HttpBasic认证 —— 只有通过 Basic 认证后才能进入该方法
  • +
+

登录认证:

+
/**
+ * 注解鉴权测试 
+ */
+@RestController
+@RequestMapping("/at/")
+public class AtController {
+
+    // 登录认证,登录之后才可以进入方法  ---- http://localhost:8081/at/checkLogin 
+    @SaCheckLogin
+    @RequestMapping("checkLogin")
+    public SaResult checkLogin() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

注:@SaCheckLogin注解可以加在类上,效果等同于标注在此类的所有方法上

+

权限认证:

+
@RestController
+@RequestMapping("/at/")
+public class AtController {
+
+    // 权限认证,具备user-add权限才可以进入方法  ---- http://localhost:8081/at/checkPermission 
+    @SaCheckPermission("user-add")
+    @RequestMapping("checkPermission")
+    public SaResult checkPermission() {
+        return SaResult.ok();
+    }
+
+    // 权限认证,同时具备所有权限才可以进入  ---- http://localhost:8081/at/checkPermissionAnd 
+    @SaCheckPermission({"user-add", "user-delete", "user-update"})
+    @RequestMapping("checkPermissionAnd")
+    public SaResult checkPermissionAnd() {
+        return SaResult.ok();
+    }
+
+    // 权限认证,只要具备其中一个就可以进入  ---- http://localhost:8081/at/checkPermissionOr 
+    @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR)
+    @RequestMapping("checkPermissionOr")
+    public SaResult checkPermissionOr() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
+

角色认证:

+
@RestController
+@RequestMapping("/at/")
+public class AtController {
+
+    // 角色认证,只有具备admin角色才可以进入  ---- http://localhost:8081/at/checkRole 
+    @SaCheckRole("admin")
+    @RequestMapping("checkRole")
+    public SaResult checkRole() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+

注:@SaCheckRole@SaCheckPermission 类似,可以指定 and | or 模式

+

二级认证:

+
@RestController
+@RequestMapping("/at/")
+public class AtController {
+
+    // 完成二级认证  ---- http://localhost:8081/at/openSafe 
+    @RequestMapping("openSafe")
+    public SaResult openSafe() {
+        StpUtil.openSafe(200); // 打开二级认证,有效期为200秒
+        return SaResult.ok();
+    }
+
+    // 通过二级认证后才可以进入  ---- http://localhost:8081/at/checkSafe 
+    @SaCheckSafe
+    @RequestMapping("checkSafe")
+    public SaResult checkSafe() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
+

注:必须先经过StpUtil.openSafe(200)打开二级认证,才可以通过 @SaCheckSafe 的检查。

+

HttpBasic认证:

+
@RestController
+@RequestMapping("/at/")
+public class AtController {
+
+    // 通过Basic认证后才可以进入  ---- http://localhost:8081/at/checkBasic 
+    @SaCheckBasic(account = "sa:123456")
+    @RequestMapping("checkBasic")
+    public SaResult checkBasic() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
+

当我们访问这个接口时,浏览器会强制弹出一个表单:

+

img

+

当我们输入账号密码后 (sa / 123456),才可以继续访问数据:

+

img

+

总结

+

通过以上示例我们可以看出,Sa-Token 的注解鉴权还是非常强大的,能够简单、灵活的帮我我们完成鉴权需求。

+

参考资料

+ +
+ +java中如何踢人下线?封禁某个账号后使其会话立即掉线! → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v-1-31-0-xin-zeng-lan-jie-qi-sainterceptor-gong-neng-shuo-ming-yi-ji-ji.html b/sa-token-doc/blog/release/sa-token-v-1-31-0-xin-zeng-lan-jie-qi-sainterceptor-gong-neng-shuo-ming-yi-ji-ji.html new file mode 100644 index 000000000..c214d5aab --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v-1-31-0-xin-zeng-lan-jie-qi-sainterceptor-gong-neng-shuo-ming-yi-ji-ji.html @@ -0,0 +1,374 @@ + + + + + Sa-Token v.1.31.0 新增拦截器 SaInterceptor 功能说明,以及旧代码迁移示例 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v.1.31.0 新增拦截器 SaInterceptor 功能说明,以及旧代码迁移示例

+ +

前言

+

如果你的项目没有使用旧写法,可以忽略本篇文章 移步官网,本文只针对旧版本到新版本的迁移提供示例。

+

1、旧版本

+

Sa-Token 在 1.30.0 及以下版本中使用两个拦截器:

+
    +
  • SaAnnotationInterceptor:负责提供注解鉴权能力。
  • +
  • SaRouteInterceptor:负责提供路由拦截鉴权能力。
  • +
+

这次新增的拦截器 SaInterceptor 名字为综合拦截器,见名思意就是综合上述两个拦截器的能力,既提供注解鉴权能力,也提供路由鉴权能力,目的是为了替代上述两个拦截器(v1.31.0已将上述两个类打上 @Deprecated 标记)。

+

2、新版本用法

+

在用法上 SaInterceptor 可以简单总结为两点:

+
    +
  1. 相对于 SaAnnotationInterceptor,只做增强,不做改变,可以直接无损替换。
  2. +
  3. 相对于 SaRouteInterceptor,除掉其默认的 StpUtil.checkLogin() 登录校验能力,默认不执行任何动作。
  4. +
+

下面将用几个示例详细展示 SaInterceptor 带来的变化点。

+

3、示例

+
示例1,注解鉴权
+

原写法:

+
@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    // 注册 Sa-Token 注解拦截器,打开注解鉴权功能 
+    registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

新版本写法:

+
@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    // 注册 Sa-Token 拦截器,打开注解鉴权功能 
+    registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
+

总结:只替换一下拦截器的名称就可以了。

+
示例2,路由鉴权,默认登录校验
+

原写法:

+
@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    // 注册 Sa-Token 的路由拦截器
+    registry.addInterceptor(new SaRouteInterceptor())
+        .addPathPatterns("/**")
+        .excludePathPatterns("/user/doLogin"); 
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

新版本写法:

+
@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    // 注册 Sa-Token 的路由拦截器
+    registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
+            .addPathPatterns("/**")
+            .excludePathPatterns("/user/doLogin"); 
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
+

总结:由于默认的登录校验功能不提供了,所有需要我们手动补一下。

+
示例3,路由鉴权,自定义认证规则
+

原写法:

+
@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    // 注册 Sa-Token 拦截器,定义详细认证规则 
+    registry.addInterceptor(new SaRouteInterceptor((req, res, handler) -> {
+        // 根据路由划分模块,不同模块不同鉴权 
+        SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
+        SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
+        SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
+        SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
+        SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
+        SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
+        // ...
+    })).addPathPatterns("/**");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

新版本写法:

+
@Override
+public void addInterceptors(InterceptorRegistry registry) {
+    // 注册 Sa-Token 拦截器,定义详细认证规则
+    registry.addInterceptor(new SaInterceptor(handler -> {
+        // 根据路由划分模块,不同模块不同鉴权 
+        SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
+        SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
+        SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
+        SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
+        SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
+        SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
+        // ...
+    })).addPathPatterns("/**");
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
+

总结:做一下名称 + 参数替换 就可以了。

+
+

新版本拦截器大体上就带来这三种变化,另外还新增了一个注解 @SaIgnore 代表忽略掉指定方法的鉴权校验,快来官网文档查看一下吧。:)

+
+ +Sa-Token v1.23.0 更新, 正式支持OAuth2.0授权认证 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-23-0-geng-xin-zheng-shi-zhi-chi-oauth2-0.html b/sa-token-doc/blog/release/sa-token-v1-23-0-geng-xin-zheng-shi-zhi-chi-oauth2-0.html new file mode 100644 index 000000000..60ecbf3b1 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-23-0-geng-xin-zheng-shi-zhi-chi-oauth2-0.html @@ -0,0 +1,434 @@ + + + + + Sa-Token v1.23.0 更新, 正式支持OAuth2.0授权认证 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.23.0 更新, 正式支持OAuth2.0授权认证

+ +

Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0 等一系列权限相关问题

+

框架集成简单、开箱即用、API设计清爽,通过Sa-Token,你将以一种极其简单的方式实现系统的权限认证部分

+

Sa-Token v1.23.0 版本更新主要新增OAuth模块,Sa-OAuth2 模块基于 RFC-6749 标准 编写,通过Sa-OAuth2你可以非常轻松的实现系统的OAuth2.0授权认证

+
    +
  1. 授权码(Authorization Code):OAuth2.0标准授权步骤,Server端向Client端下放Code码,Client端再用Code码换取授权Token
  2. +
  3. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server端使用URL重定向方式直接将Token下放到Client端页面
  4. +
  5. 密码式(Password):Client直接拿着用户的账号密码换取授权Token
  6. +
  7. 客户端凭证(Client Credentials):Server端针对Client级别的Token,代表应用自身的资源授权
  8. +
+

代码仓库地址:
+- Github:https://github.com/dromara/Sa-Token
+- Gitee:https://gitee.com/dromara/sa-token

+
+

搭建示例:

+

1、准备工作

+

首先修改hosts文件(C:\windows\system32\drivers\etc\hosts),添加以下IP映射,方便我们进行测试:

+
127.0.0.1 sa-oauth-server.com
+127.0.0.1 sa-oauth-client.com
  • 1
  • 2
+

2、引入依赖

+

创建SpringBoot项目 sa-token-demo-oauth2-server(不会的同学自行百度或参考仓库示例),添加pom依赖:

+
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.com/ -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>${sa.top.version}</version>
+</dependency>
+
+<!-- Sa-Token-OAuth2.0 模块 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-oauth2</artifactId>
+    <version>${sa.top.version}</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

3、开放服务

+

1、新建 SaOAuth2TemplateImpl

+
/**
+ * Sa-Token OAuth2.0 整合实现 
+ */
+@Component
+public class SaOAuth2TemplateImpl extends SaOAuth2Template {
+
+    // 根据 id 获取 Client 信息 
+    @Override
+    public SaClientModel getClientModel(String clientId) {
+        // 此为模拟数据,真实环境需要从数据库查询 
+        if("1001".equals(clientId)) {
+            return new SaClientModel()
+                    .setClientId("10001")
+                    .setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
+                    .setAllowUrl("*")
+                    .setContractScope("userinfo");
+        }
+        return null;
+    }
+
+    // 根据ClientId 和 LoginId 获取openid 
+    @Override
+    public String getOpenid(String clientId, Object loginId) {
+        // 此为模拟数据,真实环境需要从数据库查询 
+        return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
+

2、新建SaOAuth2ServerController

+
/**
+ * Sa-OAuth2 Server端 控制器 
+ */
+@RestController
+public class SaOAuth2ServerController {
+
+    // 处理所有OAuth相关请求 
+    @RequestMapping("/oauth2/*")
+    public Object request() {
+        System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
+        return SaOAuth2Handle.serverRequest();
+    }
+
+    // Sa-OAuth2 定制化配置 
+    @Autowired
+    public void setSaOAuth2Config(SaOAuth2Config cfg) {
+        cfg.
+            // 配置:未登录时返回的View 
+            setNotLoginView(() -> {
+                String msg = "当前会话在SSO-Server端尚未登录,请先访问"
+                            + "<a href='/oauth2/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
+                            + "进行登录之后,刷新页面开始授权";
+                return msg;
+            }).
+            // 配置:登录处理函数 
+            setDoLoginHandle((name, pwd) -> {
+                if("sa".equals(name) && "123456".equals(pwd)) {
+                    StpUtil.login(10001);
+                    return SaResult.ok();
+                }
+                return SaResult.error("账号名或密码错误");
+            }).
+            // 配置:确认授权时返回的View 
+            setConfirmView((clientId, scope) -> {
+                String msg = "<p>应用 " + clientId + " 请求授权:" + scope + "</p>"
+                        + "<p>请确认:<a href='/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scope + "' target='_blank'> 确认授权 </a></p>"
+                        + "<p>确认之后刷新页面</p>";
+                return msg;
+            })
+            ;
+    }
+
+    // 全局异常拦截  
+    @ExceptionHandler
+    public SaResult handlerException(Exception e) {
+        e.printStackTrace(); 
+        return SaResult.error(e.getMessage());
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
+

注意:在setDoLoginHandle函数里如果要获取name, pwd以外的参数,可通过SaHolder.getRequest().getParam("xxx")来获取

+

3、创建启动类:

+
/**
+ * 启动:Sa-OAuth2 Server端 
+ */
+@SpringBootApplication 
+public class SaOAuth2ServerApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaOAuth2ServerApplication.class, args);
+        System.out.println("\nSa-Token-OAuth Server端启动成功");
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

启动项目

+

4、访问测试

+

1、由于暂未搭建Client端,我们可以使用Sa-Token官网作为重定向URL进行测试:

+
http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-token.com/&scope=userinfo
  • 1
+

2、由于首次访问,我们在OAuth-Server端暂未登录,会被转发到登录视图

+

sa-oauth2-server-login-view

+

3、点击doLogin进行登录之后刷新页面,会提示我们确认授权
+sa-oauth2-server-login-view

+

4、点击确认授权之后刷新页面,我们会被重定向至 redirect_uri 页面,并携带了code参数

+

sa-oauth2-server-code

+

4、我们拿着code参数,访问以下地址:

+
http://sa-oauth-server.com:8001/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code}
  • 1
+

将得到 Access-TokenRefresh-Tokenopenid等授权信息

+

sa-oauth2-server-token

+

测试完毕

+

详细参考文档:http://sa-token.com/doc/index.html#/oauth2/readme

+
+← Sa-Token v.1.31.0 新增拦截器 SaInterceptor 功能说明,以及旧代码迁移示例 +Sa-Token v1.31.0 更新,新增账号分类封禁、阶梯封禁功能 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-31-0-geng-xin-xin-zeng-zhang-hao-fen-lei-feng-jin-jie-ti-feng-jin-go.html b/sa-token-doc/blog/release/sa-token-v1-31-0-geng-xin-xin-zeng-zhang-hao-fen-lei-feng-jin-jie-ti-feng-jin-go.html new file mode 100644 index 000000000..3c432ac53 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-31-0-geng-xin-xin-zeng-zhang-hao-fen-lei-feng-jin-jie-ti-feng-jin-go.html @@ -0,0 +1,330 @@ + + + + + Sa-Token v1.31.0 更新,新增账号分类封禁、阶梯封禁功能 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.31.0 更新,新增账号分类封禁、阶梯封禁功能

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、分布式 Session 会话、单点登录、OAuth2.0 等一系列权限相关问题。

+

框架针对踢人下线、自动续签、前后台分离、分布式会话…… 等常见业务进行 N 多适配,通过 Sa-Token,你可以以一种极简的方式实现系统的权限认证部分

+

Sa-Token v1.31.0 版本更新包括以下内容:

+
    +
  • 文档:新增优秀开源案例展示。
  • +
  • 文档:新增博客展示,欢迎大家投稿。
  • +
  • 新增:新增 SaInterceptor 综合拦截器。 [重要] [不向下兼容]
  • +
  • 新增:新增 新增 @SaIgnore 忽略鉴权注解。 [重要]
  • +
  • 新增:新增插件 sa-token-dao-redis-fastjson,感谢 @sikadai 提交的pr。 [重要]
  • +
  • 新增:新增插件 sa-token-context-grpc,感谢 @LiYiMing666 提交的pr。 [重要]
  • +
  • 重构:SaSession 取消 tokenSignList 的 final 修饰符。
  • +
  • 新增:SaSession 添加 setTokenSignList 方法。
  • +
  • 重构:TokenSign 新增 setValuesetDevice 方法。
  • +
  • 修复:修复多账号模式下不能正确重置 StpLogic 的问题。
  • +
  • 修复:修复 SaSession 对象中 TokenSign 判断有可能空指针的问题。
  • +
  • 修复:解决当权限码为 null 时可能带来的空指针问题。
  • +
  • 新增:新增 StpUtil.getExtra(tokenValue, key) 方法,用于获取任意 token 的扩展参数。
  • +
  • 优化:优化 StpLogic#logoutByTokenValue 方法逻辑,精简代码。
  • +
  • 重构:SaTokenConfig 配置类字段 isReadHead 改为 isReadHeader[不向下兼容]
  • +
  • 修复:修复部分场景下踢人下线会抛出异常 非Web上下文无法获取Request 的问题。
  • +
  • 新增:新增方法 StpLogic#getAnonTokenSession,可在未登录情况下安全的获取 Token-Session。 [重要]
  • +
  • 新增:新增 SaApplication 对象,用于全局作用域存取值。 [重要]
  • +
  • 重构:将 SaTokenListener 改为事件发布订阅模式,允许同时注册多个侦听器。 [重要] [不向下兼容]
  • +
  • 重构:StpUtil.login(id) 不再强制校验账号是否禁用,需要手动校验。 [不向下兼容]
  • +
  • 重构:新增对账号限制、分类封禁、阶梯封禁功能。 [重要]
  • +
  • 新增:会话查询API增加反序获取会话方式。
  • +
  • 新增:SSO模块增加 server-url 属性,用于简化各种 url 配置。 [重要]
  • +
  • 修复:修复单点登录模块 ssoLogoutCall 配置项无效的问题。
  • +
  • 优化:优化 SaSsoHandle.checkTicket(ticket, currUri); 方法,使其不提供 currUri 参数时将不再注册单点注销回调。
  • +
  • 修复:修复 SaOAuth2Handle 类中 doLogin 方法没有使用 Param.pwd 常量的问题。
  • +
  • 新增:新增 SaOAuth2Util.checkClientTokenScope(clientToken, scopes) 方法,校验 Client-Token 是否含有指定 Scope。
  • +
  • 删除:删除 sa-token-jwt 模块过期 class。
  • +
  • 重构:sa-token-jwt 模块依赖改为 hutool-jwt,并升级版本为 5.8.5。
  • +
  • 重构:sa-token-jwt 模块改为 Util + Template 形式,方便针对部分代码重写。 [重要]
  • +
  • 新增:在线文档添加API手册。
  • +
  • 重构:sa-token-oauth2 模块密码模式新增 client_secret 参数校验。[不向下兼容]
  • +
  • 新增:集成 jacoco 插件,核心包单元测试覆盖率提高至 90% 以上。
  • +
  • 优化:开源案例分离专属仓库:Awesome-Sa-Token
  • +
+

代码仓库:https://gitee.com/dromara/sa-token

+

框架功能结构图

+

js

+
+← Sa-Token v1.23.0 更新, 正式支持OAuth2.0授权认证 +Sa-Token v1.32.0 更新,新增账号分类封禁、阶梯封禁功能 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-32-0-geng-xin-xin-zeng-zhang-hao-fen-lei-feng-jin-jie-ti-feng-jin-go.html b/sa-token-doc/blog/release/sa-token-v1-32-0-geng-xin-xin-zeng-zhang-hao-fen-lei-feng-jin-jie-ti-feng-jin-go.html new file mode 100644 index 000000000..e39ae4bb1 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-32-0-geng-xin-xin-zeng-zhang-hao-fen-lei-feng-jin-jie-ti-feng-jin-go.html @@ -0,0 +1,327 @@ + + + + + Sa-Token v1.32.0 更新,新增账号分类封禁、阶梯封禁功能 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.32.0 更新,新增账号分类封禁、阶梯封禁功能

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、分布式 Session 会话、单点登录、OAuth2.0 等一系列权限相关问题。

+

框架针对踢人下线、自动续签、前后台分离、分布式会话…… 等常见业务进行 N 多适配,通过 Sa-Token,你可以以一种极简的方式实现系统的权限认证部分

+

Sa-Token v1.32.0 版本更新包括以下内容:

+
    +
  • 修复:修复 sa-token-dao-redis-fastjson 插件多余序列化 timeout 字段的问题。
  • +
  • 修复:修复 sa-token-dao-redis-fastjson 插件 session.getModel 无法反序列化实体类的问题。
  • +
  • 修复:修复 sa-token-quick-login 插件指定拦截排除路由不生效的问题。
  • +
  • 修复:修复 sa-token-alone-redis + sa-token-dao-redis-fastson 时 Redis 无法分离的问题。
  • +
  • 修复:修复在配置了 cookie.path 后,注销时无法彻底清除 Cookie 的问题。
  • +
  • 升级:SaFoxUtil.getValueByType() 新增对 char 类型的转换。
  • +
  • 新增:新增 sa-token-dao-redis-fastjson2 插件。 [重要]
  • +
  • 新增:新增全局配置 is-write-header,控制登录后是否将 Token 写入响应头。 [重要]
  • +
  • 新增:二级认证模块新增指定业务标识能力。 [重要]
  • +
  • 重构:Id-Token 模块更名为 Same-Token。 [重要] [不向下兼容]
  • +
  • 重构:重构会话查询参数作用:由start=-1时查询全部会话,改为 start=0,size=-1 时查询全部。 [不向下兼容]
  • +
  • 重构:SaManager.getStpLogic("type") 默认当对应type不存在时不再抛出异常,而是自动创建并返回。
  • +
  • 重构:重构SSO模块,静态式API改为实例式:SaSsoHandle -> SaSsoProcessor。 [重要] [不向下兼容]
  • +
  • 重构:SSO-Server 端单点注销地址修改 /sso/logout -> /sso/signout,避免与 SSO-Client 端同 path 的冲突。 [不向下兼容]
  • +
  • 新增:文档新增 SSO 平台中心模式示例,跳连接进入子系统。 [重要]
  • +
  • 新增:新增SSO前后端分离集成示例 vue2 & vue3 版本。 [重要]
  • +
  • 重构:SSO 示例项目 http 请求工具改为 Forest。
  • +
  • 新增:SSO模块文档新增单个项目同时搭建 sso-serversso-client 的示例。 [重要]
  • +
  • 新增:SSO模块文档新增一个项目同时搭建两个 sso-server 服务 的示例。 [重要]
  • +
  • 文档:在线文档新增代码示例。
  • +
  • 文档:在线文档增加全局调色功能。
  • +
  • 文档:[自定义 SaTokenContext 指南] 章节新增对三种模型的解释。
  • +
  • 文档:新增多账号体系混合鉴权代码示例。
  • +
  • 文档:文档增加 Gradle 依赖方式和 properties 风格配置。
  • +
  • 新增:新增 sa-token-dependencies,统一定义依赖版本。 [重要]
  • +
+ +


+ cn.dev33
+ sa-token-spring-boot-starter
+ 1.32.0
+

+

代码仓库:https://gitee.com/dromara/sa-token

+

框架功能结构图

+

js

+
+← Sa-Token v1.31.0 更新,新增账号分类封禁、阶梯封禁功能 +Sa-Token v1.33.0 更新,新增全局 Log 模块 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-33-0-geng-xin-xin-zeng-quan-ju-log-mo-kuai.html b/sa-token-doc/blog/release/sa-token-v1-33-0-geng-xin-xin-zeng-quan-ju-log-mo-kuai.html new file mode 100644 index 000000000..a9be928fa --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-33-0-geng-xin-xin-zeng-quan-ju-log-mo-kuai.html @@ -0,0 +1,309 @@ + + + + + Sa-Token v1.33.0 更新,新增全局 Log 模块 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.33.0 更新,新增全局 Log 模块

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、分布式 Session 会话、单点登录、OAuth2.0 等一系列权限相关问题。

+

框架针对踢人下线、自动续签、前后台分离、分布式会话…… 等常见业务进行 N 多适配,通过 Sa-Token,你可以以一种极简的方式实现系统的权限认证部分

+

Sa-Token v1.33.0 版本更新包括以下内容:

+
    +
  • 重构:重构异常状态码机制。 [重要]
  • +
  • 重构:重构 sa-token-sso 模块异常码改为 300 开头,sa-token-jwt 异常码改为 302 开头。 [不向下兼容]
  • +
  • 新增:新增全局 Log 模块。 [重要]
  • +
  • 重构:SaTokenListenerForConsolePrint 改名 SaTokenListenerForLog[不向下兼容]
  • +
  • 修复:修复多线程下 SaFoxUtil.getRandomString() 随机数重复问题。
  • +
  • 修复:修复 sa-token-demo-sso3-client-nosdk 项目中单点注销 url 配置错误的问题
  • +
  • 文档:文档优化。
  • +
+ +


+ cn.dev33
+ sa-token-spring-boot-starter
+ 1.33.0
+

+

代码仓库:https://gitee.com/dromara/sa-token

+

框架功能结构图

+

js

+
+← Sa-Token v1.32.0 更新,新增账号分类封禁、阶梯封禁功能 +Sa-Token v1.34.0 更新,适配 SpringBoot3 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-34-0-geng-xin-shi-pei-springboot3.html b/sa-token-doc/blog/release/sa-token-v1-34-0-geng-xin-shi-pei-springboot3.html new file mode 100644 index 000000000..f26c42896 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-34-0-geng-xin-shi-pei-springboot3.html @@ -0,0 +1,316 @@ + + + + + Sa-Token v1.34.0 更新,适配 SpringBoot3 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.34.0 更新,适配 SpringBoot3

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、分布式 Session 会话、单点登录、OAuth2.0 等一系列权限相关问题。

+

框架针对踢人下线、自动续签、前后台分离、分布式会话…… 等常见业务进行 N 多适配,通过 Sa-Token,你可以以一种极简的方式实现系统的权限认证部分

+

Sa-Token v1.34.0 版本更新包括以下内容:

+
    +
  • 新增:新增 SpringBoot3.x 集成插件,感谢 @jry 提供的参考思路。 [重要]
  • +
  • 新增:新增 sa-token-dao-redisson-jackson 插件,感谢 @疯狂的狮子Li 提交的pr。 [重要]
  • +
  • 新增:Alone-Redis 新增集群配置能力,感谢 @appleOfGray 提交的pr。 [重要]
  • +
  • 升级:升级 Sign 签名模块,增加部分重载方法。
  • +
  • 重构:SaSignTemplate#joinParams 更名为 joinParamsDictSort[不向下兼容]
  • +
  • 升级:升级临时 Token 认证模块,可指定 service 参数。
  • +
  • 删除:彻底删除过期类 SaAnnotationInterceptorSaRouteInterceptor
  • +
  • 删除:SSO 模块移除过期类 SaSsoHandle 类。
  • +
  • 新增:SSO 模块增加 ticket 的 client 锁定功能,解决部分场景下的 ticket 劫持问题。 [重要]
  • +
  • 修复:修复 SSO 模式2,在 client 端配置 is-share=false 时无法单点注销的问题。
  • +
  • 修复:修复 SSO 模式3 部分场景下注销时无法正常回退页面的问题。
  • +
  • 修复:修复 OAuth2 模块示例 getClientModel 方法 clientId 写错的问题。
  • +
  • 重构:使用 jwt-simple 模式后 is-share 恒等于 false,无论是否有设定 setExtra 数据。
  • +
  • 修复:修复源码注释和文档的部分不合适之处。
  • +
+ +


+ cn.dev33
+ sa-token-spring-boot-starter
+ 1.34.0
+

+

代码仓库:https://gitee.com/dromara/sa-token

+

框架功能结构图

+

js

+
+← Sa-Token v1.33.0 更新,新增全局 Log 模块 +Sa-Token v1.35.0 更新,新增动态 active-timeout 能力 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-35-0-geng-xin-xin-zeng-dong-tai-active-timeout-neng-li.html b/sa-token-doc/blog/release/sa-token-v1-35-0-geng-xin-xin-zeng-dong-tai-active-timeout-neng-li.html new file mode 100644 index 000000000..2cfacf9b5 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-35-0-geng-xin-xin-zeng-dong-tai-active-timeout-neng-li.html @@ -0,0 +1,389 @@ + + + + + Sa-Token v1.35.0 更新,新增动态 active-timeout 能力 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.35.0 更新,新增动态 active-timeout 能力

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、分布式 Session 会话、单点登录、OAuth2.0 等一系列权限相关问题。

+

框架针对踢人下线、自动续签、前后台分离、分布式会话…… 等常见业务进行 N 多适配,通过 Sa-Token,你可以以一种极简的方式实现系统的权限认证部分

+

Sa-Token v1.35.0.RC 版本更新包括以下内容:

+
    +
  • +

    sa-token-core:

    +
      +
    • 优化:前端未提供 token 时,getTokenSession() 将抛出未登录异常,而不是返回 null。 [不向下兼容]
    • +
    • 新增:SaSession 新增字段:typeloginTypeloginIdtoken
    • +
    • 重构:全局过滤器抽离 SaFilter 统一接口。
    • +
    • 重构:全局过滤器 includeListexcludeList 改为 public,同时移除对应的 getter 方法。 [不向下兼容]
    • +
    • 重构:将全局过滤器的 BeforeAuth 认证设为不受 includeListexcludeList 的限制,所有请求都会进入。 [不向下兼容]
    • +
    • 新增:新增循环生成 token 的算法,用于确保 Token 的唯一性。 [重要]
    • +
    • 重构:API 接口签名所有方法均迁移至 core 核心模块。 [重要]
    • +
    • 新增:新增彩色日志打印,更方便的分辨不同日志等级。 [重要]
    • +
    • 重构:重构概念:临时有效期 -> token 最低活跃频率,过期后 token 冻结。
    • +
    • 重构:重构概念:User-Session -> Account-Session
    • +
    • 新增:新增 getTokenTimeout(String token) 方法,获取任意 token 剩余有效期。
    • +
    • 优化:在登录时增加判断当前 StpLogic 是否支持 extra 扩展参数模式,如果不支持则打印警告信息。
    • +
    • 新增:NotLoginException 增加新场景值 -6、-7,提供更精确的未登录异常描述信息。
    • +
    • 新增:TokenSign 新增 tag 挂载参数,可在登录时方便的存储一些客户端特有数据。 [重要]
    • +
    • 新增:新增 SaStrategy#createStpLogic,用于指定动态创建 StpLogic 时的算法策略。
    • +
    • 新增:新增 @SaCheckOr 批量注解鉴权:只要满足其中一个注解即可通过验证。 [重要]
    • +
    • 重构:重命名:SaStrategy.me -> SaStrategy.instance
    • +
    • 重构:在登录时强制性检查账号 id 是否为异常值,如果是则登录失败。
    • +
    • 重构:重构概念:activity-timeout -> active-timeout[重要]
    • +
    • 新增:新增动态 active-timeout 能力,可在每次登录时指定 active-timeout 值。 [重要]
    • +
    • 优化:将 SaStrategy 所有策略声明抽离为单独的函数式接口。
    • +
    • 新增:增加为 StpLogic 单独配置 SaTokenConfig 参数的能力。
    • +
    +
  • +
  • +

    sa-token-sso:

    +
      +
    • 修复:在 SSO 模式三中 ticket 校验地址配错时,会出现 NPE 的问题
    • +
    • 新增:新增 getData 接口配置,在模式三拉取数据时可以传递任意参数。 [重要]
    • +
    • 重构:模式三秘钥配置更改:sa-token.sso.secretkey=xxx -> sa-token.sign.secret-key=xxx[不向下兼容]
    • +
    • 重构:模式三校验签名方法更改:SaSsoUtil.checkSign(req) -> SaSignUtil.checkRequest(req)[不向下兼容]
    • +
    • 新增:新增 sa-token.sso.mode 配置项,用于约定此系统使用的 SSO 模式。
    • +
    • 优化:优化校验 ticket 的逻辑。
    • +
    +
  • +
  • +

    sa-token-jwt:

    +
      +
    • 修复:jwt 令牌的签名类型可以被篡改的问题。 [重要]
    • +
    +
  • +
  • +

    其它:

    +
      +
    • 优化:所有模块优化注释,更方便开发者阅读源码。
    • +
    • 优化:在所有 .java 文件中添加 license 头说明。
    • +
    • 优化:修复大部分代码警告。
    • +
    • 新增:新增 thymeleaf 标签方言命名空间,增强 ide 代码提示。 [重要]
    • +
    • 新增:定义 sa-token-bom 包,方便引入 sa-token 时对齐版本。
    • +
    • 新增:sa-token-dubbo3 插件新增代码示例。
    • +
    • 新增:新增跨域文章和示例:Header 参数版和第三方 Cookie 版。 [重要]
    • +
    • 修复:修复 sa-token-alone-redis 在低版本 springboot 下无法启动成功(缺少 username 属性)的问题。
    • +
    +
  • +
  • +

    新增插件:

    +
      +
    • 新增:新增 sa-token-context-dubbo3 插件。 感谢 @qiudaozhang 提交的 pr。 [重要]
    • +
    +
  • +
  • +

    文档:

    +
      +
    • 新增:部分常见报错排查。
    • +
    • 新增:首页图片增加懒加载效果,节省流量。
    • +
    • 新增:增加 Cookie 配置示例。
    • +
    • 修复:整理 demo 结构目录结构,修复不正确的路径说明。
    • +
    • 新增:新增 api-sign 模块文档。 [重要]
    • +
    +
  • +
  • +

    简化包名 [重要] [不向下兼容]

    +
      +
    • sa-token-dao-redis -> sa-token-redis
    • +
    • sa-token-dao-redis-jackson -> sa-token-redis-jackson
    • +
    • sa-token-dao-redis-fastjson -> sa-token-redis-fastjson
    • +
    • sa-token-dao-redis-fastjson2 -> sa-token-redis-fastjson2
    • +
    • sa-token-dao-redisson-jackson -> sa-token-redis-jackson
    • +
    • sa-token-dao-redisx -> sa-token-redisx
    • +
    • sa-token-context-dubbo -> sa-token-dubbo
    • +
    • sa-token-context-dubbo3 -> sa-token-dubbo3
    • +
    • sa-token-context-grpc -> sa-token-grpc
    • +
    +
  • +
+ +


+ cn.dev33
+ sa-token-spring-boot-starter
+ 1.35.0.RC
+

+

代码仓库:https://gitee.com/dromara/sa-token

+

框架功能结构图

+

js

+
+← Sa-Token v1.34.0 更新,适配 SpringBoot3 +Sa-Token v1.39.0 更新,大幅重构 OAuth2 模块 → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-39-0-geng-xin-da-fu-zhong-gou-oauth2-mo-kuai.html b/sa-token-doc/blog/release/sa-token-v1-39-0-geng-xin-da-fu-zhong-gou-oauth2-mo-kuai.html new file mode 100644 index 000000000..1f16d4116 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-39-0-geng-xin-da-fu-zhong-gou-oauth2-mo-kuai.html @@ -0,0 +1,354 @@ + + + + + Sa-Token v1.39.0 更新,大幅重构 OAuth2 模块 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.39.0 更新,大幅重构 OAuth2 模块

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、分布式 Session 会话、单点登录、OAuth2.0 等一系列权限相关问题。

+

框架针对踢人下线、自动续签、前后台分离、分布式会话…… 等常见业务进行 N 多适配,通过 Sa-Token,你可以以一种极简的方式实现系统的权限认证部分

+

Sa-Token v1.39.0 版本更新包括以下内容:

+
    +
  • 核心:
      +
    • 升级:重构注解鉴权底层,支持自定义鉴权注解了。 [重要]
    • +
    • 修复:修复前端提交同名 Cookie 时的框架错读现象。
    • +
    +
  • +
  • 插件:
      +
    • 修复:修复 sa-token-quick-login 插件无法正常拦截的问题。
    • +
    +
  • +
  • SSO:
      +
    • 优化:优化 sso-server 前后端分离 demo 代码。
    • +
    • 优化:优化 sso-server 前后端分离时的跳转流程。
    • +
    +
  • +
  • OAuth2:
      +
    • 重构:sa-token-oauth2 模块整体重构。 [重要] [不向下兼容]
    • +
    • 新增:新增支持自定义 scope 处理器。 [重要]
    • +
    • 新增:新增支持自定义 grant_type[重要]
    • +
    • 新增:新增 scope 划分等级。 [重要]
    • +
    • 新增:新增 oidc 协议支持。 [重要]
    • +
    • 新增:新增支持默认 openid 生成算法。 [重要]
    • +
    • 新增:新增 OAuth2 注解鉴权支持。 [重要]
    • +
    • 修复:redirect_url 参数校验增加规则:不允许出现@字符、通配符只能出现在最后一位。关联 issue [重要]*
    • +
    • 优化:优化代码注释、异常提示信息。
    • +
    • 升级:兼容 Http Basic 提交 client 信息的场景。感谢 github @CuiGeekYoung 提交的pr。
    • +
    • 升级:兼容 Bearer Token 方式提交 access_tokenclient_token
    • +
    • 升级:适配拆分式路由。
    • +
    • 新增:将 scope 字段改为 List 类型。
    • +
    • 重构:抽离 SaOAuth2Strategy 全局策略接口,定义一些创建 token 的算法策略。
    • +
    • 新增:新增 addAllowUrls addContractScopes 方法,简化 SaClientModel 构建代码。
    • +
    • 重构:抽离 SaOAuth2Dao 接口,负责数据持久。
    • +
    • 重构:抽离 SaOAuth2DataLoader 数据加载器接口。
    • +
    • 重构:抽离 SaOAuth2DataGenerate 数据构造器接口。
    • +
    • 重构:抽离 SaOAuth2DataConverter 数据转换器接口。
    • +
    • 重构:抽离 SaOAuth2DataResolver 数据解析器接口。
    • +
    • 重构:重构 SaOAuth2Handle -> SaOAuth2ServerProcessor 更方便的逻辑重写。
    • +
    • 重构:重构 PastToken -> LowerClientToken
    • +
    • 新增:新增 state 值校验,同一 state 参数不可重复使用。
    • +
    • 优化:完善 SaOAuth2Util 相关方法,更方便的二次开发。
    • +
    • 新增:新增部分异常类,细分异常 ClassType
    • +
    • 优化:优化 sa-token-oauth2 异常细分状态码。
    • +
    +
  • +
  • 文档:
      +
    • 新增:新增数据结构说明。
    • +
    • 新增:新增不同 client 不同登录页说明。
    • +
    • 优化:优化文档 [将权限数据放在缓存里] 示例。
    • +
    • 新增:新增 从 Shiro、SpringSecurity、JWT 迁移 示例。 [重要]
    • +
    +
  • +
+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.39.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

代码仓库:https://gitee.com/dromara/sa-token

+

框架功能结构图

+

js

+

近期发现的录制的与 Sa-Token 相关的视频:

+

【微服务架构设计与实战】 https://www.bilibili.com/video/BV1nJWWeDEFN

+
+← Sa-Token v1.35.0 更新,新增动态 active-timeout 能力 +Sa-Token v1.40.0 发布 🚀,来看看有没有令你心动的功能! → +
+
+
+
+
+ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-40-0-fa-bu-lai-kan-kan-you-mei-you-ling-ni-xin-dong-de-gong-neng.html b/sa-token-doc/blog/release/sa-token-v1-40-0-fa-bu-lai-kan-kan-you-mei-you-ling-ni-xin-dong-de-gong-neng.html new file mode 100644 index 000000000..a171f58b3 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-40-0-fa-bu-lai-kan-kan-you-mei-you-ling-ni-xin-dong-de-gong-neng.html @@ -0,0 +1,575 @@ + + + + + Sa-Token v1.40.0 发布 🚀,来看看有没有令你心动的功能! - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.40.0 发布 🚀,来看看有没有令你心动的功能!

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。🔐

+

目前最新版本 v1.40.0 已发布至 Maven 中央仓库 🎉,大家可以通过如下方式引入:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.40.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

该版本包含大量 ⛏️️️新增特性、⛏️️️️漏洞修复、⛏️️️代码优化 等,下面容我列举几条比较重要的更新内容供大家参阅:

+ +

这是来自名为 “@massinger” 的用户创建的 GitHub issue #693
+该开发者提出“是否可以为 Cookie 添加 Partitioned 属性,用于支持 iframe 嵌入跨域” 💡。

+

起初我以为 http 响应头 Cookie 参数只有 domainpathsecurehttpOnlysameSite 等几个扩展属性😶,
+顺着这个 issue,我查阅了一下相关资料,这不查不知道,一查吓一跳 😱。

+

原来 Cookie 还有这么多我不知道的扩展属性🔍:

+

Cookie扩展属性

+

(参考文章:https://blog.csdn.net/fengbin2005/article/details/136544226)

+

本着“可扩展性”的原则,本次版本更新没有补充这些属性的配置定义,而是直接增加了一个 extraAttrs 字段,你可以借助该字段扩展任意自定义属性✨。

+

举个例子,如下代码配置:

+
sa-token: 
+    cookie:
+        domain: ""
+        path: /
+        secure: false
+        httpOnly: true
+        sameSite: Lax
+        extraAttrs:
+            Priority: Medium
+            Partitioned: ""
+            abc: def
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

将会促使 Sa-Token 框架生成以下格式的 Cookie 响应头🍪️:

+
Set-Cookie: satoken=4d1a2052-29e0-4302-9ecd-ac54dece5fde; Max-Age=2592000; Expires=Tue, 4 Mar 2025 00:49:03 +0800; Path=/; HttpOnly; SameSite=Lax; Priority=Medium; Partitioned; abc=def
  • 1
+

🛡️ 更新点2:新增 SaFirewallStrategy 防火墙策略

+

危险 path 路径符校验是在最早 v1.37.0 就支持的功能,如果前端提交的请求 path 包含 “//、\、%2e、%2f、%5c、%25” 等特殊字符时,Sa-Token 全局过滤器将会截断请求,
+返回类似 “非法请求:/test/login//” 的响应提示⚠️。

+

因为包含这些特殊字符的请求通常被用于路径遍历、请求绕行等网络攻击行为💻⚔️。

+

本次更新重构了该功能的架构设计,抽离了一个统一的 SaFirewallStrategy “防火墙”组件🧩,目前包含以下能力:
+- 请求 path 黑名单拦截。
+- 非法字符校验。
+- 白名单放行。

+

该组件在后续版本中还将计划增加ip校验、请求头校验等能力,并尝试引入 hooks 机制,以方便开发者更自由的进行功能扩展 🚀。

+

🔍 更新点3:新增对分号字符的 path 路径校验,以预防潜在的网络攻击

+

此功能提案来源于 “船山信安” 公众号的一篇漏洞报告📄:Sa-Token对url过滤不全存在的风险点 🔗

+

该文章指出了 Sa-Token 对包含 “;” 字符过滤不全存在的风险点⚠️:由于 Sa-Token 与底层 WebMVC 框架对 path 路径匹配的方式存在部分差异,
+当前端提交的请求 path 中包含 “;” 字符,将可能导致请求绕过过滤器的权限检测,执行对应的 Controller 方法,从而导致越权问题💥。

+

目前在最新 v1.40.0 版本中已修复此问题,框架将从最顶层过滤器直接截断 path 中包含“;”字符的请求,避免鉴权被绕过的问题🔒。

+

感谢“船山信安”团队做出的贡献🙏🌟。

+

🚀 更新点4:新增 OAuth2 Client 前端测试页,助你快速对接调试 OAuth2-Server

+

对于刚刚接触 OAuth2 概念的初学者来说,不仅 OAuth2 Server 端的搭建比较困难,其接入与测试也是一件极其繁琐的事情😫。

+

OAuth2 对接涉及大量繁琐的参数拼接、响应报文解析等步骤,仅一个授权码式的对接,其调试过程可能也要可能耗费数小时乃至数天⏳。

+

为此,我们提供了一个专门对接 OAuth2 Server 的前端测试页🖥️:

+

OAuth2 前端测试页

+

该测试页允许你:
+- 1️⃣ 在前端直接配置 OAuth2 Server 的主机地址、授权页地址、token接口地址、Scope参数 等信息。
+- 2️⃣ 一键拼接授权URL地址,一键测试对应平台 授权码式、隐藏式、密码式和客户端凭证式 的完整授权流程。
+- 3️⃣ 离线化自由部署,所有代码均在前端实现,敏感数据全程不经过第三方服务器,完整开源可自由二开。

+

通过该测试页面,你将非常直观的 get 到 OAuth2 四种模式的对接步骤、响应报文等信息,帮助你快速定位和解决 OAuth2 对接流程中的大部分问题 💡✨。

+

详情可参考该视频:Sa-Token v1.40 新功能预览:OAuth2 四种模式 一键测试页 🔗

+

🤝 更新点5:新增 UnionId 联合 ID 实现,方便跨应用识别用户

+

此提案来自于一位开发者对 官网 sa-token 小助手的咨询 💬:

+

有关 UnionId 功能的 sa-token 小助手咨询

+

这位开发者在使用 Sa-Token 的 OAuth2 模块时,需要一个叫做 UnionId 的功能。简单来讲:UnionId 可以帮助开发者在同主体的多个应用下定位到同一用户 🔍。

+

作为立志要成为国内第一 java 权限认证框架的 Sa-Token 来讲,这是必须给安排上的💪🏆。

+

详情可参考该视频️:OAuth2 授权流程中的 clientId、openId、unionId、userId 都是干嘛的?Sa-Token v1.40 预览:新增unionid支持 🔗

+

🌐 更新点6:新增 OAuth2-Server 端前后台分离示例与文档

+

有 Web Page 服务的地方就有前后台分离,当然 OAuth2 也不例外🌍。

+

之前的版本 本质也是支持搭建 OAuth2-Server 端前后台分离服务的,但是由于官网仓库缺乏 Demo示例和相关介绍文档,导致 “OAuth2-Server 端能否前后台分离?” 成为点击量特别高的一个问题🔥。

+

OAuth2-Server 端能否前后台分离?微信咨询

+

参考 issue:
+- https://gitee.com/dromara/sa-token/issues/I9DQGA
+- https://gitee.com/dromara/sa-token/issues/I9W2RU

+

本次 v1.40.0 版本针对 OAuth2-Server 端的前后台分离搭建补充了详细的 demo示例📦 与 说明文档📚。

+

在线官网直达地址:OAuth2-Server 端前后台分离 🔗

+

🔌 更新点7:新增 sa-token-freemarker 插件,整合 Freemarker 视图引擎。

+

来源 issue:#651 💡

+

虽然前后台分离已成为当今的主流架构模式 🌊,但仍有一部分系统依旧采用前后台一体的方式进行搭建开发🏗️。

+

在这部分应用中,80%的系统会采用 SpringBoot 官方推荐的 Thymeleaf 作为视图引擎组件📊。只有很少一部分才会采用 Freemarkerjspbeetl 等技术🛠️。

+

但是!少~(三声转折),我们也要支持!💪✨

+

本次新增的 sa-token-freemarker 插件,用于整合 Freemarker 视图引擎。该插件使用方式非常简单,你只需要在 pom.xml 中引入如下依赖👇:

+
<!-- 在 Freemarker 页面中使用 Sa-Token 自定义标签 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-freemarker</artifactId>
+    <version>1.40.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

然后便可以愉快的在 Freemarker Page 中🎉,使用 Sa-Token 自定义标签控制某些内容是否显示,把权限精确到按钮级🎛️:

+
<!-- 有user-add权限才显示按钮 -->  
+<@sa.hasPermission value="user-add">
+  <button class="btn-danger">点击发射核弹</button>
+</@sa.hasPermission>
  • 1
  • 2
  • 3
  • 4
+

完整示例代码参考如下📑:

+
<div class="view-box" style="padding: 30px;">
+    <h2>Sa-Token 集成 Freemarker 标签方言 —— 测试页面</h2>
+    <p>当前是否登录:<#if stp.isLogin()>是<#else>否</#if></p>
+    <p>
+        <a href="login" target="_blank">登录</a>
+        <a href="logout" target="_blank">注销</a>
+    </p>
+
+    <p>登录之后才能显示:<@sa.login>value</@sa.login></p>
+    <p>不登录才能显示:<@sa.notLogin>value</@sa.notLogin></p>
+
+    <p>具有角色 admin 才能显示:<@sa.hasRole value="admin">value</@sa.hasRole></p>
+    <p>同时具备多个角色才能显示:<@sa.hasRoleAnd value="admin, ceo, cto">value</@sa.hasRoleAnd></p>
+    <p>只要具有其中一个角色就能显示:<@sa.hasRoleOr value="admin, ceo, cto">value</@sa.hasRoleOr></p>
+    <p>不具有角色 admin 才能显示:<@sa.notRole value="admin">value</@sa.notRole></p>
+
+    <p>具有权限 user-add 才能显示:<@sa.hasPermission value="user-add">value</@sa.hasPermission></p>
+    <p>同时具备多个权限才能显示:<@sa.hasPermissionAnd value="user-add, user-delete, user-get">value</@sa.hasPermissionAnd></p>
+    <p>只要具有其中一个权限就能显示:<@sa.hasPermissionOr value="user-add, user-delete, user-get">value</@sa.hasPermissionOr></p>
+    <p>不具有权限 user-add 才能显示:<@sa.notPermission value="user-add">value</@sa.notPermission></p>
+
+    <p>
+        从SaSession中取值:
+        <#if stp.isLogin()>
+            <span>${stp.getSession().get('name')}</span>
+        </#if>
+    </p>
+
+</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
+

在线文档直达链接:Sa-Token 整合 Freemarker 📚

+

💡 更新点8:新增 sa-token-spring-el 插件,用于支持 SpEL 表达式注解鉴权。

+

fix issue:
+- #IB3GBB
+- #IAIXSL
+- #I9P24F

+

“注解鉴权动态传递参数,支持‌SpEL表达式” 是呼声很高的一个提案📢,之前由于我本人对 SpringEL 表达式的了解非常有限,一直没有实现此功能。

+

在开发前,我构思了好几种实现方案🤔,比如对 @SaCheckPermission("xxx") 注解支持动态表达式,或者手写一个简单的表达式解析引擎等。

+

最终权衡功能性、扩展性以及代码性能等 多个维度考虑⚖️,实现方案为新增一个独立的插件 sa-token-spring-el,该插件借助 SpringAOP 能力,提供一个 @SaCheckEL 鉴权注解,
+用于书写 SpringEL 表达式进行鉴权✍️。

+

简单示例:

+
@RestController
+@RequestMapping("/check-el/")
+public class SaCheckELController {
+
+    // 登录校验 
+    @SaCheckEL("stp.checkLogin()")
+    @RequestMapping("test1")
+    public SaResult test1() {
+        return SaResult.ok();
+    }
+
+    // 权限校验 
+    @SaCheckEL("stp.checkPermission('user:edit')")
+    @RequestMapping("test3")
+    public SaResult test3() {
+        return SaResult.ok();
+    }
+
+    // 参数长度校验 
+    @SaCheckEL("NEED( #name.length() > 3 )")
+    @RequestMapping("test5")
+    public SaResult test5(@RequestParam(defaultValue = "") String name) {
+        return SaResult.ok().set("name", name);
+    }
+
+    // SaSession 里取值校验 
+    @SaCheckEL("NEED( stp.getSession().get('name') == 'zhangsan' )")
+    @RequestMapping("test8")
+    public SaResult test8() {
+        return SaResult.ok();
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
+

在线文档直达链接:SpEL 表达式注解鉴权 📚

+

值得一提的是:在本次更新中,@ly-chn 同学给了我很大的技术支持🙏。是他提供的 demo 案例才让我顺利完成了 sa-token-spring-el 插件开发🚀。

+

同时他也是 idea 插件 SpEL Assistant 的开发作者💡,该插件允许为自定义注解书写 SpEL 表达式时增加代码提示功能,
+开源地址:https://github.com/ly-chn/SpEL-Assistant🔗

+

🍃 更新点9:新增 MongoDB 集成示例,权限数据可以缓存到 MongoDB 中了!

+

merge pr:
+- pr322
+- pr667

+

集成 MongoDB 是我一直想做而没有做的一个功能,一来是因为我对 MongoDB 了解有限📉,二来我感觉 MongoDB 的数据存储格式也不如 Redis 更加合适🔍。

+

有太多更加紧迫的提案需要我去实现,所以整合 MongoDB 一直处于搁置状态⏳。

+

感谢来自 @lilihao 提供的 demo 示例🙏,也算是补充了 Sa-Token 在这块的缺失✅。

+

在线文档直达链接:
+- Sa-Token 集成 MongoDB 参考一 📘。
+- Sa-Token 集成 MongoDB 参考二 📙。

+

💾 更新点10:封禁模块新增支持实时从数据库查询数据

+

Sa-Token 默认将封禁信息储存在缓存中,缓存中的数据是 “临时性的⏳”、“易丢失的💨”,而在大多数系统的设计中,需要将封禁数据持久化到数据库中💾。

+

要使封禁信息持久化,你只需要在调用 Sa-Token 的封禁 API 后,再继续调用插入数据库的代码即可

+
// 在 Sa-Token 框架中封禁指定账号
+StpUtil.disable(10001, 86400); 
+
+// 更改数据库中此人信息 (举例代码)
+userMapper.disableUser(10001);
  • 1
  • 2
  • 3
  • 4
  • 5
+

这样即可保证封禁数据同步插入到缓存和数据库中,但是还有一个问题,如果我们的程序或缓存中间件重启了,导致缓存数据丢失⚠️, 那再调用 StpUtil.checkDisable(10001) 代码将没有效果,无法约束到此用户🚫。

+

比较次的解决方案是在程序启动时,读取数据库中所有封禁信息同步到缓存中去,但是如果封禁记录较多这样将会严重拖慢程序启动时间⏱️💣。

+

v1.40.0 版本提供一种新的方案🌟,你只需要实现 StpInterface 接口的 isDisabled 方法,即可让 Sa-Token 实时查询某个账号是否被封禁了指定业务。

+
@Component
+public class StpInterfaceImpl implements StpInterface {
+
+    /**
+     * 返回指定账号 id 是否被封禁
+     *
+     * @param loginId  账号id
+     * @param service 业务标识符
+     * @return 描述该账号是否封禁的包装信息对象
+     */
+    public SaDisableWrapperInfo isDisabled(Object loginId, String service) {
+        // 查库操作 ...  (此处仅做示例代码)
+        return SaDisableWrapperInfo.createDisabled(86400, 1);
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
+

该方法返回值具有多种写法,分别代表不同的封禁策略🔧,详细可参考在线文档:
+Sa-Token 账号封禁 📚

+

📚 更新点11:文档新增内容

+
    +
  • 文档首页新增:哔哩哔哩、抖音、微信视频号链接,欢迎大家关注❤️。
  • +
  • 导航栏视频新增👉 [fox说技术] 录制的 Sa-Token 课程链接 🎓。
  • +
  • “API接口参数签名”章节 新增视频讲解链接📹(该课程由B站up主 [抓蛙师] 讲解录制👨🏫)。
  • +
  • 文档首页首屏增加 “需求提交” 按钮,欢迎各位同学对 Sa-Token 后续版本提出你的需求与建议💡。
  • +
  • 文档首页以及 Readme 新增 gitcode G-Star badge 展示🏅🌟。
  • +
  • 文档新增新增团队成员展示👥。
  • +
  • 补全赞助者名单:v1.40 版本开发期间,共有20+用户进行打赏💰,总金额超过500元+,感谢这些用户的充电支持🙏。
  • +
+

📜 完整更新日志

+

除了以上提到的几点以外,还有更多更新点无法逐一详细介绍,下面是 v1.40.0 版本的完整更新日志:

+
    +
  • core:
      +
    • 新增:新增 Cookie 自定义属性支持。 fix: #693 [重要]
    • +
    • 新增:SaFirewallStrategy 防火墙策略:请求 path 黑名单校验、非法字符校验、白名单放行。 [重要]
    • +
    • 修复:新增对分号字符的 path 路径校验。 参考:Sa-Token对url过滤不全存在的风险点 [漏洞修复]
    • +
    • 修复: 修复部分场景下登录后已存在的 token-session 没有被续期的问题。 fix: #IA8U1O
    • +
    • 优化:优化 active-timeout 的检查与续期操作,同一请求内只会检查与续期一次。
    • +
    • 修复:SaFoxUtil.joinSharpParam 方法中不正确的注释。
    • +
    • 新增:封禁模块新增支持实时从数据库查询数据。
    • +
    +
  • +
  • SSO:
      +
    • 优化:SSO 示例代码的跨域处理由原生方式改为 Sa-Token 过滤器模式。
    • +
    • 新增:文档新增 “SSO整合 - NoSdk 模式与非 java 项目” 章节。
    • +
    • 新增:“不同 SSO Client 配置不同秘钥” 章节增加部分异常的处理方案提示,fix: #IAFZXL
    • +
    • 删除:sso demo 示例中部分不必要的代码内容。
    • +
    +
  • +
  • OAuth2:
      +
    • 新增:OAuth2 Client 前端测试页。 [重要]
    • +
    • 新增:UnionId 联合id 实现。 [重要]
    • +
    • 新增:oauth2-server 端前后台分离示例与文档。 fix: #I9DQGA#I9W2RU [重要]
    • +
    • 新增:OIDC 模式 nonce 随机数响应校验。 merge: pr311
    • +
    • 修复:错误方法名 deleteGrantScope(String state) -> deleteState(String state)
    • +
    • 修复:全局配置项 sa-token.oauth2-server.oidc.iss 无效的问题。
    • +
    • 新增:回收 Refresh-Token 方法: revokeRefreshTokenrevokeRefreshTokenByIndex
    • +
    • 新增:为 CodeModelAccessTokenModelRefreshTokenModelClientTokenModel 添加 createTime 字段,以记录该数据的创建时间。
    • +
    • 新增:为 Access-Token、Client-Token 添加 grantType 字段,以记录该数据的授权类型。
    • +
    • 新增:SaOAuth2Util.getCode 等方法,以更方便的获取、校验授权码。
    • +
    +
  • +
  • 插件:
      +
    • 新增:新增 sa-token-freemarker 插件,整合 Freemarker 视图引擎。 fix: #651 [重要]
    • +
    • 新增:新增 sa-token-spring-el 插件,用于支持 SpEL 表达式注解鉴权。 fix: #IB3GBB、fix: #IAIXSL、fix: #I9P24F [重要]
    • +
    +
  • +
  • 文档:
      +
    • 新增:新增 MongoDB 集成示例。 感谢 @lilihao 提供的示例。 merge: pr322pr667 [重要]
    • +
    • 新增:“fox说技术” 视频教程链接。
    • +
    • 新增:“API接口参数签名”章节 视频讲解链接(B站抓蛙师)。
    • +
    • 优化:文档首页首屏增加需求提交按钮。
    • +
    • 其它:补全赞助者名单、Dromara 项目链接等信息。
    • +
    • 新增:SpringBoot3.x 版本配置 Redis 注意事项。fix: #688
    • +
    • 新增:gitcode g-star badge 展示。
    • +
    • 修复:OAuth2 滞后的配置信息示例。
    • +
    • 新增:新增视频账号链接。
    • +
    • 新增:新增团队成员展示。
    • +
    +
  • +
+

更新日志在线文档直达链接:https://sa-token.com/doc.html#/more/update-log

+

🌟 其它

+

代码仓库地址:https://gitee.com/dromara/sa-token

+

框架功能结构图:

+

js

+

本文章由 DeepSeek 参与指导与润色,感谢 DeepSeek 团队。

+
+← Sa-Token v1.39.0 更新,大幅重构 OAuth2 模块 +Sa-Token v1.41.0 发布 🚀,来看看有没有令你心动的功能! → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-41-0-fa-bu-lai-kan-kan-you-mei-you-ling-ni-xin-dong-de-gong-neng.html b/sa-token-doc/blog/release/sa-token-v1-41-0-fa-bu-lai-kan-kan-you-mei-you-ling-ni-xin-dong-de-gong-neng.html new file mode 100644 index 000000000..ac3b31934 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-41-0-fa-bu-lai-kan-kan-you-mei-you-ling-ni-xin-dong-de-gong-neng.html @@ -0,0 +1,503 @@ + + + + + Sa-Token v1.41.0 发布 🚀,来看看有没有令你心动的功能! - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.41.0 发布 🚀,来看看有没有令你心动的功能!

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。🔐

+

目前最新版本 v1.41.0 已推送至 Maven 中央仓库 🎉,大家可以通过如下方式引入:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.41.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

该版本包含大量 ⛏️️️新增特性、⛏️底层重构、⛏️️️代码优化 等,下面容我列举几条比较重要的更新内容供大家参阅:

+

🛡️ 更新点1:防火墙模块新增 hooks 扩展机制

+

本次更新针对防火墙新增了多条校验规则,之前的规则为:
+- path 白名单放行。
+- path 黑名单拦截。
+- path 危险字符校验。

+

本次新增规则为:
+- path 禁止字符校验。
+- path 目录遍历符检测(优化了检测算法)。
+- 请求 host 检测。
+- 请求 Method 检测。
+- 请求 Header 头检测。
+- 请求参数检测。

+

并且本次更新开放了 hooks 机制,允许开发者注册自定义的校验规则 🛠️,参考如下:

+
@PostConstruct
+public void saTokenPostConstruct() {
+    // 注册新 hook 演示,拦截所有带有 pwd 参数的请求,拒绝响应 
+    SaFirewallStrategy.instance.registerHook((req, res, extArg)->{
+        if(req.getParam("pwd") != null) {
+            throw new FirewallCheckException("请求中不可包含 pwd 参数");
+        }
+    });
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
+

文档直达地址:Sa-Token 防火墙 🔗

+

💡 更新点2:新增基于 SPI 机制的插件体系

+

之前在 Sa-Token 中也有插件体系,不过都是利用 SpringBoot 的 SPI 机制完成组件注册的。

+

这种注册机制有一个问题,就是插件只能在 SpringBoot 环境下正常工作,在其它环境,比如 Solon 项目中,就只能手动注册插件才行 😫。

+

也就是说,严格来讲,这些插件只能算是 SpringBoot 的插件,而非 Sa-Token 框架的插件 🌐。

+

为了提高插件的通用性,Sa-Token 设计了自己的 SPI 机制,使得这些插件可以在更多的项目环境下正常工作 🚀。

+

第一步:实现插件注册类,此类需要 implements SaTokenPlugin 接口 👨💻:

+
/**
+ * SaToken 插件安装:插件作用描述
+ */
+public class SaTokenPluginForXxx implements SaTokenPlugin {
+    @Override
+    public void install() {
+        // 书写需要在项目启动时执行的代码,例如:
+        // SaManager.setXxx(new SaXxxForXxx());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

第二步:在项目的 resources\META-INF\satoken\ 文件夹下 📂 创建 cn.dev33.satoken.plugin.SaTokenPlugin 文件,内容为该插件注册类的完全限定名:

+
cn.dev33.satoken.plugin.SaTokenPluginForXxx
  • 1
+

这样便可以在项目启动时,被 Sa-Token 插件管理器加载到此插件,执行插件注册类的 install 方法,完成插件安装 ✅。

+

文档直达地址:Sa-Token 插件开发指南 🔗

+

🎛️ 更新点3:重构缓存体系,将数据读写与序列化操作分离

+

在之前的版本中,Redis 集成通常和具体的序列化方式耦合在一起,这不仅让 Redis 相关插件产生大量的重复冗余代码,也让大家在选择 Redis 插件时严重受限。⚠️

+

本次版本更新彻底重构了此模块,将数据读写与序列化操作分离,使其每一块都可以单独自定义实现类,做到灵活扩展 ✨,例如:

+
    +
  • 1️⃣ SaTokenDao 数据读写可以选择:RedisTemplate、Redisson、ConcurrentHashMap、Hutool-Timed-Cache 等不同实现类。
  • +
  • 2️⃣ SaSerializerTemplate 序列化器可以选择:Base64编码、Hex编码、ISO-8859-1编码、JSON序列化等不同方式。
  • +
  • 3️⃣ JSON 序列化可以选择:Jackson、Fastjson、Snack3 等组件。
  • +
+

所有实现类均可以按需选择,自由搭配,大大提高灵活性🏗️。

+

⚙️️ 更新点4:SaLoginParameter 登录参数类新增大量配置项

+

SaLoginParameter (前SaLoginModel) 用于控制登录操作中的部分细节行为,本次新增的配置项有:
+- isConcurrent:决定是否允许同一账号多地同时登录(为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)。🌍
+- isShare:在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)。🔄
+- maxLoginCount:同一账号最大登录数量,超出此数量的客户端将被自动注销,-1代表不限制数量。🚫
+- maxTryTimes:在创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用。⏳
+- deviceId:此次登录的客户端设备id,用于判断后续某次登录是否为可信任设备。📱
+- terminalExtraData:本次登录挂载到 SaTerminalInfo 的自定义扩展数据。📦

+

以上大部分配置项在之前的版本中也有支持,不过它们都被定义在了全局配置类 SaTokenConfig 之上,本次更新支持在 SaLoginParameter 中定义这些配置项,
+这将让登录策略的控制变得更加灵活。✨

+

🚪 更新点5:新增 SaLogoutParameter 注销参数类

+

SaLogoutParameter 用于控制注销操作中的部分细节行为️,例如:

+

通过 Range 参数决定注销范围 🎯:

+
// 注销范围: TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话
+StpUtil.logout(new SaLogoutParameter().setRange(SaLogoutRange.TOKEN));
  • 1
  • 2
+

通过 DeviceType 参数决定哪些登录设备类型参与注销 💻:

+
// 指定 10001 账号,所有 PC 端注销下线,其它端如 APP 端不受影响 
+StpUtil.logout(10001, new SaLogoutParameter().setDeviceType("PC"));
  • 1
  • 2
+

还有其它参数此处暂不逐一列举,文档直达地址:Sa-Token 登录参数 & 注销参数 🔗

+

🐞 更新点6:修复 StpUtil.setTokenValue("xxx")loginParameter.getIsWriteHeader() 空指针的问题。

+

这个没啥好说的,有 bug 🐛 必须修复。

+

fix issue:#IBKSM0 🔗

+

✨ 更新点7:API 参数签名模块升级

+
    +
  • 1、新增了 @SaCheckSign 注解,现在 API 参数签名模块也支持注解鉴权了。🆕
  • +
  • 2、新增自定义签名的摘要算法,现在不仅可以 md5 算法计算签名,也支持 sha1、sha256 等算法了。🔐
  • +
  • 3、新增多应用模式:
  • +
+

多应用模式就是指,允许在对接多个系统时分别使用不同的秘钥等配置项,配置示例如下 📝:

+
sa-token: 
+    # API 签名配置 多应用模式
+    sign-many:
+        # 应用1
+        xm-shop:
+            secret-key: 0123456789abcdefg
+            digest-algo: md5
+        # 应用2
+        xm-forum:
+            secret-key: 0123456789hijklmnopq
+            digest-algo: sha256
+        # 应用3
+        xm-video:
+            secret-key: 12341234aaaaccccdddd
+            digest-algo: sha512
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

然后在签名时通过指定 appid 的方式获取对应的 SignTemplate 进行操作 👨💻:

+
// 创建签名示例
+String paramStr = SaSignMany.getSignTemplate("xm-shop").addSignParamsAndJoin(paramMap);
+
+// 校验签名示例
+SaSignMany.getSignTemplate("xm-shop").checkRequest(SaHolder.getRequest());
  • 1
  • 2
  • 3
  • 4
  • 5
+

⚡ 更新点8:新增 sa-token-caffeine 插件,用于整合 Caffeine

+

Caffeine 是一个基于 Java 的高性能本地缓存库,本次新增 sa-token-caffeine 插件用于将 Caffeine 作为 Sa-Token 的缓存层,存储会话鉴权数据。🚀
+这进一步丰富了 Sa-Token 的缓存层插件生态。🌱

+
<!-- Sa-Token 整合 Caffeine -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-caffeine</artifactId>
+    <version>1.41.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

🎪 更新点9:新增 sa-token-serializer-features 序列化扩展包

+

引入此插件可以为 Sa-Token 提供一些有意思的序列化方案。(娱乐向,不建议上生产 🎭)

+

例如:以base64 编码,采用:元素周期表 🧪、特殊符号 🔣、或 emoji 😊 作为元字符集存储数据 :

+

sa-custom-serializer-yszqb.png

+

sa-custom-serializer-tsfh.png

+

sa-custom-serializer-emoji.png

+

sa-custom-serializer-emoji2.png

+

📜 完整更新日志

+

除了以上提到的几点以外,还有更多更新点无法逐一详细介绍,下面是 v1.41.0 版本的完整更新日志:

+
    +
  • core:
      +
    • 修复:修复 StpUtil.setTokenValue("xxx")loginParameter.getIsWriteHeader() 空指针的问题。 fix: #IBKSM0
    • +
    • 修复:将 SaDisableWrapperInfo.createNotDisabled() 默认返回值封禁等级改为 -2,以保证向之前版本兼容。
    • +
    • 新增:新增基于 SPI 的插件体系。 [重要]
    • +
    • 重构:JSON 转换器模块。 [重要]
    • +
    • 新增:新增 serializer 序列化模块,控制 ObjectString 的序列化方式。 [重要]
    • +
    • 重构:重构防火墙模块,增加 hooks 机制。 [重要]
    • +
    • 新增:防火墙新增:请求 path 禁止字符校验、Host 检测、请求 Method 检测、请求头检测、请求参数检测。重构目录遍历符检测算法。
    • +
    • 重构:重构 SaTokenDao 模块,将序列化与存储操作分离。 [重要]
    • +
    • 重构:重构 SaTokenDao 默认实现类,优化底层设计。
    • +
    • 新增:isLastingCookie 配置项支持在全局配置中定义了。
    • +
    • 重构:SaLoginModel -> SaLoginParameter[不向下兼容]
    • +
    • 重构:TokenSign -> SaTerminalInfo[不向下兼容]
    • +
    • 新增:SaTerminalInfo 新增 extraData 自定义扩展数据设置。
    • +
    • 新增:SaLoginParameter 支持配置 isConcurrentisSharemaxLoginCountmaxTryTimes
    • +
    • 新增:新增 SaLogoutParameter,用于控制注销会话时的各种细节。 [重要]
    • +
    • 新增:新增 StpLogic#isTrustDeviceId 方法,用于判断指定设备是否为可信任设备。
    • +
    • 新增:新增 StpUtil.getTerminalListByLoginId(loginId)StpUtil.forEachTerminalList(loginId) 方法,以更方便的实现单账号会话管理。
    • +
    • 升级:API 参数签名配置支持自定义摘要算法。
    • +
    • 新增:新增 @SaCheckSign 注解鉴权,用于 API 签名参数校验。
    • +
    • 新增:API 参数签名模块新增多应用模式。 fix: #IAK2BI, #I9SPI1, #IAC0P9 [重要]
    • +
    • 重构:全局配置 is-share 默认值改为 false。 [不向下兼容]
    • +
    • 重构:踢人下线、顶人下线默认将删除对应的 token-session 对象。
    • +
    • 优化:优化注销会话相关 API。
    • +
    • 重构:登录默认设备类型值改为 DEF。 [不向下兼容]
    • +
    • 重构:BCrypt 标注为 @Deprecated
    • +
    • 新增:sa-token-quick-login 支持 SpringBoot3 项目。 fix: #IAFQNE#673
    • +
    • 新增:SaTokenConfig 新增 replacedRangeoverflowLogoutModelogoutRangeisLogoutKeepFreezeOpsisLogoutKeepTokenSession 配置项。
    • +
    +
  • +
  • OAuth2:
      +
    • 重构:重构 sa-token-oauth2 插件,使注解鉴权处理器的注册过程改为 SPI 插件加载。
    • +
    +
  • +
  • 插件:
      +
    • 新增:sa-token-serializer-features 插件,用于实现各种形式的自定义字符集序列化方案。
    • +
    • 新增:sa-token-fastjson 插件。
    • +
    • 新增:sa-token-fastjson2 插件。
    • +
    • 新增:sa-token-snack3 插件。
    • +
    • 新增:sa-token-caffeine 插件。
    • +
    +
  • +
  • 单元测试:
      +
    • 新增:sa-token-json-test json 模块单元测试。
    • +
    • 新增:sa-token-serializer-test 序列化模块单元测试。
    • +
    +
  • +
  • 文档:
      +
    • 新增:QA “多个项目共用同一个 redis,怎么防止冲突?”
    • +
    • 优化:补全 OAuth2 模块遗漏的相关配置项。
    • +
    • 优化:优化 OAuth2 简述章节描述文档。
    • +
    • 优化:完善 “SSO 用户数据同步 / 迁移” 章节文档。
    • +
    • 修正:补全项目目录结构介绍文档。
    • +
    • 新增:文档新增 “登录参数 & 注销参数” 章节。
    • +
    • 优化:优化“技术求助”按钮的提示文字。
    • +
    • 新增:新增 preview-doc.bat 文件,一键启动文档预览。
    • +
    • 完善:完善 Redis 集成文档。
    • +
    • 新增:新增单账号会话查询的操作示例。
    • +
    • 新增:新增顶人下线 API 介绍。
    • +
    • 新增:新增 自定义序列化插件 章节。
    • +
    +
  • +
  • 其它:
      +
    • 新增:新增 sa-token-demo/pom.xml 以便在 idea 中一键导入所有 demo 项目。
    • +
    • 删除:删除不必要的 .gitignore 文件
    • +
    • 重构:重构 sa-token-solon-plugin 插件。
    • +
    • 新增:新增设备锁登录示例。
    • +
    +
  • +
+

更新日志在线文档直达链接:https://sa-token.com/doc.html#/more/update-log

+

🌟 其它

+

代码仓库地址:https://gitee.com/dromara/sa-token

+

框架功能结构图:

+

js

+
+← Sa-Token v1.40.0 发布 🚀,来看看有没有令你心动的功能! +Sa-Token v1.42.0 发布 🚀,新增 API Key、TOTP 验证码、RefreshToken 反查等能力 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-42-0-fa-bu-xin-zeng-api-key-totp-yan-zheng-ma-refreshtoken-fan-cha-d.html b/sa-token-doc/blog/release/sa-token-v1-42-0-fa-bu-xin-zeng-api-key-totp-yan-zheng-ma-refreshtoken-fan-cha-d.html new file mode 100644 index 000000000..8156e54e0 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-42-0-fa-bu-xin-zeng-api-key-totp-yan-zheng-ma-refreshtoken-fan-cha-d.html @@ -0,0 +1,466 @@ + + + + + Sa-Token v1.42.0 发布 🚀,新增 API Key、TOTP 验证码、RefreshToken 反查等能力 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.42.0 发布 🚀,新增 API Key、TOTP 验证码、RefreshToken 反查等能力

+ +

Sa-Token 是一款 免费开源 的轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。🔐

+

目前最新版本 v1.42.0 已推送至 Maven 中央仓库 🎉,大家可以通过如下方式引入:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.42.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

该版本包含大量 ⛏️️️新增特性、⛏️底层重构、⛏️️️代码优化 等,下面容我列举几条比较重要的更新内容供大家参阅:

+

🗝️ 更新点1:新增 API Key 模块

+

如果你曾经对接过 ChatGPT、DeepSeek 等大模型平台的开放接口,那你一定对 API Key 不陌生。🤝

+

API Key 是一种接口调用密钥,类似于会话 token ,但比会话 token 具有更灵活的权限控制。🔑

+

本次更新带来了 API Key 的全流程管理,支持为指定账号签发、校验、禁用、删除 API Key 。
+同时每个 API Key 都可以单独设置不同的 scope 权限,以便在不同的场景下使用不同的 API Key,做到秘钥相互隔离,最小化授权。🛡️

+

为了更好的展示此模块的能力,我们专门制作了一个 demo 示例:

+

sa-api-key

+

示例仓库地址:sa-token-demo-apikey 🔗

+

在这个示例中,你可以登录测试不同的账号,并为它们签发 API Key,设置 scope 权限,并使用不同的 API Key 测试调用接口,观察响应结果。 🧪

+

框架默认将所有 API Key 信息保存在缓存中,这可以称之为“缓存模式”,在这种模式下,重启缓存库后,数据将会丢失。⚠️

+

框架预留了 SaApiKeyDataLoader 接口,以便你将数据的加载切换为 “数据库模式”,做到数据长久有效保存。 💾

+

在线文档直达地址:API Key 接口调用秘钥 🔗

+

🔍 更新点2:重构 TempToken 模块新增 value 反查机制

+

在 Sa-Token 文档中有一段这样的示例:📚

+

sa-refresh-token

+

该示例演示了如何通过临时 Token 认证模块,创建 RefreshToken 为登录会话做到双 Token 的效果。🔄

+

但是有一天我在官网 sa-token 小助手接收到一位用户的咨询: 💬

+

sa-refresh-token-wnglian-zixun

+

该用户指出,是否可以为 RefreshToken 提供反查机制,以便获取某个账号历史签发的 全部 RefreshToken

+

必须安排!💪🏆

+

此次版本更新,允许程序在创建 refresh-token 时,指定第三个参数,该参数表示是否允许框架记录 Token 索引信息:

+
SaTempUtil.createToken("10001", 2592000, true);
  • 1
+

指定为 false 代表不记录索引,只生成 token,指定为 true 代表记录索引信息,以便日后可以通过 value 反查历史签发的所有 token。🔍

+

例如我们可以通过 SaTempUtil.getTempTokenList("xxx") 方法获取指定账号所有历史签发的 RefreshToken 记录:

+
List<String> refreshTokenList = SaTempUtil.getTempTokenList("10001");
  • 1
+

在线文档直达地址:临时 Token 令牌认证 🔗

+

⏱️ 更新点3:新增 TOTP 算法实现

+

TOTP 是一种动态密码算法,用于生成短暂有效的数字验证码(通常6-8位)️。它的核心原理是:结合密钥与当前时间,通过哈希运算生成一次性密码。⏱

+

TOTP 一般有以下应用场景:

+
    +
  • 1、登录时的双因子认证:用户输入账号密码后还需要再输入 TOTP 验证码才可以登录成功。 🔐
  • +
  • 2、敏感操作的二次认证:用户在进行一些高危敏感操作时,需要输入 TOTP 验证码才可以继续操作。🛡️
  • +
  • 3、替代短信验证码:TOTP 验证码无需网络,可离线计算生成,一定程度上可以替代短信验证码验证身份。📴
  • +
+

本次版本新增了 TOTP 验证码的生成与校验功能,这将方便大家为自己的系统添加双因子认证能力。🚀

+

⚙️ 更新点4:重构升级 SaTokenContext 上下文读写策略

+

这可能是近几个版本中最底层的一次重构,几乎完全推翻了之前上下文模块的设计。💥

+

在之前的版本中,Sa-Token 对接不同的 Web 框架需要利用这些 Web 框架的原生上下文能力来构建 Sa-Token 的上下文。 🌐

+

本次更新 Sa-Token 利用 ThreadLocal 实现了自己的上下文存储机制,这将带来以下好处:

+
    +
  • 1、可以更方便、更简单的对接更多的 Web 框架。
  • +
  • 2、可在异步场景中临时 Mock 一个上下文,调用 Sa-Token 框架同步 API。
  • +
  • 3、彻底删除了二级上下文模块,做到了 Web 请求与 RPC 请求的上下文统一。
  • +
  • 4、在防火墙 hook 里也可以调用 Sa-Token 同步 API 了。
  • +
+

🌐 更新点5:新增 CORS 跨域策略处理函数,提供不同架构下统一的跨域处理方案

+

在之前的版本中,跨域处理总是要写在全局鉴权过滤器中,属于“鉴权之下的额外补充操作”。⏳

+

新版本专门提供了一个 CORS 跨域处理策略组件,以后再也不用仅仅为了跨域就书写一个长长的鉴权过滤器组件了。🚀

+
/**
+ * CORS 跨域处理
+ */
+@Bean
+public SaCorsHandleFunction corsHandle() {
+    return (req, res, sto) -> {
+        res.
+                // 允许指定域访问跨域资源
+                setHeader("Access-Control-Allow-Origin", "*")
+                // 允许所有请求方式
+                .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
+                // 有效时间
+                .setHeader("Access-Control-Max-Age", "3600")
+                // 允许的header参数
+                .setHeader("Access-Control-Allow-Headers", "*");
+
+        // 如果是预检请求,则立即返回到前端
+        SaRouter.match(SaHttpMethod.OPTIONS)
+                .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
+                .back();
+    };
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
+

开源仓库示例:sa-token-demo-cross 🔗

+

🔑 更新点6:sa-token-quick-login 插件支持 Http Basic 方式通过认证

+

sa-token-quick-login 可以快速、方便的为项目注入一个登录页面,当我们引入依赖后:

+
<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-quick-login</artifactId>
+    <version>1.42.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
+

启动类:

+
@SpringBootApplication
+public class SaTokenQuickDemoApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SaTokenQuickDemoApplication.class, args);
+
+        System.out.println("\n------ 启动成功 ------");
+        System.out.println("name: " + SaQuickManager.getConfig().getName());
+        System.out.println("pwd:  " + SaQuickManager.getConfig().getPwd());
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

测试 Controller

+
@RestController
+public class TestController {
+    @RequestMapping({"/", "/index"})
+    public String index() {
+        String str = "<br />"
+                + "<h1 style='text-align: center;'>资源页 (登录后才可进入本页面) </h1>"
+                + "<hr/>"
+                + "<p style='text-align: center;'> Sa-Token " + SaTokenConsts.VERSION_NO + " </p>";
+        return str;
+    }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

启动项目,使用浏览器访问:http://localhost:8081,首次访问时,由于处于未登录状态,会被强制进入登录页面 🚪:

+

登录

+

使用默认账号:sa / 123456进行登录,会看到资源页面

+

登录

+

新版本中更新了通过 Http Basic 的方式直接进行认证的能力:

+
http://sa:123456@localhost:8081/
  • 1
+

这将非常有助于大家在专门的 API 测试工具下进行 quick-login 相关资源接口的测试。🧪

+

📜 完整更新日志

+

除了以上提到的几点以外,还有更多更新点无法逐一详细介绍,下面是 v1.42.0 版本的完整更新日志:

+
    +
  • core:
      +
    • 新增: 新增 API Key 模块。 [重要]
    • +
    • 新增: 新增 TOTP 实现。 [重要]
    • +
    • 重构:重构 TempToken 模块,新增 value 反查 token 机制。 [重要]
    • +
    • 升级: 重构升级 SaTokenContext 上下文读写策略。 [重要]
    • +
    • 新增: 新增 Mock 上下文模块。 [重要]
    • +
    • 删除: 删除二级上下文模块。
    • +
    • 新增: 新增异步场景使用 demo。 [重要]
    • +
    • 新增: 新增 Base32 编码工具类。
    • +
    • 新增:新增 CORS 跨域策略处理函数,提供不同架构下统一的跨域处理方案。
    • +
    • 新增:renewTimeout 续期方法增加 token 终端信息有效性校验。
    • +
    • 新增: 全局配置项 cookieAutoFillPrefix:cookie 模式是否自动填充 token 前缀。
    • +
    • 新增: 全局配置项 rightNowCreateTokenSession:在登录时,是否立即创建对应的 Token-Session
    • +
    • 优化:优化 Token-Session 获取算法,减少缓存读取次数。
    • +
    • 新增:SaLoginParameter 支持配置 SaCookieConfig,以配置 Cookie 相关参数。
    • +
    • 修改:防火墙校验过滤器的注册顺序 修改为 -102。
    • +
    • 新增:防火墙 hook 注册新增 registerHookToFirstregisterHookToSecond 方法,以便更灵活的控制 hook 顺序。
    • +
    +
  • +
  • 插件:
      +
    • 新增: sa-token-quick-login 插件支持 Http Basic 方式通过认证。
    • +
    +
  • +
  • 单元测试:
      +
    • 补全:补全 Temp Token 模块单元测试。
    • +
    +
  • +
  • 文档:
      +
    • 补全:补全赞助者名单。
    • +
    • 修复:修复 Thymeleaf 集成文档不正确的依赖示例说明。
    • +
    • 修复:修复 unionid 章节错误描述。
    • +
    • 优化:采用更细致的描述优化SSO模式三单点注销步骤。
    • +
    • 新增:登录认证文档添加 Cookie 查看步骤演示图。
    • +
    • 新增:多账号模式新增注意点:运行时不可更改 LoginType
    • +
    • 新增: 多账号模式QA:在一个接口里获取是哪个体系的账号正在登录。
    • +
    • 新增:新增QA:解决低版本 SpringBoot (<2.2.0) 引入 Sa-Token 报错的问题。
    • +
    • 新增:新增QA:前后端一体项目下,在拦截未登录进入登录页面时,如何登录完成后原路返回?
    • +
    • 新增:新增QA:Sa-Token 集成 Redis 如何集群?
    • +
    • 新增:新增QA:如何自定义框架读取 token 的方式?
    • +
    • 新增:新增QA:修改 hosts 文件无效可能原因排查。
    • +
    • 新增:新增QA:如何防止 CSRF 攻击。
    • +
    • 新增: “异步 & Mock 上下文” 章节。
    • +
    • 升级:升级“自定义 SaTokenContext 指南”章节文档。
    • +
    +
  • +
+

更新日志在线文档直达链接:https://sa-token.com/doc.html#/more/update-log

+

🌟 其它

+

代码仓库地址:https://gitee.com/dromara/sa-token

+

框架功能结构图:

+

js

+
+← Sa-Token v1.41.0 发布 🚀,来看看有没有令你心动的功能! +Sa-Token v1.43.0 发布 🚀,新增 SSO 单设备注销、消息推送,多 Access-Token 并存能力 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-43-0-fa-bu-xin-zeng-sso-dan-she-bei-zhu-xiao-xiao-xi-tui-song-duo-ac.html b/sa-token-doc/blog/release/sa-token-v1-43-0-fa-bu-xin-zeng-sso-dan-she-bei-zhu-xiao-xiao-xi-tui-song-duo-ac.html new file mode 100644 index 000000000..5fe161bea --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-43-0-fa-bu-xin-zeng-sso-dan-she-bei-zhu-xiao-xiao-xi-tui-song-duo-ac.html @@ -0,0 +1,523 @@ + + + + + Sa-Token v1.43.0 发布 🚀,新增 SSO 单设备注销、消息推送,多 Access-Token 并存能力 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.43.0 发布 🚀,新增 SSO 单设备注销、消息推送,多 Access-Token 并存能力

+ +

Sa-Token 是一款 免费开源 的轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。🔐

+

sa-token-jss--tran

+

目前最新版本 v1.43.0 已推送至 Maven 中央仓库 🎉,大家可以通过如下方式引入:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot-starter</artifactId>
+    <version>1.43.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

该版本包含大量 ⛏️️️新增特性、⛏️底层重构、⛏️️️代码优化 等,下面容我列举几条比较重要的更新内容供大家参阅:

+

⛏️️️️ 更新点1:单点登录模块新增“单设备注销”模式

+

有单点登录就必然要有单点注销,目前 SSO 单点登录模块包含:单应用注销、单设备注销、全端注销 三种模式,怎么理解三者的差异呢?

+

举个例子,用户在 Chrome 浏览器 登录了 应用A、应用B、应用C,又在 Firefox 浏览器登录了应用A、应用B,那么此时他:

+
    +
  • 点击“单应用注销”后:他只会在应用A注销下线,其它应用依然会保持登录状态。
  • +
  • 点击“单设备注销”后:他在 Chrome 浏览器登录的所有应用会一起下线,但是 Firefox 浏览器登录的应用不受影响。
  • +
  • 点击“全端注销”后:用户在所有浏览器登录的所以会话一起注销下线。
  • +
+

以上几种注销方式仅需通过一行代码或者调用一个接口即可实现,详细可参考在线文档:Sa-Token SSO 单点注销

+

⛏️️️️ 更新点2:单点登录模块新增消息推送机制

+

此提案来源于社区交流群中的一位开发者咨询:

+

sa-sso-message-push-tian.png

+

该开发者指出是否可以在 SSO 模块增加各个系统互相通知 token 续期的功能,以便做到系统之间会话有效期的强同步。

+

本次更新没有直接增加这两个 API,而是从底层构建了一套消息推送体系,允许 sso-client 端按照特点格式构建一个 http 请求,调用 sso-server 端的 /sso/pushS 接口,sso-server 接收到消息后做出处理回应 sso-client 端。

+

消息推送是相互的,sso-server 端也可以构建 http 请求,调用 sso-client 端的 /sso/pushC 接口。

+

消息推送机制是应用与认证中心相互沟通的桥梁,ticket 校验、单点注销等行为都是依赖消息推送机制来实现的。

+

当然你也可以通过自定义消息处理器的方式,来扩展消息推送能力,这将非常有助于你完成一些应用与认证中心的自定义数据交互。

+

假设我们现在有如下需求:在 sso-client 获取 sso-server 端指定账号 id 的昵称、头像等信息,即:用户资料的拉取。

+

首先,我们需要在 sso-server 实现一个消息处理器:

+
@RestController
+public class SsoServerController {
+
+    // 配置SSO相关参数 
+    @Autowired
+    private void configSso(SaSsoServerTemplate ssoServerTemplate) {
+
+        // 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口)
+        ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> {
+            System.out.println("收到消息:" + message);
+
+            // 自定义返回结果(模拟)
+            return SaResult.ok()
+                    .set("id", message.get("loginId"))
+                    .set("name", "LinXiaoLin")
+                    .set("sex", "女")
+                    .set("age", 18);
+        });
+
+    }
+
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
+

在 sso-client 端配置文件里要配置上消息推送的具体地址

+
# sa-token配置 
+sa-token:
+    # sso-client 相关配置
+    sso-client:
+        # 应用标识
+        client: sso-client3
+        # sso-server 端推送消息地址
+        push-url: http://sa-sso-server.com:9000/sso/pushS
+        # API 接口调用秘钥
+        secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
+

然后在需要拉取资料的地方:

+
// 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端
+@RequestMapping("/sso/myInfo")
+public Object myInfo() {
+    // 如果尚未登录
+    if( ! StpUtil.isLogin()) {
+        return "尚未登录,无法获取";
+    }
+
+    // 获取本地 loginId
+    Object loginId = StpUtil.getLoginId();
+
+    // 构建消息对象 
+    SaSsoMessage message = new SaSsoMessage();
+    message.setType("userinfo");
+    message.set("loginId", loginId);
+
+    // 推送至 sso-server,并接收响应数据 
+    SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);
+
+    // 返回给前端
+    return result;
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
+

详细可参考在线文档:Sa-Token SSO 消息推送机制

+

⛏️️️️ 更新点3:单点登录新增 resdk 对接模式

+

Sa-Token SSO 模块一直是支持非 Sa-Token 技术栈、甚至非 java 项目对接的,在之前版本给出的对接方式是 NoSdk 模式。

+

NoSdk 模式就是指不集成 Sa-Token,直接通过 http 工具类调用接口的方式来对接 SSO-Server。

+

参考 demo:sa-token-demo-sso3-client-nosdk

+

该 demo 假设应用端没有使用任何“权限认证框架”,使用最基础的 ServletAPI 进行会话管理,模拟了 /sso/login/sso/logout/sso/logoutCall 三个接口的处理逻辑。

+

但是基于以下原因,NoSdk 示例将不再维护:

+
    +
  • 1、NoSdk demo 相当于通过 http 工具类再次重写了一遍 Sa-Token SSO 模块代码,繁琐且冗余。
  • +
  • 2、重写的代码无法拥有 Sa-Token SSO 模块全部能力,仅能完成基本对接,算是一个简化版 SDK。
  • +
+

最新版我们推荐使用 ReSdk 方式进行对接:ReSdk 模式(重写SDK部分方法):通过重写框架关键步骤点,来对接 SSO-Server。

+

参考 demo:sa-token-demo-sso3-client-resdk

+

ReSdk 模式优点:

+

1、依然支持客户端使用任意技术栈。
+2、仅重写少量部分关键代码,即可完成对接。几乎可以得到 Sa-Token SSO 模块全量能力。

+

⛏️️️️ 更新点4:OAuth2新增多 Access-Token 并存能力。

+

点击非常高的一个提案:OAuth2 模块每次生成新的 access_token 时能否保留旧 access_token 依然有效

+

fix issues: #IBHFD1#IBLL4Q#724

+

sa-many-access-token-issue1

+

sa-many-access-token-issue2

+

sa-many-access-token-issue3

+

在之前的版本中,因索引数据结构设计所限,每次申请 access_token 时会导致旧 access_token 立即失效,新版本重构了索引数据结构,增加了多 Access-Token 并存能力。

+

相关 API 展示:

+
// 获取 AccessTokenModel,无效的 AccessToken 会返回 null
+SaOAuth2Util.getAccessToken(accessToken);
+
+// 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常
+SaOAuth2Util.checkAccessToken(accessToken);
+
+// 获取 Access-Token 列表:此应用下 对 某个用户 签发的所有 Access-token
+SaOAuth2Util.getAccessTokenValueList(clientId, loginId);
+
+// 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false
+SaOAuth2Util.hasAccessTokenScope(accessToken, ...scopes);
+
+// 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常
+SaOAuth2Util.checkAccessTokenScope(accessToken, ...scopes);
+
+// 获取 Access-Token 所代表的LoginId
+SaOAuth2Util.getLoginIdByAccessToken(accessToken);
+
+// 获取 Access-Token 所代表的 clientId
+SaOAuth2Util.getClientIdByAccessToken(accessToken);
+
+// 回收一个 Access-Token
+SaOAuth2Util.revokeAccessToken(accessToken);
+
+// 回收全部 Access-Token:指定应用下 指定用户 的全部 Access-Token
+SaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
+

在线文档相关章节:Sa-Token-OAuth2 常用方法

+

⛏️️️️ 更新点5:简化 core 核心包功能模块,进行拆包

+

在社区群聊中有同学提到目前 sa-token-core 核心包功能过于拥挤复杂。应要求本次将部分功能模块进行删减,转移到了 plugin 包下:

+
    +
  • 拆分:API Key 模块拆分独立插件包:sa-token-apikey
  • +
  • 拆分:API Sign 模块拆分独立插件包:sa-token-sign
  • +
+

并且本次更新新增了以下插件包:

+
    +
  • 新增:新增 sa-token-forest 插件,用于在 Http 请求处理器模块整合 Forest。
  • +
  • 新增:新增 sa-token-okhttps 插件,用于在 Http 请求处理器模块整合 OkHttps。
  • +
+

📜 完整更新日志

+

除了以上提到的几点以外,还有更多更新点无法逐一详细介绍,下面是 v1.43.0 版本的完整更新日志:

+
    +
  • core:
      +
    • 新增:SaLogoutParameter 新增 deviceId 参数,用于控制指定设备 id 的注销。 [重要]
    • +
    • 新增:新增 SaHttpTemplate 请求处理器模块。
    • +
    • 新增:TOTP 增加 issuer 字段。 merge: pr 329
    • +
    • 修复:修复 Http Digest 认证时 url 上带有查询参数时认证无法通过的问题。merge: pr 334
    • +
    • 新增:@SaCheckOr 注解添加 append 字段,用于抓取未预先定义的注解类型进行批量注解鉴权。
    • +
    • 新增:侦听器 doRenewTimeout 方法添加 loginType 参数。
    • +
    • 新增:SaInterceptor 新增 beforeAuth 认证前置函数。
    • +
    +
  • +
  • SSO:
      +
    • 新增:单点注销支持单设备注销。 [重要] fix: #IA6ZK0#747
    • +
    • 新增:新增消息推送机制。 [重要] fix: #IBGXA7
    • +
    • 新增:配置项 clients 用于单独配置每个 client 的授权信息。 [重要]
    • +
    • 新增:配置项 allowAnonClient 决定是否启用匿名 client。
    • +
    • 新增:SSO 模块新增配置文件方式启用“不同 client 不同秘钥”能力。
    • +
    • 重构:sso-client 封装化获取 client 标识值。
    • +
    • 新增:新增 SSO Strategy 策略类。
    • +
    • 新增:sso-client 新增 convertCenterIdToLoginIdconvertLoginIdToCenterId 策略函数,用于描述本地 LoginId 与认证中心 loginId 的转换规则。
    • +
    • 新增:sso-server 新增 jumpToRedirectUrlNotice 策略,用于授权重定向跳转之前的通知。
    • +
    • 优化:调整整体 SSO 示例代码。
    • +
    • 新增:新增 ReSdk 模式对接示例:sa-token-demo-sso3-client-resdk[重要]
    • +
    • 新增:新增匿名应用模式对接示例:sa-token-demo-sso3-client-anon[重要]
    • +
    +
  • +
  • OAuth2:
      +
    • 新增:SaClientModel 新增 isAutoConfirm 配置项,用于决定是否允许应用可以自动确认授权。 [重要]
    • +
    • 新增:多 Access-Token 并存、多 Refresh-Token 并存、多 Client-Token 并存能力。 [重要] fix: #IBHFD1#IBLL4Q#724
    • +
    • 新增:Scope 分割符支持加号。merge: pr 333
    • +
    • 修复:修复 oidc 协议下,当用户数据变动后,id_token 仍是旧信息的问题。
    • +
    • 优化:对 OAuth2 Password 认证模式需要重写处理器添加强提醒。
    • +
    • 优化:将认证流程回调从 SaOAuth2ServerConfig 转移到 SaOAuth2Strategy
    • +
    • 新增:新增 SaOAuth2Strategy.instance.userAuthorizeClientCheck 策略,用于检查指定用户是否可以授权指定应用。fix: #553
    • +
    • 优化:优化调整 sa-token-oauth2 模块代码结构及注释。
    • +
    • 新增:currentAccessToken()currentClientToken(),简化读取 access_tokenclient_token 步骤
    • +
    +
  • +
  • 插件:
      +
    • 新增:新增 sa-token-forest 插件,用于在 Http 请求处理器模块整合 Forest。
    • +
    • 新增:新增 sa-token-okhttps 插件,用于在 Http 请求处理器模块整合 OkHttps。
    • +
    • 拆分:API Key 模块拆分独立插件包:sa-token-apikey
    • +
    • 拆分:API Sign 模块拆分独立插件包:sa-token-sign
    • +
    • 修复:修复 sa-token-dubbo 插件部分场景上下文控制出错的问题。
    • +
    • 修复:修复 sa-token-sanck3 SaSessionForSnack3Customized:getModel 接收 map 值时会出错的问题。 merge: pr 330
    • +
    • 修复:修复使用 sa-token-redis-template-jdk-serializer 时反序列化错误。merge: pr 331
    • +
    • 修复:sa-token-snack3 优化 objectToJson 序列化处理(增加类名,但不增加根类名)。
    • +
    • 重构:重构 sa-token-redis-templatesa-token-redis-template-jdk-serializer 插件中 update 方法 ttl 获取方式改为毫秒,以减少 update 时的 ttl 计算误差。 [重要]
    • +
    +
  • +
  • 示例:
      +
    • 新增:新增 SSE 鉴权示例。
    • +
    +
  • +
  • 文档:
      +
    • 新增:新增文档离线版下载。
    • +
    • 新增:新增框架功能列表插图。
    • +
    • 新增:新增示例:如何在响应式环境下的 Filter 里调用 Sa-Token 同步 API。
    • +
    • 新增:新增 QA:在 idea 导入源码,运行报错:java: 程序包cn.dev33.satoken.oauth2不存在。
    • +
    • 新增:新增 QA:新增QA:报错:SaTokenContext 上下文尚未初始化。
    • +
    • 新增:新增 QA:在 idea 导入源码,运行报错:java: 程序包cn.dev33.satoken.oauth2不存在。
    • +
    • 新增:重写路由匹配算法修正为最新写法。
    • +
    • 新增:修复 OAuth2 UnionId 章节相关不正确描述。
    • +
    • 优化:完善 QA:访问了一个不存在的路由,报错:SaTokenContext 上下文尚未初始化。 fix: #771
    • +
    • 优化:补充 sso 模块遗漏的配置字段介绍。
    • +
    • 优化:OAuth2-Server 示例添加真正表单。
    • +
    • 新增:文档新增重写 PasswordGrantTypeHandler 处理器示例。
    • +
    • 新增:sso 章节和 oauth2 章节文档增加可重写策略说明。
    • +
    +
  • +
  • 其它:
      +
    • 新增:readme 新增框架功能介绍图。
    • +
    • 新增:SSO 模块新增思维导图说明。
    • +
    • 新增:readme 新增 Forest 的友情链接。
    • +
    +
  • +
+

更新日志在线文档直达链接:https://sa-token.com/doc.html#/more/update-log

+

🌟 其它

+

代码仓库地址:https://gitee.com/dromara/sa-token

+
+← Sa-Token v1.42.0 发布 🚀,新增 API Key、TOTP 验证码、RefreshToken 反查等能力 +Sa-Token v1.45.0 发布 🚀,正式支持 Spring Boot 4、新增 Jackson3/Snack4 插件适配 → +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/release/sa-token-v1-45-0-fa-bu-zheng-shi-zhi-chi-spring-boot-4-xin-zeng-jackson3-snack4.html b/sa-token-doc/blog/release/sa-token-v1-45-0-fa-bu-zheng-shi-zhi-chi-spring-boot-4-xin-zeng-jackson3-snack4.html new file mode 100644 index 000000000..4cd319711 --- /dev/null +++ b/sa-token-doc/blog/release/sa-token-v1-45-0-fa-bu-zheng-shi-zhi-chi-spring-boot-4-xin-zeng-jackson3-snack4.html @@ -0,0 +1,487 @@ + + + + + Sa-Token v1.45.0 发布 🚀,正式支持 Spring Boot 4、新增 Jackson3/Snack4 插件适配 - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.45.0 发布 🚀,正式支持 Spring Boot 4、新增 Jackson3/Snack4 插件适配

+ +

Sa-Token 是一款 开源免费 的轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0微服务网关鉴权 等一系列权限相关问题。🔐

+

sa-token-jss--tran

+

目前最新版本 v1.45.0 已推送至 Maven 中央仓库 🎉,大家可以通过如下方式引入:

+
<!-- Sa-Token 权限认证 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-spring-boot4-starter</artifactId>
+    <version>1.45.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
+

该版本包含大量 ⛏️️️新增特性、⛏️底层重构、⛏️️️代码优化 等,下面容我列举几条比较重要的更新内容供大家参阅:

+

🚀 更新点1:万人血书的 Spring Boot 4 集成包,它来了!

+

Spring Boot 4 正式版发布后,社区里「求适配」的呼声就没停过!fix: #869#IDB02G#IDGQYM 这次,它真的来了!🎉

+

need-spring-boot-4-starter.png.png

+

本次更新新增了完整的 Spring Boot 4 集成支持:🌟

+
    +
  • sa-token-spring-boot4-starter:WebMVC 环境下的 Spring Boot 4 集成包。
  • +
  • sa-token-reactor-spring-boot4-starter:Reactor 响应式环境下的 Spring Boot 4 集成包。
  • +
+

同时配套新增了示例项目:

+
    +
  • sa-token-demo-springboot4:Spring Boot 4 + WebMVC 整合 demo。
  • +
  • sa-token-demo-webflux-springboot4:Spring Boot 4 + WebFlux 示例。
  • +
+

如果你正在或计划升级到 Spring Boot 4,可以直接引入对应 starter,体验与 Spring Boot 2/3 一致的丝滑集成。✨

+

🎯 更新点2:重复登录处理策略升级,可灵活配置是 “顶人下线” 还是 “不允许登录”

+

Sa-Token 在多端登录控制场景下,现已支持通过 replacedLoginExitMode 配置项自定义重复登录时的行为方式:

+
    +
  • 当同一账号不允许多客户端同时登录时,以往 Sa-Token 的默认策略为“新登录顶掉旧会话”,即新登录后会将旧端踢下线。🔄
  • +
  • 但部分业务对安全性有更高要求,例如用户 A 已在手机端登录,再用电脑登录时,希望直接拦截本次登录,不影响原有会话,即提示“该账号已在其他设备登录,无法顶替下线”,而不是让手机端被踢下线。📱💻
  • +
+

本次更新新增了配置项 replacedLoginExitMode,你可通过它自由选择策略,无需变动业务代码,灵活应对不同安全需求。配置项含义说明如下:

+
    +
  • replacedLoginExitMode = OLD_DEVICE:旧设备下线,新设备登录成功(默认行为,顶人下线模式)。
  • +
  • replacedLoginExitMode = NEW_DEVICE:新设备登录失败,旧设备维持在线(拦截本次登录)。
  • +
+

只需在 Sa-Token 配置文件或启动参数中切换即可,非常便捷。🛡️

+

merge: pr 349

+

📦 更新点3:新增 sa-token-jackson3、sa-token-snack4 插件,生态持续扩展

+

Sa-Token 的 JSON 与序列化生态一直在持续丰富,本次又迎来两位新成员:📚

+
    +
  • sa-token-jackson3:用于 Jackson 3 的 JSON 操作。Jackson 3 是 Jackson 的最新大版本,如果你已经在使用 Jackson 3,现在可以无缝对接 Sa-Token 了。
  • +
  • sa-token-snack4:用于 Snack4 的 JSON 操作。merge: pr 356
  • +
+

引入方式示例:

+
<!-- Sa-Token 整合 Jackson 3 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-jackson3</artifactId>
+    <version>1.45.0</version>
+</dependency>
+
+<!-- Sa-Token 整合 Snack4 -->
+<dependency>
+    <groupId>cn.dev33</groupId>
+    <artifactId>sa-token-snack4</artifactId>
+    <version>1.45.0</version>
+</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

无论你偏好 Jackson、Fastjson、Snack3 还是 Snack4,Sa-Token 都能满足。🎛️

+

🏗️ 更新点4:重构 sa-token-dependencies 及 WebMVC/Reactor 集成包

+

本次版本对依赖体系进行了一次重要重构:🔧

+
    +
  • 重构 sa-token-dependencies 相关模块,优化依赖关系,使版本管理更清晰。
  • +
  • 重构 Spring Boot WebMVC/Reactor 相关集成包,优化模块划分与依赖传递。
  • +
  • 优化整体模块依赖关系,减少冗余、提升构建效率。
  • +
+

这是一次「看不见的升级」,但对长期维护和后续扩展都有积极影响。📐

+

📺 更新点5:SSO 模块新增 STS 协议定义、视频讲解与平台中心模式 demo

+

SSO 单点登录模块在本版本也有不少文档与示例上的增强:📖

+
    +
  • STS 协议定义:文档为 sa-token-sso 模块正式定义了 STS 协议,方便大家理解与对接。
  • +
  • 平台中心模式 demo:sso-server 前后端分离模式下,新增平台中心模式 demo 示例。
  • +
  • 消息处理器相关文档:补全了 SSO 模块内置消息处理器相关文档,修复了 msgType 参数说明与 API 说明。🔗
  • +
  • 视频讲解:B 站 up 主「王清江唷」录制了 SSO 篇共 29 集视频,从零到一讲解单点登录,非常适合入门与进阶。
  • +
+ + +

🐞 更新点6:修复 OAuth2 序列化类型转换、Dubbo 上下文清理等问题

+

本版本修复了多个社区反馈的问题:🙏

+
    +
  • OAuth2:修复 sa-token-oauth2 组件使用 sa-token-fastjson2 序列化导致的类型转换问题。merge: pr 355
  • +
  • Dubbo:修复 Dubbo 上下文清理问题,避免 RPC 调用时上下文污染。merge: pr 889
  • +
  • Core:修复 StpUtil.getLoginIdByTokenNotThinkFreeze 方法缺少 static 修饰符的问题。
  • +
  • Core:优化路由匹配 pattern 缓存算法,消除魔法值。merge: pr 907
  • +
+

📚 更新点7:文档与社区建设

+

文档与社区方面也有不少更新:❤️

+
    +
  • 新增 Sa-Token 内容合作者群,欢迎愿意参与文档、教程、视频等内容创作的小伙伴加入。
  • +
  • 新增赞赏码展示、文档首页 stars 对比图、解决跨域专题文章。
  • +
  • 优化框架 Slogan、README、案例库展示。
  • +
  • 文档主题切换增加水滴特效,登录认证、权限认证、路由拦截鉴权等章节优化。
  • +
  • 补全全局策略说明、数据结构说明,目录树增加项目架构设计栏目。
  • +
  • 新增 Maven 父子项目无法下载依赖的问题解决方案。merge: pr 358
  • +
  • 新增《Gitee 2025 年度开源项目 Web 应用开发 Top 2》证书展示,感谢社区认可。🏆
  • +
+

gitee-2025-top2

+

📜 完整更新日志

+

除了以上提到的几点以外,还有更多更新点无法逐一详细介绍,下面是 v1.45.0 版本的完整更新日志:

+
    +
  • core:
      +
    • 新增:新增重复登录处理策略,当同一账号不允许多客户端同时登录时支持选择踢人下线或拦截本次登录。 [重要] merge: pr 349
    • +
    • 修复:修复 StpUtil.getLoginIdByTokenNotThinkFreeze 方法缺少 static 修饰符的问题。
    • +
    • 优化:优化路由匹配 pattern 缓存算法,消除魔法值。merge: pr 907
    • +
    • 优化:移除冗余导包。
    • +
    +
  • +
  • 插件:
      +
    • 新增:新增 sa-token-jackson3 插件,用于 Jackson 3 的 JSON 操作。 [重要]
    • +
    • 新增:新增 sa-token-jackson3-test 单元测试。
    • +
    • 新增:新增 sa-token-snack4 插件。 [重要] merge: pr 356
    • +
    • 修复:修复 Dubbo 上下文清理问题。 merge: pr 889
    • +
    • 新增:loveqq-framework 版本更新。merge: pr 351
    • +
    +
  • +
  • starter:
      +
    • 新增:新增 sa-token-spring-boot4-starter 集成包,支持 Spring Boot 4 环境集成。 [重要]
    • +
    • 新增:新增 sa-token-reactor-spring-boot4-starter 集成包,支持 Reactor + Spring Boot 4 环境集成。 [重要]
    • +
    • 新增:新增 sa-token-demo-springboot4sa-token-demo-webflux-springboot4 示例。
    • +
    • 新增:新增 Spring Boot 4 整合 demo 示例。
    • +
    +
  • +
  • 重构:
      +
    • 重构:重构 sa-token-dependencies 相关模块,优化依赖关系。 [重要]
    • +
    • 重构:重构 Spring Boot WebMVC/Reactor 相关集成包,优化依赖关系。 [重要]
    • +
    • 优化:优化整体模块依赖关系。
    • +
    +
  • +
  • Solon:
      +
    • 优化:sa-token-solon-plugin 优化 Gateway 接口的处理,避免使用路由接口。merge: pr 348
    • +
    +
  • +
  • SSO:
      +
    • 新增:sso-server 前后端分离模式下 平台中心模式 demo 示例。
    • +
    • 修复:SSO 模块 msgType 参数说明、API 说明修正。
    • +
    • 新增:SSO 模块视频讲解链接:B站 王清江唷 SSO篇(29集)。 [重要]
    • +
    • 补全:SSO 模块内置消息处理器相关文档。
    • +
    • 新增:文档为 sa-token-sso 模块定义 STS 协议。 [重要]
    • +
    +
  • +
  • OAuth2:
      +
    • 修复:修复 sa-token-oauth2 组件使用 sa-token-fastjson2 序列化导致的类型转换问题。merge: pr 355
    • +
    • 优化:修改 ClientIdSecretModel 的读取构建逻辑。merge: pr 346
    • +
    +
  • +
  • 文档:
      +
    • 同步:同步公众号文章列表、博客列表、赞助者名单、企业登记案例。
    • +
    • 新增:新增 Sa-Token 内容合作者群。 [重要]
    • +
    • 新增:新增《Gitee 2025 年度开源项目 Web 应用开发 Top 2》证书展示。
    • +
    • 新增:新增赞赏码展示、文档首页 stars 对比图。
    • +
    • 新增:新增解决跨域专题文章。
    • +
    • 新增:增加微信群聊信息展示。
    • +
    • 优化:优化框架 Slogan。
    • +
    • 优化:优化 README、案例库展示。
    • +
    • 优化:文档主题切换增加水滴特效,调整主题色块顺序。
    • +
    • 优化:文档优化 [登录认证]、[权限认证]、[路由拦截鉴权] 篇。
    • +
    • 优化:补全全局策略说明、数据结构说明。
    • +
    • 新增:目录树增加专门栏目记录项目架构设计。
    • +
    • 优化:功能结构图增加点击事件跳转到对应功能文档。
    • +
    • 优化:子服务外网隔离章节增加示意图。
    • +
    • 优化:Same-Token 同源系统认证图示说明。
    • +
    • 修复:更换 GitCode logo 为 AtomGit。
    • +
    • 修复:更换 QQ 群链接、微信群聊展示图。
    • +
    • 修复:文档图片地址更换为本地文件。
    • +
    • 修复:错别字修复。
    • +
    • 修复:maven-pull.md 文档,解决父子项目依赖下载问题。
    • +
    • 新增:Maven 父子项目无法下载依赖的问题解决方案。merge: pr 358
    • +
    • 修复:订正文档错别字。merge: pr 354
    • +
    • 修复:文档内代码示例修正。merge: pr 347
    • +
    +
  • +
  • AI:
      +
    • 新增:新增 organize-update-log SKILL,用于格式化整理版本更新日志信息。
    • +
    • 新增:新增 commit-message SKILL,用于整理 git commit 日志信息。
    • +
    • 新增:新增 upgrade-version SKILL,用于统一升级修改版本号。
    • +
    • 新增:新增 remove-redundancy-import SKILL,用于检查 Java 类中无效冗余导包并移除。
    • +
    +
  • +
  • 其它:
      +
    • 新增:readme 增加快问快答区域。
    • +
    • 新增:增加忽略 .vscode 目录。
    • +
    • 优化:注释优化。
    • +
    • 重构:备忘录重构为专门的文件夹。
    • +
    • 重构:调整项目发布配置至 Maven Central Portal。merge: pr 792
    • +
    • 优化:部分构建配置升级到最新版。
    • +
    +
  • +
+

更新日志在线文档直达链接:https://sa-token.com/doc.html#/more/update-log

+

🌟 其它

+

代码仓库地址:https://gitee.com/dromara/sa-token

+

框架功能结构图:

+

js

+
+← Sa-Token v1.43.0 发布 🚀,新增 SSO 单设备注销、消息推送,多 Access-Token 并存能力 +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/sitemap.xml b/sa-token-doc/blog/sitemap.xml new file mode 100644 index 000000000..9b6953cff --- /dev/null +++ b/sa-token-doc/blog/sitemap.xml @@ -0,0 +1,191 @@ + + + + https://sa-token.com/blog/index.html + 2026-08-12 + + + https://sa-token.com/blog/special/tai-qiang-le-sa-token-de-nodejs-ban-ben-2.html + 2026-05-16 + + + https://sa-token.com/blog/release/sa-token-v1-45-0-fa-bu-zheng-shi-zhi-chi-spring-boot-4-xin-zeng-jackson3-snack4.html + 2026-03-09 + + + https://sa-token.com/blog/release/sa-token-v1-43-0-fa-bu-xin-zeng-sso-dan-she-bei-zhu-xiao-xiao-xi-tui-song-duo-ac.html + 2025-05-19 + + + https://sa-token.com/blog/featured/shi-yong-sa-token-cors-ce-lve-chu-li-kua-yu-wen-ti-san-zhong-fang-shi-quan-ban.html + 2025-04-11 + + + https://sa-token.com/blog/release/sa-token-v1-42-0-fa-bu-xin-zeng-api-key-totp-yan-zheng-ma-refreshtoken-fan-cha-d.html + 2025-04-11 + + + https://sa-token.com/blog/release/sa-token-v1-41-0-fa-bu-lai-kan-kan-you-mei-you-ling-ni-xin-dong-de-gong-neng.html + 2025-03-21 + + + https://sa-token.com/blog/release/sa-token-v1-40-0-fa-bu-lai-kan-kan-you-mei-you-ling-ni-xin-dong-de-gong-neng.html + 2025-02-01 + + + https://sa-token.com/blog/release/sa-token-v1-39-0-geng-xin-da-fu-zhong-gou-oauth2-mo-kuai.html + 2024-08-28 + + + https://sa-token.com/blog/release/sa-token-v1-35-0-geng-xin-xin-zeng-dong-tai-active-timeout-neng-li.html + 2023-06-27 + + + https://sa-token.com/blog/featured/qian-tan-ti-ren-xia-xian-de-she-ji-si-lu-fu-dai-ma-shi-xian-fang-an-2.html + 2023-04-24 + + + https://sa-token.com/blog/essays/dui-min-gan-cao-zuo-de-er-ci-ren-zheng-xiang-jie-sa-token-er-ji-ren-zheng.html + 2023-04-24 + + + https://sa-token.com/blog/essays/zai-wei-fu-wu-zhong-shi-yong-sa-token-fen-bu-shi-session-hui-hua-wang-guan-tong.html + 2023-04-24 + + + https://sa-token.com/blog/essays/qian-hou-duan-fen-li-jia-gou-xia-shi-yong-sa-token-wan-cheng-deng-lu-ren-zheng.html + 2023-04-24 + + + https://sa-token.com/blog/essays/bie-ren-rao-guo-wo-de-wang-guan-zhi-jie-diao-yong-zi-yuan-fu-wu-zen-me-ban-shi-y.html + 2023-04-24 + + + https://sa-token.com/blog/other/chu-shi-sa-token-yi-xing-dai-ma-gao-ding-deng-lu-shou-quan.html + 2023-04-24 + + + https://sa-token.com/blog/other/gong-zhong-hao-shou-xuan-2.html + 2023-04-24 + + + https://sa-token.com/blog/featured/shi-yong-sa-token-jie-jue-websocket-wo-shou-shen-fen-ren-zheng-2.html + 2023-04-24 + + + https://sa-token.com/blog/essays/shi-yong-sa-token-shi-xian-bu-tong-de-deng-lu-mo-shi-dan-di-deng-lu-duo-di-deng.html + 2023-04-24 + + + https://sa-token.com/blog/essays/shi-yong-sa-token-shi-xian-ji-zhu-wo-mo-shi-deng-lu-qi-tian-mian-deng-lu.html + 2023-04-24 + + + https://sa-token.com/blog/essays/shi-yong-sa-token-wan-cheng-ti-ren-xia-xian-gong-neng.html + 2023-04-24 + + + https://sa-token.com/blog/essays/bu-xiang-xie-dai-ma-dan-xiang-yao-ji-cheng-yi-ge-deng-lu-ye-mian-sa-token-quick.html + 2023-04-24 + + + https://sa-token.com/blog/essays/yi-wen-xiang-jie-sa-token-zhong-de-sasession-dui-xiang.html + 2023-04-24 + + + https://sa-token.com/blog/featured/yi-ge-deng-lu-gong-neng-ye-neng-wan-chu-zhe-me-duo-hua-yang-sa-token-dan-di-deng.html + 2023-04-24 + + + https://sa-token.com/blog/other/java-feng-jin-mou-ge-zhang-hao-hou-shi-qi-hui-hua-li-ji-diao-xian.html + 2023-04-24 + + + https://sa-token.com/blog/essays/springboot-xiang-mu-shi-yong-sa-token-wan-cheng-deng-lu-ren-zheng.html + 2023-04-24 + + + https://sa-token.com/blog/essays/springboot-shi-yong-sa-token-shi-xian-zhang-hao-feng-jin-fen-lei-feng-jin-jie-ti.html + 2023-04-24 + + + https://sa-token.com/blog/essays/springboot-shi-yong-sa-token-wan-cheng-lu-you-lan-jie-jian-quan.html + 2023-04-24 + + + https://sa-token.com/blog/essays/springboot-shi-yong-sa-token-wan-cheng-zhu-jie-jian-quan-gong-neng.html + 2023-04-24 + + + https://sa-token.com/blog/essays/springboot-shi-yong-sa-token-wan-cheng-quan-xian-ren-zheng.html + 2023-04-24 + + + https://sa-token.com/blog/other/sa-token-you-ya-de-jiang-jian-quan-yu-ye-wu-dai-ma-fen-li.html + 2023-04-24 + + + https://sa-token.com/blog/essays/sa-token-ji-cheng-jwt.html + 2023-04-24 + + + https://sa-token.com/blog/special/sa-token-di-10000-ge-star-li-cheng-bei-gan-xie-san-nian-lai-suo-you-guan-zhu-sa.html + 2023-04-24 + + + https://sa-token.com/blog/essays/sa-token-duo-zhang-hao-ren-zheng-tong-shi-wei-xi-tong-de-admin-zhang-hao-he-user.html + 2023-04-24 + + + https://sa-token.com/blog/essays/sa-token-dan-dian-deng-lu-di-si-pian-kua-yu-kua-redis-xia-de-sso-dui-jie-bu-zhou.html + 2023-04-24 + + + https://sa-token.com/blog/essays/sa-token-dan-dian-deng-lu-di-liu-pian-ding-zhi-hua-deng-lu-ye-mian.html + 2023-04-24 + + + https://sa-token.com/blog/essays/sa-token-dan-dian-deng-lu-di-wu-pian-qian-hou-duan-fen-li-jia-gou-xia-de-sso-dui.html + 2023-04-24 + + + https://sa-token.com/blog/essays/sa-token-dan-dian-deng-lu-di-er-pian-tong-yu-tong-redis-xia-de-sso-dui-jie-bu-zh.html + 2023-04-24 + + + https://sa-token.com/blog/essays/sa-token-dan-dian-deng-lu-di-san-pian-kua-yu-tong-redis-xia-de-sso-dui-jie-bu-zh.html + 2023-04-24 + + + https://sa-token.com/blog/essays/sa-token-dan-dian-deng-lu-di-yi-pian-da-jian-sso-server-tong-yi-ren-zheng-zhong.html + 2023-04-24 + + + https://sa-token.com/blog/release/sa-token-v1-34-0-geng-xin-shi-pei-springboot3.html + 2023-04-24 + + + https://sa-token.com/blog/release/sa-token-v1-33-0-geng-xin-xin-zeng-quan-ju-log-mo-kuai.html + 2023-04-24 + + + https://sa-token.com/blog/release/sa-token-v1-32-0-geng-xin-xin-zeng-zhang-hao-fen-lei-feng-jin-jie-ti-feng-jin-go.html + 2023-04-24 + + + https://sa-token.com/blog/release/sa-token-v1-31-0-geng-xin-xin-zeng-zhang-hao-fen-lei-feng-jin-jie-ti-feng-jin-go.html + 2023-04-24 + + + https://sa-token.com/blog/special/sa-token-v1-28-0-geng-xin-bing-rong-huo-kai-yuan-zhong-guo-ma-yun-gvp-ren-zheng.html + 2023-04-24 + + + https://sa-token.com/blog/release/sa-token-v1-23-0-geng-xin-zheng-shi-zhi-chi-oauth2-0.html + 2023-04-24 + + + https://sa-token.com/blog/release/sa-token-v-1-31-0-xin-zeng-lan-jie-qi-sainterceptor-gong-neng-shuo-ming-yi-ji-ji.html + 2023-04-24 + + diff --git a/sa-token-doc/blog/special/sa-token-di-10000-ge-star-li-cheng-bei-gan-xie-san-nian-lai-suo-you-guan-zhu-sa.html b/sa-token-doc/blog/special/sa-token-di-10000-ge-star-li-cheng-bei-gan-xie-san-nian-lai-suo-you-guan-zhu-sa.html new file mode 100644 index 000000000..2fc733c1b --- /dev/null +++ b/sa-token-doc/blog/special/sa-token-di-10000-ge-star-li-cheng-bei-gan-xie-san-nian-lai-suo-you-guan-zhu-sa.html @@ -0,0 +1,313 @@ + + + + + Sa-Token 第 10000 个star 里程碑,感谢三年来所有关注 Sa-Token 的小伙伴! - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token 第 10000 个star 里程碑,感谢三年来所有关注 Sa-Token 的小伙伴!

+ +

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。

+

就在今天(2022-8-18),Sa-Token 的 GitHub 仓库终于达到第 10000 个 star 里程碑,感谢这位名字 polarisgh 的用户,感谢三年来所有关注 Sa-Token 的小伙伴!

+

github-10k-1.png

+

github-10k-2.png

+

github-10k-3.png

+

Sa-Token 开源大事记:

+
    +
  • 2020-02-04: 在 GitHub 提交第一个版本,正式开源。
  • +
  • 2020-09-14: GitHub star 数量破 100。
  • +
  • 2020-10-26: Gitee star 数量破 100。
  • +
  • 2021-03-01: 被 [HelloGitHub] 第 59 期收录推荐。
  • +
  • 2021-03-26: GitHub star 数量破 1k。
  • +
  • 2021-03-30: 受 TLog 作者邀请,Sa-Token 加入 dromara 社区。
  • +
  • 2021-03-30: 被 Gitee 列为推荐项目。
  • +
  • 2021-03-31: Gitee star 数量破1K。
  • +
  • 2021-06-17: Sa-Token star 数量 (3529) 超过 Shiro (3506)。
  • +
  • 2021-07-26: GitHub star 数量破5K。
  • +
  • 2021-09-24: Sa-Token star 数量 (6280) 超过 SpringSecurity (6247)。
  • +
  • 2021-11-08: 荣获开源中国 “码云 GVP 认证”。
  • +
  • 2021-11-28: Gitee star 数量破5K。
  • +
  • 2021-12-27: 荣获 OSC-2021 最佳软件 Top 30。
  • +
  • 2022-05-20: 成为 [可信开源社区共同体] 预备成员。
  • +
  • 2022-08-01: 加入 [中国开源社区 landscape]。
  • +
  • 2021-08-18: GitHub 第 10000 个 star 里程碑!
  • +
+

Sa-Token 已经迭代共 30 个大版本,下一个版本(v1.31.0)目前正在火速开发中,预计更新的特性这里先卖个关子,大家感兴趣的可以看一下仓库 commit 记录。

+

再次感谢每一位小伙伴的热心支持 ❤️ ❤️ ❤️ !

+
+← Sa-Token v1.28.0更新,并荣获开源中国“码云GVP认证” +太强了!Sa-Token 的 NodeJS 版本! → +
+
+
+
+
+
+
章节目录
+
+
+
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/special/sa-token-v1-28-0-geng-xin-bing-rong-huo-kai-yuan-zhong-guo-ma-yun-gvp-ren-zheng.html b/sa-token-doc/blog/special/sa-token-v1-28-0-geng-xin-bing-rong-huo-kai-yuan-zhong-guo-ma-yun-gvp-ren-zheng.html new file mode 100644 index 000000000..a1c762da0 --- /dev/null +++ b/sa-token-doc/blog/special/sa-token-v1-28-0-geng-xin-bing-rong-huo-kai-yuan-zhong-guo-ma-yun-gvp-ren-zheng.html @@ -0,0 +1,344 @@ + + + + + Sa-Token v1.28.0更新,并荣获开源中国“码云GVP认证” - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

Sa-Token v1.28.0更新,并荣获开源中国“码云GVP认证”

+ +

更新日志

+

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题。

+

时隔近一个月,Sa-Token终于迎来了 v1.28.0 新版本,本次更新主要解决了与 jwt、dubbo 整合的问题:

+
    +
  • 新增:新增 sa-token-jwt 插件,用于与jwt的整合 [重要]
  • +
  • 新增:新增 sa-token-context-dubbo 插件,用于与 Dubbo 的整合 [重要]
  • +
  • 文档:文档新增章节:Sa-Token 插件开发指南 [重要]
  • +
  • 文档:文档新增章节:名称解释
  • +
  • 优化:抽离 getSaTokenDao() 方法,方便重写
  • +
  • 新增:单元测试新增多账号模式数据不互通测试
  • +
  • 优化:优化在线文档,修复部分错误之处
  • +
  • 优化:优化未登录异常抛出提示,标注无效的Token值
  • +
  • 修复:修复单词拼写错误 getDeviceOrDefault
  • +
  • 优化:优化 jwt 集成示例
  • +
  • 文档:新增常见问题总结
  • +
+

另附一个好消息:2021-11-8,Sa-Token 荣获开源中国 “码云最有价值开源项目” GVP 认证。

+

sa-token-gvp1

+

sa-token-gvp2

+

sa-token-gvp3

+

目前功能点

+

Sa-Token 致力于让鉴权变得简单、优雅,目前已经集成:

+
    +
  • 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录
  • +
  • 权限认证 —— 权限认证、角色认证、会话二级认证
  • +
  • Session会话 —— 全端共享Session、单端独享Session、自定义Session
  • +
  • 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线
  • +
  • 账号封禁 —— 指定天数封禁、永久封禁、设定解封时间
  • +
  • 持久层扩展 —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
  • +
  • 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案
  • +
  • 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
  • +
  • 单点登录 —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
  • +
  • OAuth2.0认证 —— 基于RFC-6749标准编写,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集成包,真正的开箱即用
  • +
+

项目地址

+

更多信息可以参考项目地址:https://gitee.com/dromara/sa-token

+
+ +Sa-Token 第 10000 个star 里程碑,感谢三年来所有关注 Sa-Token 的小伙伴! → +
+
+
+
+
+
+
章节目录
+
+
+
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
+ +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/special/tai-qiang-le-sa-token-de-nodejs-ban-ben-2.html b/sa-token-doc/blog/special/tai-qiang-le-sa-token-de-nodejs-ban-ben-2.html new file mode 100644 index 000000000..7a853556f --- /dev/null +++ b/sa-token-doc/blog/special/tai-qiang-le-sa-token-de-nodejs-ban-ben-2.html @@ -0,0 +1,862 @@ + + + + + 太强了!Sa-Token 的 NodeJS 版本! - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+

太强了!Sa-Token 的 NodeJS 版本!

+ +

这是由 Sa-Token 社区成员发起的一个项目:xlt-token —— sa-token 的 NodeJS 版本。

+

对 NodeJS / TypeScript 熟悉的同学速来围观啦。

+

目前该仓库已完成 1.0.0-rc.1 版本发布,完成度相当不错,单测覆盖率 98%+,195 个测试用例全部通过。源码全部使用 TypeScript 编写,类型定义完整无缺。

+

开源地址:https://github.com/xiaoLangtou/xlt-token

+
+

📱 加群交流:作者已建好 xlt-token 交流群,欢迎大家扫码加入,一起讨论、提需求、报 Bug~

+
+

xlt-token 交流群

+

一个为 NestJS 设计的轻量级、功能完备的 Token 认证授权框架:

+

✨ 核心特性

+
    +
  • 🔐 灵活的 Token 管理 — 支持登录、登出、续签、踢人下线等完整生命周期
  • +
  • 🌐 多端登录 — 支持同账号多设备同时在线,可配置互踢模式
  • +
  • 🎨 Token 策略 — 支持多种 token 格式(UUID、Simple UUID、随机字符串)
  • +
  • 🛡️ 全局守卫 — 黑名单 / 白名单双模式,默认安全
  • +
  • 🧩 可扩展守卫 — 提供 XltAbstractLoginGuard 抽象基类,通过 onAuthSuccess / onAuthFail 钩子注入业务会话
  • +
  • 🎯 声明式装饰器@XltIgnore / @XltCheckLogin / @LoginId / @TokenValue / @XltCheckPermission / @XltCheckRole
  • +
  • 🔑 权限 / 角色校验StpPermLogic 引擎,支持 AND / OR 模式 + 通配符匹配(user:*
  • +
  • 🗂️ 会话对象XltSession 承载一次登录期间的扩展数据,与 token 同生命周期
  • +
  • 📜 下线追溯 — 被踢 / 被顶后可查询下线时间和原因
  • +
  • 💾 内置存储 — 内置内存存储和 Redis 存储实现,开箱即用
  • +
  • 🔧 零业务依赖 — 纯粹的认证库,不依赖任何业务代码
  • +
  • 📦 TypeScript — 完整的类型定义
  • +
  • 静态门面 — 提供 StpUtil 静态方法,无需注入即可使用
  • +
  • 🧪 质量保障 — 195 个测试用例(158 单测 + 37 E2E),单测覆盖率 98%+
  • +
+

📦 项目结构

+
xlt-token/
+├── src/
+│   ├── auth/                          # 认证核心逻辑
+│   │   ├── stp-logic.ts               # Token 生命周期管理(登录、登出、踢人、续签)
+│   │   └── stp-util.ts                # 静态门面 StpUtil
+│   ├── core/
+│   │   └── xlt-token-config.ts        # 配置定义
+│   ├── perm/                          # 权限 / 角色校验
+│   │   ├── stp-interface.ts           # 业务接口(权限列表、角色列表)
+│   │   ├── stp-perm-logic.ts          # 校验引擎(AND/OR + 通配符)
+│   │   └── perm-pattern-match.ts      # 通配符匹配引擎
+│   ├── session/
+│   │   └── xlt-session.ts             # 会话管理
+│   ├── guards/                        # 守卫层
+│   │   ├── xlt-token.guard.ts         # 全局 Token 守卫
+│   │   └── xlt-abstract-login.guard.ts # 可扩展登录守卫基类
+│   ├── decorators/                    # 声明式装饰器
+│   │   ├── xlt-ignore.decorator.ts
+│   │   ├── xlt-check-login.decorator.ts
+│   │   ├── login-id.decorator.ts
+│   │   ├── token-value.decorator.ts
+│   │   ├── xlt-check-permission.decorator.ts
+│   │   └── xlt-check-role.decorator.ts
+│   ├── store/                         # 存储后端
+│   │   ├── xlt-token-store.interface.ts # 存储接口
+│   │   ├── memory-store.ts            # 内存存储(开发环境)
+│   │   └── redis-store.ts             # Redis 存储(生产环境)
+│   ├── token/                         # Token 生成策略
+│   │   ├── token-strategy.interface.ts
+│   │   └── uuid-strategy.ts
+│   ├── exceptions/                    # 异常定义
+│   │   ├── not-login.exception.ts
+│   │   ├── not-permission.exception.ts
+│   │   └── not-role.exception.ts
+│   ├── const/                         # 常量定义
+│   │   └── index.ts
+│   ├── index.ts                       # 包入口
+│   └── xlt-token.module.ts            # NestJS 模块入口
+├── test/                              # 测试用例
+│   ├── unit/                          # 158 个单元测试
+│   └── e2e/                           # 37 个 E2E 测试
+├── docs/                              # 📖 在线文档
+├── package.json
+└── tsconfig.json
+
+

🎯 核心组件

+

1. StpLogic

+

核心认证授权逻辑:

+
    +
  • Token 生成、验证、续签和刷新
  • +
  • Session 管理
  • +
  • 权限和角色检查
  • +
  • 多端登录、互踢模式
  • +
  • 下线原因追溯
  • +
+

2. StpUtil(静态门面)

+

提供无需注入即可直接调用的静态 API:

+
import { StpUtil } from 'xlt-token';
+
+// 无需注入,直接调用
+const token = await StpUtil.login(userId);
+const loginId = await StpUtil.getLoginId(req);
  • 1
  • 2
  • 3
  • 4
  • 5
+

3. 声明式装饰器

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
装饰器作用参数
@XltIgnore()忽略登录校验(黑名单模式下放行)
@XltCheckLogin()强制校验登录(白名单模式下开启)
@LoginId()注入当前登录用户 ID(参数装饰器)
@TokenValue()注入当前 token 值(参数装饰器)
@XltCheckPermission(perms, options?)校验权限perms: string \| string[]options?: { mode: XltMode }
@XltCheckRole(roles, options?)校验角色roles: string \| string[]options?: { mode: XltMode }
+

4. 可扩展守卫(XltAbstractLoginGuard)

+

如果你需要在校验通过后把用户信息加载到 request.user、记录审计日志、使用自己的元数据键(如 @RequireLogin()),请继承 XltAbstractLoginGuard,仅重写你关心的钩子即可。token 校验、异常抛出、默认元数据解析已在基类完成。

+

生命周期:

+
canActivate
+ ├─ requiresLogin(ctx)           // 可重写:替换元数据策略
+ │   └─ 否 → 直接放行
+ ├─ stpLogic.checkLogin(request)
+ ├─ !ok → onAuthFail(result, request)  // 可重写
+ │   throw NotLoginException
+ └─ ok → request.stpLoginId / stpToken 赋值
+     → onAuthSuccess(result, request)  // 可重写:业务会话加载
+
+

5. 权限 / 角色校验引擎

+

StpPermLogic 支持 AND / OR 两种模式,以及通配符匹配(user:* 可匹配 user:readuser:write 等)。

+
// AND 模式:全部满足
+@XltCheckPermission(['order:read', 'order:write'], { mode: XltMode.AND })
+
+// OR 模式:任一满足
+@XltCheckRole(['admin', 'super'], { mode: XltMode.OR })
+
+// 通配符匹配
+@XltCheckPermission('order:*')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
+

🚀 快速开始

+

1. 安装

+
pnpm add xlt-token
+# 或
+npm install xlt-token
+# 或
+yarn add xlt-token
  • 1
  • 2
  • 3
  • 4
  • 5
+

如需使用 Redis 存储,还需安装 redis 包:

+
pnpm add redis
  • 1
+

2. 注册模块

+
// app.module.ts
+import { Module } from '@nestjs/common';
+import { XltTokenModule } from 'xlt-token';
+
+@Module({
+  imports: [
+    XltTokenModule.forRoot({
+      isGlobal: true,
+      config: {
+        tokenName: 'authorization',
+        timeout: 2592000,      // 30 天
+        tokenStyle: 'uuid',
+        tokenPrefix: 'Bearer ',
+      },
+    }),
+  ],
+})
+export class AppModule {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
+

3. 用户登录

+
// auth.service.ts
+import { Injectable } from '@nestjs/common';
+import { StpLogic } from 'xlt-token';
+
+@Injectable()
+export class AuthService {
+  constructor(private readonly stpLogic: StpLogic) {}
+
+  async login(userId: string) {
+    const token = await this.stpLogic.login(userId);
+    return { token };
+  }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

4. 使用守卫 + 装饰器

+
// user.controller.ts
+import { Controller, Get, Post } from '@nestjs/common';
+import { XltIgnore, LoginId } from 'xlt-token';
+
+@Controller('user')
+export class UserController {
+  @XltIgnore()  // 忽略登录校验
+  @Post('login')
+  async login() {
+    // 登录逻辑
+  }
+
+  @Get('profile')
+  async getProfile(@LoginId() loginId: string) {
+    return { userId: loginId };
+  }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
+

5. 注册全局守卫

+
// app.module.ts
+import { APP_GUARD } from '@nestjs/core';
+import { XltTokenGuard } from 'xlt-token';
+
+@Module({
+  providers: [
+    {
+      provide: APP_GUARD,
+      useClass: XltTokenGuard,
+    },
+  ],
+})
+export class AppModule {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
+

XltTokenGuard 只做 token 校验并把 loginId / token 挂到 request.stpLoginId / request.stpToken,不涉及业务。

+

6. 权限 / 角色校验

+
// stp.service.ts — 实现 StpInterface 业务接口
+import { Injectable } from '@nestjs/common';
+import { StpInterface } from 'xlt-token';
+
+@Injectable()
+export class StpService implements StpInterface {
+  async getPermissionList(loginId: string): Promise<string[]> {
+    // 从数据库 / 缓存读取
+    return ['user:read', 'user:write', 'order:*'];
+  }
+
+  async getRoleList(loginId: string): Promise<string[]> {
+    return ['admin'];
+  }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+
// 在 Controller 上使用
+import { XltCheckPermission, XltCheckRole, XltMode } from 'xlt-token';
+
+@Controller('order')
+export class OrderController {
+  @XltCheckPermission('order:read')                     // 单一权限
+  @Get()
+  list() {}
+
+  @XltCheckPermission(['order:read', 'order:write'],
+    { mode: XltMode.AND })                              // 全部满足
+  @Post()
+  create() {}
+
+  @XltCheckRole(['admin', 'super'],
+    { mode: XltMode.OR })                               // 任一满足
+  @Delete(':id')
+  remove() {}
+
+  @XltCheckPermission('order:*')                        // 通配符匹配
+  @Patch(':id')
+  update() {}
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
+

权限校验失败抛出 NotPermissionException(HTTP 403),角色校验失败抛出 NotRoleException(HTTP 403)。

+

🗂️ 会话管理(XltSession)

+

每个登录账号关联一个 XltSession,用于存储登录期间的扩展数据(昵称、最近 IP、扩展字段等)。生命周期与 token 一致。

+
import { StpUtil } from 'xlt-token';
+
+// 写入
+const session = StpUtil.getSession(loginId);
+await session.set('nickname', 'xlt');
+await session.set('lastLoginIp', '127.0.0.1');
+
+// 读取
+const nickname = await session.get<string>('nickname');
+
+// 其他方法
+await session.has('nickname');    // boolean
+await session.remove('nickname');
+const keys = await session.keys(); // string[]
+await session.clear();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
+

📜 下线原因追溯

+

被踢 / 被顶后,旧 token 失效。可查询下线原因:

+
const record = await StpUtil.getOfflineReason(token);
+// { reason: 'KICK_OUT' | 'BE_REPLACED', time: 1714112400000 }
  • 1
  • 2
+

⚠️ 异常处理

+

库提供三种业务异常,建议在全局 ExceptionFilter 中统一处理:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
异常HTTP 状态触发场景
NotLoginException401未登录 / token 无效 / 被顶 / 被踢 / 冻结 / 超时
NotPermissionException403@XltCheckPermission 校验失败
NotRoleException403@XltCheckRole 校验失败
+

NotLoginException 提供了 NotLoginType 常量用于区分登录失败场景:

+
import { NotLoginException, NotLoginType } from 'xlt-token';
+
+try {
+  await stpLogic.checkLogin(req);
+} catch (e) {
+  if (e instanceof NotLoginException) {
+    switch (e.message) {
+      case NotLoginType.NOT_TOKEN:      // 请求中没 token
+        break;
+      case NotLoginType.INVALID_TOKEN:  // token 在服务端找不到
+        break;
+      case NotLoginType.TOKEN_TIMEOUT:  // token 已过期
+        break;
+      case NotLoginType.TOKEN_FREEZE:   // 临时活跃过期
+        break;
+      case NotLoginType.BE_REPLACED:    // 被顶号
+        break;
+      case NotLoginType.KICK_OUT:       // 被踢下线
+        break;
+    }
+  }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
+

💾 使用 Redis 存储

+

库已内置 RedisStore 实现,只需提供 Redis 客户端即可使用:

+
import { Module } from '@nestjs/common';
+import { XltTokenModule, RedisStore, XLT_REDIS_CLIENT } from 'xlt-token';
+import { createClient } from 'redis';
+
+@Module({
+  imports: [
+    XltTokenModule.forRoot({
+      store: { useClass: RedisStore },
+      providers: [
+        {
+          provide: XLT_REDIS_CLIENT,
+          useFactory: async () => {
+            const client = createClient({
+              url: 'redis://localhost:6379',
+            });
+            await client.connect();
+            return client;
+          },
+        },
+      ],
+    }),
+  ],
+})
+export class AppModule {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
+

🔧 自定义 Store

+

如需实现自定义存储,实现 XltTokenStore 接口即可:

+
import { XltTokenStore } from 'xlt-token';
+
+export class CustomStore implements XltTokenStore {
+  async get(key: string): Promise<string | null> { /* ... */ }
+  async set(key: string, value: string, timeoutSec: number): Promise<void> { /* ... */ }
+  async delete(key: string): Promise<void> { /* ... */ }
+  async has(key: string): Promise<boolean> { /* ... */ }
+  async update(key: string, value: string): Promise<void> { /* ... */ }
+  async updateTimeout(key: string, timeoutSec: number): Promise<void> { /* ... */ }
+  async getTimeout(key: string): Promise<number> { /* ... */ }
+}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
+

🎛️ 配置选项

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
字段类型默认值说明
tokenNamestring'authorization'HTTP header / cookie / query 中读取 token 的键名
timeoutnumber2592000(30 天)token 有效期(秒)
activeTimeoutnumber-1滑动过期秒数,-1 表示不启用
isConcurrentbooleantrue是否允许同账号多端同时在线
isSharebooleantrue同账号多次登录是否共享同一 token
tokenStyle'uuid' \| 'simple-uuid' \| 'random-32''uuid'token 格式
isReadHeaderbooleantrue是否从 HTTP Header 读取 token
isReadCookiebooleanfalse是否从 Cookie 读取
isReadQuerybooleanfalse是否从 URL Query 读取
tokenPrefixstring'Bearer 'Header 中 token 的前缀(读取时自动剥离)
defaultCheckbooleantrue全局守卫默认模式:true=黑名单,false=白名单
+

📖 核心 API

+

StpLogic / StpUtil

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方法参数返回值说明
login(loginId, options?)loginId: string \| numberoptions: { timeout?, device?, token? }Promise<string>登录,返回 token
logout(token)token: stringPromise<boolean \| null>登出(通过 token)
logoutByLoginId(loginId)loginId: stringPromise<boolean \| null>登出(通过 loginId)
kickout(loginId)loginId: stringPromise<boolean \| null>踢人下线
renewTimeout(token, timeout)token: stringtimeout: numberPromise<boolean \| null>续签 token
isLogin(req)req: RequestPromise<boolean>判断是否登录
checkLogin(req)req: RequestPromise<{ ok, loginId?, token?, reason? }>校验登录(未登录抛异常)
getTokenValue(req)req: RequestPromise<string \| null>获取 token 值
getLoginId(req)req: RequestPromise<string>获取当前登录用户 ID
getSession(loginId)loginId: stringXltSession获取会话对象
getOfflineReason(token)token: stringPromise<{ reason, time }>获取下线原因
+

📣 小结

+

xlt-token 作为 Sa-Token 在 NodeJS / NestJS 生态的移植版本,在 API 设计上非常接近 Java 原版的使用体验,同时充分利用了 TypeScript 的类型系统和 NestJS 的装饰器 / 守卫机制,做到了大道至简、开箱即用。

+

对于使用 NestJS 开发后端项目、又需要一套成熟的认证授权方案的小伙伴来说,这是一个非常值得关注的项目。195 个测试用例 + 98% 的单测覆盖率,质量方面也让人放心 👍

+
+

🔗 开源地址

+

感兴趣的同学可以来关注一波:

+

👉 https://github.com/xiaoLangtou/xlt-token

+

📖 在线文档:https://xiaolangtou.github.io/xlt-token/

+
+
+

📱 还没有加群? 扫下方二维码加入 xlt-token 交流群,和作者面对面交流~

+
+

xlt-token 交流群

+
+← Sa-Token 第 10000 个star 里程碑,感谢三年来所有关注 Sa-Token 的小伙伴! +
+
+
+
+ +
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/template/blog-template.html b/sa-token-doc/blog/template/blog-template.html new file mode 100644 index 000000000..0a07fd6d7 --- /dev/null +++ b/sa-token-doc/blog/template/blog-template.html @@ -0,0 +1,83 @@ + + + + + {{title}} - Sa-Token 博客 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {{docNav}} + +
+ {{blogSidebar}} +
+
+
+

{{title}}

+ {{articleMeta}} + {{content}} + {{prevNext}} +
+
+
+
+
+
章节目录
+
{{articleToc}}
+
+ {{blogRightAside}} +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/blog/template/index-template.html b/sa-token-doc/blog/template/index-template.html new file mode 100644 index 000000000..47ae8f601 --- /dev/null +++ b/sa-token-doc/blog/template/index-template.html @@ -0,0 +1,68 @@ + + + + + {{title}} + + + + + + + + + + + + + + + + + + + + + + + + {{docNav}} + +
+
+
+
+ {{indexContent}} +
+
+
+
+
+
博客说明
+
+

本站官方博客开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+

社区博客和推荐博客版权属于其作者本人,如需转载请联系对应作者申请授权。

+
+
+ {{blogRightAside}} +
+
+ +
+
+ Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | + 鲁ICP备18046274号-5 | + + 鲁公网安备37011202002956号 + +
+
+ + + + + + + + + diff --git a/sa-token-doc/blog/template/partials/blog-right-aside-article.html b/sa-token-doc/blog/template/partials/blog-right-aside-article.html new file mode 100644 index 000000000..00d4d430e --- /dev/null +++ b/sa-token-doc/blog/template/partials/blog-right-aside-article.html @@ -0,0 +1,23 @@ +
+ +
+

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

+
+
diff --git a/sa-token-doc/blog/template/partials/blog-right-aside-index.html b/sa-token-doc/blog/template/partials/blog-right-aside-index.html new file mode 100644 index 000000000..3e1179fd5 --- /dev/null +++ b/sa-token-doc/blog/template/partials/blog-right-aside-index.html @@ -0,0 +1,31 @@ +
+ + +
+

如果 Sa-Token 帮助到了你,希望你可以向同事、朋友推荐了解本框架,这对我们非常重要,感谢支持!

+

加油,工程师!

+
+
diff --git a/sa-token-doc/blog/template/partials/doc-nav.html b/sa-token-doc/blog/template/partials/doc-nav.html new file mode 100644 index 000000000..2087a1d45 --- /dev/null +++ b/sa-token-doc/blog/template/partials/doc-nav.html @@ -0,0 +1,96 @@ + diff --git a/sa-token-doc/doc.html b/sa-token-doc/doc.html new file mode 100644 index 000000000..a4ddb09ae --- /dev/null +++ b/sa-token-doc/doc.html @@ -0,0 +1,510 @@ + + + + + Sa-Token 文档 + + + + + + + + + + + + + + + + + + + + + +
+ +
加载中...
+ + +
+
目录
+
+ + + + + + + + + +
+

如果 Sa-Token 帮助到了你,希望你可以向同事、朋友推荐了解本框架,这对我们非常重要,感谢支持!

+

加油,工程师!

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/doc/README.md b/sa-token-doc/doc/README.md deleted file mode 100644 index c90340524..000000000 --- a/sa-token-doc/doc/README.md +++ /dev/null @@ -1,212 +0,0 @@ -

- logo -

-

Sa-Token v1.24.0

-

这可能是史上功能最全的 Java 权限认证框架!

-

- - - - - - - -

- ---- - - -## 在线资料 - -- [在线文档:http://sa-token.dev33.cn/](http://sa-token.dev33.cn/) - -- [需求提交:我们深知一个优秀的项目需要海纳百川,点我在线提交需求](http://sa-app.dev33.cn/wall.html?name=sa-token) - -- [开源不易,求鼓励,点个star吧 !](###) - - -## Sa-Token 介绍 -Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题 - -框架集成简单、开箱即用、API设计清爽,通过Sa-Token,你将以一种极其简单的方式实现系统的权限认证部分 - -- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录 -- **权限认证** —— 权限认证、角色认证、会话二级认证 -- **Session会话** —— 全端共享Session、单端独享Session、自定义Session -- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线 -- **账号封禁** —— 指定天数封禁、永久封禁、设定解封时间 -- **持久层扩展** —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失 -- **分布式会话** —— 提供jwt集成、共享数据中心两种分布式会话方案 -- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证 -- **单点登录** —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定 -- **OAuth2.0认证** —— 基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式 -- **二级认证** —— 在已登录的基础上再次认证,保证安全性 -- **独立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 功能结构图 -![sa-token-js](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/x/sa-token-js3.png 's-w') - -##### Sa-Token 认证流程图 -![sa-token-rz](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/x/sa-token-rz2.png 's-w') - - -## Sa-Token-SSO 单点登录 -对于单点登录,网上教程大多以CAS模式为主,其实对于不同的系统架构,实现单点登录的步骤也大为不同,Sa-Token由简入难将其划分为三种模式: - -| 系统架构 | 采用模式 | 简介 | 文档链接 | -| :-------- | :-------- | :-------- | :-------- | -| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie同步会话 | [文档](http://sa-token.dev33.cn/doc/index.html#/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso1) | -| 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [文档](http://sa-token.dev33.cn/doc/index.html#/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso2-server) | -| 前端不同域 + 后端 不同Redis | 模式三 | Http请求获取会话 | [文档](http://sa-token.dev33.cn/doc/index.html#/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso3-server) | - - -1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com` -2. 后端同Redis:就是指多个系统可以连接同一个Redis,其它的缓存数据中心亦可。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案,详情戳:[Alone独立Redis插件](http://sa-token.dev33.cn/doc/index.html#/plugin/alone-redis) -3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成) -4. 技术选型一定要根据系统架构对症下药,切不可胡乱选择 - - -## Sa-Token-SSO 特性 -1. API简单易用,文档介绍详细,且提供直接可用的集成示例 -2. 支持三种模式,不论是否跨域、是否共享Redis,都可以完美解决 -3. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝`Ticket劫持`、`Token窃取`等常见攻击手段(文档讲述攻击原理和防御手段) -4. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:`http://a.com?id=1&name=2`,登录成功之后就变成了:`http://a.com?id=1`,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面 -5. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合…… -6. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发 - - -## Sa-Token-OAuth2.0 授权登录 -Sa-OAuth2 模块基于 [RFC-6749 标准](https://tools.ietf.org/html/rfc6749) 编写,通过Sa-OAuth2你可以非常轻松的实现系统的OAuth2.0授权认证 - -1. 授权码(Authorization Code):OAuth2.0标准授权步骤,Server端向Client端下放Code码,Client端再用Code码换取授权Token -2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server端使用URL重定向方式直接将Token下放到Client端页面 -3. 密码式(Password):Client直接拿着用户的账号密码换取授权Token -4. 客户端凭证(Client Credentials):Server端针对Client级别的Token,代表应用自身的资源授权 - -详细参考文档:[http://sa-token.dev33.cn/doc/index.html#/oauth2/readme](http://sa-token.dev33.cn/doc/index.html#/oauth2/readme) - - -## 代码示例 - -Sa-Token的API调用非常简单,有多简单呢?以登录验证为例,你只需要: - -``` java -// 在登录时写入当前会话的账号id -StpUtil.login(10001); - -// 然后在任意需要校验登录处调用以下API -// 如果当前会话未登录,这句代码会抛出 `NotLoginException`异常 -StpUtil.checkLogin(); -``` -至此,我们已经借助Sa-Token框架完成登录授权! - -此时的你小脑袋可能飘满了问号,就这么简单?自定义Realm呢?全局过滤器呢?我不用写各种配置文件吗? - -事实上在此我可以负责的告诉你,在Sa-Token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权! - -当你受够Shiro、Spring Security等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token的API设计是多么的清爽! - -权限认证示例 (只有具有`user:add`权限的会话才可以进入请求) -``` java -@SaCheckPermission("user:add") -@RequestMapping("/user/insert") -public String insert(SysUser user) { - // ... - return "用户增加"; -} -``` - -将某个账号踢下线 (待到对方再次访问系统时会抛出`NotLoginException`异常) -``` java -// 使账号id为10001的会话注销登录 -StpUtil.logoutByLoginId(10001); -``` - -除了以上的示例,Sa-Token还可以一行代码完成以下功能: -``` java -StpUtil.login(10001); // 标记当前会话登录的账号id -StpUtil.getLoginId(); // 获取当前会话登录的账号id -StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false -StpUtil.logout(); // 当前会话注销登录 -StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线) -StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false -StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false -StpUtil.getSession(); // 获取当前账号id的Session -StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session -StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值 -StpUtil.login(10001, "PC"); // 指定设备标识登录 -StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响) -StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号 -``` -Sa-Token API 众多,请恕此处无法为您逐一展示,更多示例请戳官方在线文档 - - -## Star 趋势 -[![giteye-chart](https://chart.giteye.net/gitee/dromara/sa-token/77YQZ6UK.png 'Gitee')](https://giteye.net/chart/77YQZ6UK) - -[![github-chart](https://starchart.cc/dromara/sa-token.svg 'GitHub')](https://starchart.cc/dromara/sa-token) - - - - -## 使用Sa-Token的开源项目 -[**[ sa-plus ]** 一个基于springboot架构的快速开发框架,内置代码生成器](https://gitee.com/click33/sa-plus) - -[**[ jthink ]** 一个基于springboot+sa-token+thymeleaf的博客系统](https://gitee.com/wtsoftware/jthink) - -[**[ dcy-fast ]** 一个基于springboot+sa-token+mybatis-plus的后台管理系统,前端vue-element-admin,并且内置代码生成器](https://gitee.com/dcy421/dcy-fast) - -[**[ helio-starters ]** 基于JDK15 + Spring Boot 2.4 + Sa-Token + Mybatis-Plus的单体Boot版脚手架和微服务Cloud版脚手架,带有配套后台管理前端模板及代码生成器](https://gitee.com/uncarbon97/helio-starters) - -如果您的项目使用了Sa-Token,欢迎提交pr - - -## 友情链接 -[**[ okhttps ]** 一个轻量级http通信框架,API设计无比优雅,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps) - -[**[ 小诺快速开发平台 ]** 基于SpringBoot2 + AntDesignVue全新快速开发平台,同时拥有三个版本](https://xiaonuo.vip/index#pricing) - -[**[ Jpom ]** 简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件](https://gitee.com/dromara/Jpom) - -[**[ TLog ]** 一个轻量级的分布式日志标记追踪神器](https://gitee.com/dromara/TLog) - - -## 交流群 -QQ交流群:1002350610 [点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM) - -微信交流群: - -![微信群](https://dev33-test.oss-cn-beijing.aliyuncs.com/sa-token/i-wx-qr.png ':size=230') - -(扫码添加微信,备注:sa-token,邀您加入群聊) - -
diff --git a/sa-token-doc/doc/_sidebar.md b/sa-token-doc/doc/_sidebar.md deleted file mode 100644 index ee6e31619..000000000 --- a/sa-token-doc/doc/_sidebar.md +++ /dev/null @@ -1,82 +0,0 @@ - - -- **开始** - - [介绍](/README) - - [集成](/start/download) - - [在SpringBoot环境集成](/start/example) - - [在WebFlux环境集成](/start/webflux-example) - -- **基础** - - [登录认证](/use/login-auth) - - [权限认证](/use/jur-auth) - - [踢人下线](/use/kick) - - [注解式鉴权](/use/at-check) - - [路由拦截鉴权](/use/route-check) - - [Session会话](/use/session) - - [框架配置](/use/config) - -- **深入** - - [集成Redis](/up/integ-redis) - - [前后台分离](/up/not-cookie) - - [自定义Token风格](/up/token-style) - - [自定义Token前缀](/up/token-prefix) - - [记住我模式](/up/remember-me) - - [模拟他人 & 身份切换](/up/mock-person) - - [同端互斥登录](/up/mutex-login) - - [二级认证](/up/safe-auth) - - [密码加密](/up/password-secure) - - [会话治理](/up/search-session) - - [全局侦听器](/up/global-listener) - - [全局过滤器](/up/global-filter) - - [多账号验证](/up/many-account) - - -- **单点登录** - - [单点登录简述](/sso/readme) - - [SSO模式一 共享Cookie同步会话](/sso/sso-type1) - - [SSO模式二 URL重定向传播会话](/sso/sso-type2) - - [SSO模式三 Http请求获取会话](/sso/sso-type3) - - [SSO整合-常见问题总结](/sso/sso-cd) - -- **OAuth2.0** - - [OAuth2.0简述](/oauth2/readme) - - [OAuth2-Server搭建](/oauth2/oauth2-server) - - [OAuth2-Server端-API列表](/oauth2/oauth2-api) - - [OAuth2-二次开发说明](/oauth2/oauth2-dev) - -- **微服务** - - [分布式Session会话](/micro/dcs-session) - - [网关统一鉴权](/micro/gateway-auth) - - [内部服务外网隔离](/micro/id-token) - - [依赖引入说明](/micro/import-intro) - -- **插件** - - [AOP注解鉴权](/plugin/aop-at) - - [临时Token验证](/plugin/temp-token) - - [Quick-Login快速登录插件](/plugin/quick-login) - - [Alone独立Redis插件](/plugin/alone-redis) - - [持久层扩展](/plugin/dao-extend) - -- **其它** - - [更新日志](/more/update-log) - - [友情链接](/more/link) - - [推荐公众号](/more/tj-gzh) - -- **附录** - - [常用类、方法](/more/common-action) - - [常见问题排查](/more/common-questions) - - [Sa-Token功能结构图](/fun/auth-flow) - - [未登录场景值详解](/fun/not-login-scene) - - [Token有效期详解](/fun/token-timeout) - - [Session模型详解](/fun/session-model) - - [TokenInfo参数详解](/fun/token-info) - - [框架源码所有技术栈](/fun/tech-stack) - - [为Sa-Token贡献代码](/fun/git-pr) - - [Sa-Token框架掌握度--在线考试](/fun/sa-token-test) - - - - - -













-

----- 到底线了 -----

\ No newline at end of file diff --git a/sa-token-doc/doc/fun/auth-flow.md b/sa-token-doc/doc/fun/auth-flow.md deleted file mode 100644 index 8ec1048a2..000000000 --- a/sa-token-doc/doc/fun/auth-flow.md +++ /dev/null @@ -1,15 +0,0 @@ -# Sa-Token 功能结构图 - ---- - -### Sa-Token 功能结构图: - -![sa-token-rz](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/x/sa-token-js3.png 's-w') - -### Sa-Token 认证流程图: - -![sa-token-rz](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/x/sa-token-rz2.png 's-w') - - - -PS:鼠标右键选择 **`[在新窗口打开图片]`** 即可高清模式查看图片。(本流程图使用 [ProcessOn](https://www.processon.com) 绘制完成) diff --git a/sa-token-doc/doc/fun/git-pr.md b/sa-token-doc/doc/fun/git-pr.md deleted file mode 100644 index 1a9bc36bf..000000000 --- a/sa-token-doc/doc/fun/git-pr.md +++ /dev/null @@ -1,102 +0,0 @@ -# 如何更新在线文档 -1. 打开要修改的文档页面 -2. 滑动右侧页面滑块, 查看页面内容最下方, 评论区上方 -3. 找到这一行文字 - -![在线编辑提示](https://oss.dev33.cn/sa-token/doc/git-pr/online_1.png) - -4. 点击Gitee或GitHub按钮中的任意一个, 国内用户推荐使用 [Gitee](https://gitee.com) (请先注册登录后再往下浏览) -5. 此时会进入当前页面源码预览页面,找到下方按钮组 - -![按钮组](https://oss.dev33.cn/sa-token/doc/git-pr/online_2.png) - -6. 点击编辑按钮 -7. 此时进入待修改页面的源码页面, 按照markdown格式编辑为需要的结果(Ctrl+P可查看最终效果,再次按下可恢复源码界面) -8. 滑动到最下方点击提交审核即可 - - -# 如何提交代码 -## 环境安装过程 -1. 在本地[下载Git软件](https://pc.qq.com/detail/13/detail_22693.html)并安装 -2. 配置用户名和邮件地址(Gitee或GitHub上关联的邮箱) - -``` -git config --global user.name "这里替换为你在项目中希望展示的昵称" -git config --global user.email 这里替换为你的关联邮箱 -// 查看是否配置正确 -git config --list -``` - -3. 为了让Gitee服务器认可你的身份,需要配置一次SSH Key, 在本地生成密匙对, 公钥上传到Gitee服务器后台 -4. 具体方法见[Gitee如何配置SSH](https://gitee.com/help/articles/4181#article-header0), [Github如何配置SSH](https://docs.github.com/cn/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account) -5. 最小开发环境安装包括[Java JDK 8+](https://pc.qq.com/detail/0/detail_18360.html),[Maven 最新版](http://maven.apache.org/download.cgi) 和[idea IDE 社区版](https://www.jetbrains.com/zh-cn/idea/download/#section=windows) -6. 在idea 中[配置Java环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEjava%E7%8E%AF%E5%A2%83)和[配置maven环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEmaven%E7%8E%AF%E5%A2%83), 基础部分不再赘述 - -## 项目下载过程 -1. 点击[Gitee](https://gitee.com/dromara/sa-token)或[Github](https://github.com/dromara/sa-token)进入Sa-Token项目主页, 以下以Gitee为例,Github类似(请先注册登录后再往下浏览) -2. 找到页面右上角的按钮组, 点击Forked按钮 - -![按钮组](https://oss.dev33.cn/sa-token/doc/git-pr/code_1.png) - -3. 选择个人仓库并点击确认 -4. 此时在你的个人仓库中会多了一个Sa-Token项目 -5. 在新的Sa-Token项目中, 点击 ![克隆/下载](https://oss.dev33.cn/sa-token/doc/git-pr/code_2.png) 按钮, 点击弹出框里面的复制按钮 -6. 在本地某空文件夹下右键选择: git bash here - -![git bash](https://oss.dev33.cn/sa-token/doc/git-pr/code_4.png) - -![git bash 打开后的图](https://oss.dev33.cn/sa-token/doc/git-pr/code_3.png) - -14. 在里面输入如下命令, 按换行后自动下载整个项目 - -``` -git clone 这里替换为复制后的链接 -``` - -## 项目载入过程 -1. 下载结束后, 开启 idea, 选择 File->Open... 选中项目下载后的Sa-Token文件夹(Trust Project 相信此项目, 否则不可编辑) -2. 这时项目就是可编辑状态, 修改完代码并测试完成后即可提交 - -## 项目暂存过程 -1. 在idea中打开项目进入Commit选项 - -![本地暂存](https://oss.dev33.cn/sa-token/doc/git-pr/code_5.png) - -2. 勾选需要本地暂存的文件 -3. 在同一页面的下方输入提示信息 - -![提示信息](https://oss.dev33.cn/sa-token/doc/git-pr/code_6.png) - -4. 点击Commit按钮暂存到本地, 点击Commit and Push按钮暂存之后提交到远程 - -## 项目提交远程 -1. 除了点击Commit and Push按钮外,还有一个地方可以提交git - -![git按钮](https://oss.dev33.cn/sa-token/doc/git-pr/code_7.png) - -2. 位置在idea右上方的工具栏里面 -3. 指向左下箭头为拉取项目,可以随时更新 -4. 打对号为本地暂存 -5. 指向右上箭头提交远程 -6. 提交后进入Gitee个人仓库中的Sa-Token项目 -7. 找到下图的Pull Request按钮 - -![工具栏](https://oss.dev33.cn/sa-token/doc/git-pr/code_8.png) - -8. 点击提交, 进入如下页面 - -![提交信息填写页面](https://oss.dev33.cn/sa-token/doc/git-pr/code_9.png 's-width') - -9. 在这里,你可以选择要提交的分支,一般都是dev开发分支.可以填写合并信息,其他测试审查之类的可以不填写, 最后点击创建即可完成一次提交. - -## 远程项目更新 -1. 有时候主项目更新了,之前克隆的项目代码陈旧,如何处理? -2. 在个人仓库的Sa-Token项目主页面中, 找到下图的圆圈 - -![更新按钮](https://oss.dev33.cn/sa-token/doc/git-pr/code_10.png) - -3. 点击右侧圆圈按钮后Gitee会自动同步主项目, 这样就不用向我之前一样,删除项目又重新fork了. - -## 为什么在国内推荐Gitee -1. 近期Github下载网速较慢 -2. Gitee上中文界面方便操作 \ No newline at end of file diff --git a/sa-token-doc/doc/fun/not-login-scene.md b/sa-token-doc/doc/fun/not-login-scene.md deleted file mode 100644 index 9fbf766da..000000000 --- a/sa-token-doc/doc/fun/not-login-scene.md +++ /dev/null @@ -1,60 +0,0 @@ -# NotLoginException 场景值 - -本篇介绍如何根据`NotLoginException`异常的场景值,来定制化处理未登录的逻辑
-应用场景举例:未登录、被顶下线、被踢下线等场景需要不同方式来处理 - - -## 何为场景值 -在前面的章节中,我们了解到,在会话未登录的情况下尝试获取`loginId`会使框架抛出`NotLoginException`异常,而同为未登录异常却有五种抛出场景的区分 - -| 场景值 | 对应常量 | 含义说明 | -|--- |--- |--- | -| -1 | NotLoginException.NOT_TOKEN | 未能从请求中读取到token | -| -2 | NotLoginException.INVALID_TOKEN| 已读取到token,但是token无效 | -| -3 | NotLoginException.TOKEN_TIMEOUT| 已读取到token,但是token已经过期 | -| -4 | NotLoginException.BE_REPLACED| 已读取到token,但是token已被顶下线 | -| -5 | NotLoginException.KICK_OUT| 已读取到token,但是token已被踢下线 | - - - -那么,如何获取场景值呢?废话少说直接上代码: - - -``` java -// 全局异常拦截(拦截项目中的NotLoginException异常) -@ExceptionHandler(NotLoginException.class) -public AjaxJson handlerNotLoginException(NotLoginException nle, HttpServletRequest request, HttpServletResponse response) - throws Exception { - - // 打印堆栈,以供调试 - nle.printStackTrace(); - - // 判断场景值,定制化异常信息 - String message = ""; - if(nle.getType().equals(NotLoginException.NOT_TOKEN)) { - message = "未提供token"; - } - else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) { - message = "token无效"; - } - else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) { - message = "token已过期"; - } - else if(nle.getType().equals(NotLoginException.BE_REPLACED)) { - message = "token已被顶下线"; - } - else if(nle.getType().equals(NotLoginException.KICK_OUT)) { - message = "token已被踢下线"; - } - else { - message = "当前会话未登录"; - } - - // 返回给前端 - return AjaxJson.getError(message); -} -``` - -
-注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理 - diff --git a/sa-token-doc/doc/fun/timeline.md b/sa-token-doc/doc/fun/timeline.md deleted file mode 100644 index 63867b66a..000000000 --- a/sa-token-doc/doc/fun/timeline.md +++ /dev/null @@ -1,14 +0,0 @@ -# Sa-Token 大事记 - - -- **2020-02-04:** 在GitHub提交第一个版本,正式开源 -- **2020-09-14:** GitHub star数量破100 -- **2020-10-26:** Gitee star数量破100 -- **2021-03-01:** 被[HelloGitHub]第59期收录推荐 -- **2021-03-26:** GitHub star数量破1k -- **2021-03-30:** 受TLog作者邀请,Sa-Token加入dromara社区 -- **2021-03-30:** 被Gitee官方列为推荐项目 -- **2021-03-31:** Gitee star数量破1K -- **2021-04-09:** GitHub star数量破2K - - diff --git a/sa-token-doc/doc/fun/token-info.md b/sa-token-doc/doc/fun/token-info.md deleted file mode 100644 index bab0a4365..000000000 --- a/sa-token-doc/doc/fun/token-info.md +++ /dev/null @@ -1,22 +0,0 @@ -# SaTokenInfo 参数详解 - -token信息Model: 用来描述一个token的常用参数 - -``` js -{ - "code": 200, - "msg": "ok", - "data": { - "tokenName": "satoken", // token名称 - "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值 - "isLogin": true, // 此token是否已经登录 - "loginId": "10001", // 此token对应的LoginId,未登录时为null - "loginType": "login", // 账号类型标识 - "tokenTimeout": 2591977, // token剩余有效期 (单位: 秒) - "sessionTimeout": 2591977, // User-Session剩余有效时间 (单位: 秒) - "tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) - "tokenActivityTimeout": -1, // token剩余无操作有效时间 (单位: 秒) - "loginDevice": "default-device" // 登录设备标识 - }, -} -``` \ No newline at end of file diff --git a/sa-token-doc/doc/fun/token-timeout.md b/sa-token-doc/doc/fun/token-timeout.md deleted file mode 100644 index aeadfca88..000000000 --- a/sa-token-doc/doc/fun/token-timeout.md +++ /dev/null @@ -1,49 +0,0 @@ -# Token有效期详解 - - - -Sa-Token 提供两种Token自动过期策略,分别是`timeout`与`activity-timeout`,其详细用法如下: - - -### timeout -1. `timeout`代表Token的长久有效期,单位/秒,例如将其配置为 2592000 (30天),代表在30天后,Token必定过期,无法继续使用 -2. `timeout`无法续签,想要继续使用必须重新登录 -3. `timeout`的值配置为-1后,代表永久有效,不会过期 - - -### activity-timeout -1. `activity-timeout`代表临时有效期,单位/秒,例如将其配置为 1800 (30分钟),代表用户如果30分钟无操作,则此Token会立即过期 -2. 如果在30分钟内用户有操作,则会再次续签30分钟,用户如果一直操作则会一直续签,直到连续30分钟无操作,Token才会过期 -3. `activity-timeout`的值配置为-1后,代表永久有效,不会过期,此时也无需频繁续签 - - -### 关于activity-timeout的续签 -如果`activity-timeout`配置了大于零的值,Sa-Token 会在登录时开始计时,在每次直接或间接调用`getLoginId()`时进行一次过期检查与续签操作。 -此时会有两种情况: -1. 一种是会话无操作时间太长,Token已经过期,此时框架会抛出`NotLoginException`异常(场景值=-3), -2. 另一种则是会话在`activity-timeout`有效期内通过检查,此时Token可以成功续签 - - -### 我可以手动续签吗? -**可以!** -如果框架的自动续签算法无法满足您的业务需求,你可以进行手动续签,Sa-Token 提供两个API供你操作: -1. `StpUtil.checkActivityTimeout()`: 检查当前Token 是否已经[临时过期],如果已经过期则抛出异常 -2. `StpUtil.updateLastActivityToNow()`: 续签当前Token:(将 [最后操作时间] 更新为当前时间戳) - -注意:在手动续签时,即使Token已经 [临时过期] 也可续签成功,如果此场景下需要提示续签失败,可采用先检查再续签的形式保证Token有效性 - -例如以下代码: -``` java -// 先检查是否已过期 -StpUtil.checkActivityTimeout(); -// 检查通过后继续续签 -StpUtil.updateLastActivityToNow(); -``` - -同时,你还可以关闭框架的自动续签(在配置文件中配置 `autoRenew=false` ),此时续签操作完全由开发者控制,框架不再自动进行任何续签操作 - - -### timeout与activity-timeout可以同时使用吗? -**可以同时使用!** -两者的认证逻辑彼此独立,互不干扰,可以同时使用。 - diff --git a/sa-token-doc/doc/index-backup.html b/sa-token-doc/doc/index-backup.html new file mode 100644 index 000000000..b9e9b98c6 --- /dev/null +++ b/sa-token-doc/doc/index-backup.html @@ -0,0 +1,24 @@ + + + + + Sa-Token + + + + + + + +
+

+ Sa-Token 文档已迁移,请打开:最新地址 +

+
+ + + + + diff --git a/sa-token-doc/doc/index.html b/sa-token-doc/doc/index.html index 169f2a6ce..b9e9b98c6 100644 --- a/sa-token-doc/doc/index.html +++ b/sa-token-doc/doc/index.html @@ -4,185 +4,21 @@ Sa-Token - + - - - - + - -
- -

Sa-Token

-
-
- -
-
加载中...
+
+

+ Sa-Token 文档已迁移,请打开:最新地址 +

-
-
-
- - - - - - - - - - - - - - - - - - -
- -
- - - - + + diff --git a/sa-token-doc/doc/lib/index.css b/sa-token-doc/doc/lib/index.css deleted file mode 100644 index 1d07e407b..000000000 --- a/sa-token-doc/doc/lib/index.css +++ /dev/null @@ -1,99 +0,0 @@ -/* 调整一下左侧树的样式 */ -body{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;} -.logo-box {position: absolute;left: 30px;top: 10px;cursor: pointer;color: #000;} -.logo-box img {width: 50px;height: 50px;vertical-align: middle;} -.logo-box .logo-text {display: inline-block;vertical-align: middle;font-size: 24px;font-weight: 400;} -#main {padding-bottom: 100px;} -#main h2 {font-size: 1.6rem;} -#main h3 {font-size: 1.25rem;} - -.main-box .markdown-section{ padding: 30px 20px; max-width: 75%; margin-left: 10%;} -@media screen and (max-width: 800px) { - .logo-box {display: none;} - .main-box .markdown-section{max-width: 1000px; margin-left: auto;} -} -/* @media screen and (min-width: 1700px) { - .main-box .markdown-section{max-width: 70%;} -} */ -/* 手机端不显示广告 */ -@media (max-width: 576px) {.wwads-cn{display:none!important}} - - -/* 调整一下左侧树的字体样式 */ -.sidebar .sidebar-nav>ul>li>p{font-size: 1.2em; margin-top: 10px;} -.sidebar .sidebar-nav>ul>li> strong{font-size: 1.2em; margin-top: 10px;} -.sidebar ul li a{color: #222;} -.sidebar .sidebar-nav>ul>li>ul>li>a{/* color: #222; */font-size: 16px; /* font-weight: 700; */} - -/* GitHub折线图最大宽度 */ -[alt=github-chart]{max-width: 897px;} -/* 大屏幕时,某些图片限制一下宽度 */ -@media screen and (min-width: 800px) { - [title=s-w],[title=s-w-sh]{max-width: 80%;} -} -[title=s-w-sh]{display: inline-block; border: 1px #eee solid;} - -/* 公众号table */ -.gzh-table{ /* table-layout:fixed !important; */} -/* .gzh-table,.gzh-table tr,.gzh-table td{display: block !important;} */ -/* .gzh-table tbody{display: block !important; width: 100% !important;} */ -#main .gzh-table tr{background-color: #FFF;} -.gzh-table td{padding: 20px !important; width: 20%; border: 0;} -.gzh-table td b{display: block; margin-bottom: 10px; } -/* .gzh-table td img{width: 100%;} */ -/* .gzh-table tr, .gzh-table td{ border: 0px !important;} */ - -/* tab选项卡优化 */ -.docsify-tabs__tab{outline: 0; cursor: pointer;} -.docsify-tabs--classic .docsify-tabs__tab--active{box-shadow: 0 0 0;} - -/* ============== 代码样式优化 ================ */ - -/* 背景变黑 */ -.main-box [data-lang]{padding: 0px !important; border-radius: 4px;overflow-x: auto; overflow-y: hidden;} -.main-box [v-pre] code{border: 0px red solid; border-radius: 0px; /* background-color: #282828; */ background-color: #111; color: #FFF;} -.main-box [v-pre] code{padding: 1.5em 1.3em; margin-left: 40px !important;} -/* .main-box h2{margin-top: 70px;} */ - -/* xml语言样式优化 */ -.lang-xml .token.comment{color: #CDAB53;} -.lang-xml .token.tag *{color: #db2d20;} -.lang-xml .token.attr-value{color: #A6E22E;} - -/* java语言样式优化 */ -.main-box .lang-java{color: #01a252 !important;; opacity: 1;} -.lang-java .token.keyword{color: #db2d20;} -.lang-java .token.namespace,.lang-java .token.namespace *{color: #01A252; opacity: 1;} -.lang-java .token.class-name,.lang-java .cm-variable{color: #55b5db; opacity: 1;} -.lang-java .token.comment{color: #CDAB53;} -.lang-java .token.annotation.punctuation{color: #ddd;} -.lang-java .token.punctuation{color: #ddd;} - -/* yml语言样式优化 */ -.main-box .lang-yml{color: #01A252 !important; opacity: 1;} - -/* yml语言样式优化 */ -.main-box .lang-url{color: #E96917 !important; opacity: 1;} - -/* js语言样式优化 */ -.main-box .lang-js{color: #01a252 !important;} -.lang-js .token.comment{color: #CDAB53;} -/* .lang-js .token.string{color: #fded02;} */ -.lang-js .token.string{color: #ddd;} -.lang-js .token.punctuation{color: #ddd;} - -.gt-container{padding: 1.5em; padding-bottom: 100px;} - -/* 调整表格的响应式 */ -#main table{margin-left: 0px;} -@media screen and (min-width: 800px) { - #main table tr th{min-width: 150px;} -} - -/* 代码行号盒子样式 */ -.code-line-box {list-style-type: none; border-right: 1px solid #000; position: absolute; top: 0; left: 0; width: 40px; user-select: none;} -.code-line-box {padding: calc(1.5em + 1px) 0px !important; padding-bottom: calc(1.5em + 20px) !important; margin: 0px !important;} -.code-line-box {line-height: inherit !important; background-color: #111; color: #aaa;font-weight: 400;font-size: 0.85em;text-align: center;} -.code-line-box {font-family: source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace;} - - diff --git a/sa-token-doc/doc/logo-150.png b/sa-token-doc/doc/logo-150.png deleted file mode 100644 index f2538cff9..000000000 Binary files a/sa-token-doc/doc/logo-150.png and /dev/null differ diff --git a/sa-token-doc/doc/micro/dcs-session.md b/sa-token-doc/doc/micro/dcs-session.md deleted file mode 100644 index 84e44a3c2..000000000 --- a/sa-token-doc/doc/micro/dcs-session.md +++ /dev/null @@ -1,52 +0,0 @@ -# 微服务 - 分布式Session会话 - ---- - -### 需求场景 - -微服务架构下的第一个难题便是数据同步,单机版的`Session`在分布式环境下一般不能正常工作,为此我们需要对框架做一些特定的处理。 - -首先我们要明白,分布式环境下为什么`Session`会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点, -这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。 - -### 解决方案 - -要怎么解决这个问题呢?目前的主流方案有四种: -1. **Session同步**:只要一个节点的数据发生了改变,就强制同步到其它所有节点 -2. **Session粘滞**:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务 -3. **建立会话中心**:将Session存储在专业的缓存中间件上,使每个节点都变成了无状态服务,例如:`Redis` -4. **颁发无状态token**:放弃Session机制,将用户数据直接写入到令牌本身上,使会话数据做到令牌自解释,例如:`jwt` - - -### 方案选择 - -该如何选择一个合适的方案? -- 方案一:性能消耗太大,不太考虑 -- 方案二:需要从网关处动手,与框架无关 -- 方案三:Sa-Token 整合`Redis`非常简单,详见章节:[集成 Redis](/up/integ-redis) -- 方案四:详见官方仓库中 Sa-Token 整合`jwt`的示例 - -由于`jwt`模式不在服务端存储数据,对于比较复杂的业务可能会功能受限,因此更加推荐使用方案三 - -``` xml - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - - - org.apache.commons - commons-pool2 - -``` -详细参考:[集成 Redis](/up/integ-redis) - - - - - - - - - diff --git a/sa-token-doc/doc/micro/gateway-auth.md b/sa-token-doc/doc/micro/gateway-auth.md deleted file mode 100644 index c60239f1d..000000000 --- a/sa-token-doc/doc/micro/gateway-auth.md +++ /dev/null @@ -1,112 +0,0 @@ -# 微服务 - 网关统一鉴权 - -微服务架构下的鉴权一般分为两种: -1. 每个服务各自鉴权 -2. 网关统一鉴权 - -方案一和传统单体鉴权差别不大,不再过多赘述,本篇介绍方案二的整合步骤: - ---- - - - -### 1、引入依赖 - -首先,根据 [依赖引入说明](/micro/import-intro) 引入正确的依赖,以`[SpringCloud Gateway]`为例: - -``` xml - - - cn.dev33 - sa-token-reactor-spring-boot-starter - ${sa.top.version} - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - - - org.apache.commons - commons-pool2 - -``` -注:Redis包是必须的,因为我们需要和各个服务通过Redis来同步数据 - -### 2、实现鉴权接口 -``` java -/** - * 自定义权限验证接口扩展 - */ -@Component -public class StpInterfaceImpl implements StpInterface { - - @Override - public List getPermissionList(Object loginId, String loginType) { - // 返回此 loginId 拥有的权限列表 - return ...; - } - - @Override - public List getRoleList(Object loginId, String loginType) { - // 返回此 loginId 拥有的角色列表 - return ...; - } - -} - -``` -关于数据的获取,建议以下方案三选一: -1. 在网关处集成ORM框架,直接从数据库查询数据 -2. 先从Redis中获取数据,获取不到时走ORM框架查询数据库 -3. 先从Redis中获取缓存数据,获取不到时走RPC调用子服务 (专门的权限数据提供服务) 获取 - - -### 3、注册全局过滤器 -然后在网关处注册全局过滤器进行鉴权操作 - -``` java -/** - * [Sa-Token 权限认证] 配置类 - * @author kong - */ -@Configuration -public class SaTokenConfigure { - // 注册 Sa-Token全局过滤器 - @Bean - public SaReactorFilter getSaReactorFilter() { - return new SaReactorFilter() - // 拦截地址 - .addInclude("/**") - // 开放地址 - .addExclude("/favicon.ico") - // 鉴权方法:每次访问进入 - .setAuth(r -> { - // 登录验证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 - SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin()); - - // 权限认证 -- 不同模块, 校验不同权限 - SaRouter.match("/user/**", () -> StpUtil.checkPermission("user")); - SaRouter.match("/admin/**", () -> StpUtil.checkPermission("admin")); - SaRouter.match("/goods/**", () -> StpUtil.checkPermission("goods")); - SaRouter.match("/orders/**", () -> StpUtil.checkPermission("orders")); - - // ... - }) - // 异常处理方法:每次setAuth函数出现异常时进入 - .setError(e -> { - return SaResult.error(e.getMessage()); - }) - ; - } -} -``` - -详细操作参考:[路由拦截鉴权](/use/route-check) - - - - - - diff --git a/sa-token-doc/doc/micro/id-token.md b/sa-token-doc/doc/micro/id-token.md deleted file mode 100644 index 68523ce1f..000000000 --- a/sa-token-doc/doc/micro/id-token.md +++ /dev/null @@ -1,228 +0,0 @@ -# 微服务 - 内部服务外网隔离 - ---- - - -### 一、需求场景 - -我们的子服务一般不能通过外网直接访问,必须通过网关转发才是一个合法的请求,这种子服务与外网的隔离一般分为两种: - -1. 物理隔离:子服务部署在指定的内网环境中,只有网关对外网开放 -2. 逻辑隔离:子服务与网关同时暴露在外网,但是子服务会有一个权限拦截层保证只接受网关发送来的请求,绕过网关直接访问子服务会被提示:无效请求 - -这种鉴权需求牵扯到两个环节:**`网关转发鉴权`**、**`服务内部调用鉴权`** - -Sa-Token提供两种解决方案: -1. 使用 OAuth2.0 模式的凭证式,将 Client-Token 用作各个服务的身份凭证进行权限校验 -2. 使用 Id-Token 模块提供的身份校验能力,完成服务间的权限认证 - -本篇主要讲解方案二 `Id-Token` 模块的整合步骤,其鉴权流程与 OAuth2.0 类似,不过使用方式上更加简洁(希望使用方案一的同学可参考Sa-OAuth2模块,此处不再赘述) - -### 二、网关转发鉴权 - -##### 1、引入依赖 - -在网关处引入的依赖为(此处以 SpringCloud Gateway 为例): -``` xml - - - cn.dev33 - sa-token-reactor-spring-boot-starter - ${sa.top.version} - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - - - org.apache.commons - commons-pool2 - -``` - -在子服务引入的依赖为: -``` xml - - - cn.dev33 - sa-token-spring-boot-starter - ${sa.top.version} - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - - - org.apache.commons - commons-pool2 - -``` - -##### 2、网关处添加Id-Token - -为网关添加全局过滤器: -``` java -/** - * 全局过滤器,为请求添加 Id-Token - */ -@Component -public class ForwardAuthFilter implements GlobalFilter { - @Override - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - ServerHttpRequest newRequest = exchange - .getRequest() - .mutate() - // 为请求追加 Id-Token 参数 - .header(SaIdUtil.ID_TOKEN, SaIdUtil.getToken()) - .build(); - ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); - return chain.filter(newExchange); - } -} -``` -此过滤器会为 Request 请求头追加 `Id-Token` 参数,这个参数会被转发到子服务 - - -##### 3、在子服务里校验参数 - -在子服务添加过滤器校验参数 -``` java -/** - * Sa-Token 权限认证 配置类 - */ -@Configuration -public class SaTokenConfigure implements WebMvcConfigurer { - // 注册 Sa-Token 全局过滤器 - @Bean - public SaServletFilter getSaServletFilter() { - return new SaServletFilter() - .addInclude("/**") - .addExclude("/favicon.ico") - .setAuth(r -> { - // 校验 Id-Token 身份凭证 —— 以下两句代码可简化为:SaIdUtil.checkCurrentRequestToken(); - String token = SaHolder.getRequest().getHeader(SaIdUtil.ID_TOKEN); - SaIdUtil.checkToken(token); - }) - .setError(e -> { - return SaResult.error(e.getMessage()); - }) - ; - } -} -``` - -启动网关与子服务,访问测试: - -> 如果通过网关转发,可以正常访问,直接访问子服务会提示:`无效Id-Token:xxx` - - -### 三、服务内部调用鉴权 - -有时候我们需要在一个服务调用另一个服务的接口,这也是需要添加`Id-Token`作为身份凭证的 - -在服务里添加 Id-Token 流程与网关类似,我们以RPC框架 `Feign` 为例: - -##### 1、首先在调用方添加 FeignInterceptor -``` java -/** - * feign拦截器, 在feign请求发出之前,加入一些操作 - */ -@Component -public class FeignInterceptor implements RequestInterceptor { - // 为 Feign 的 RCP调用 添加请求头Id-Token - @Override - public void apply(RequestTemplate requestTemplate) { - requestTemplate.header(SaIdUtil.ID_TOKEN, SaIdUtil.getToken()); - } -} -``` - -##### 2、在调用接口里使用此 Interceptor -``` java -/** - * 服务调用 - */ -@FeignClient( - name = "sp-home", // 服务名称 - configuration = FeignInterceptor.class, // 请求拦截器 (关键代码) - fallbackFactory = SpCfgInterfaceFallback.class // 服务降级处理 - ) -public interface SpCfgInterface { - - // 获取server端指定配置信息 - @RequestMapping("/SpConfig/getConfig") - public String getConfig(@RequestParam("key")String key); - -} -``` - -被调用方的代码无需更改(按照网关转发鉴权处的代码注册全局过滤器),保持启动测试即可 - - -### 四、Id-Token 模块详解 - -Id-Token —— 专门解决身份凭证问题的一个模块,它的作用不仅局限于微服务调用场景 - -基本使用流程为:服务调用方获取Token,提交到请求中,被调用方取出Token进行校验:Token一致则校验通过,否则拒绝服务 - -首先我们预览一下此模块的相关API: -``` java -// 获取当前Id-Token -SaIdUtil.getToken(); - -// 判断一个Id-Token是否有效 -SaIdUtil.isValid(token); - -// 校验一个Id-Token是否有效 (如果无效则抛出异常) -SaIdUtil.checkToken(token); - -// 校验当前Request提供的Id-Token是否有效 (如果无效则抛出异常) -SaIdUtil.checkCurrentRequestToken(); - -// 刷新一次Id-Token (注意集群环境中不要多个服务重复调用) -SaIdUtil.refreshToken(); - -// 在 Request 上储存 Id-Token 时建议使用的key -SaIdUtil.ID_TOKEN; -``` - -##### 1、疑问:这个Token保存在什么地方?有没有泄露的风险?Token为永久有效还是临时有效? -Id-Token 默认随 Sa-Token 数据一起保存在Redis中,理论上不会存在泄露的风险,每个Token默认有效期只有一天 - -##### 2、如何主动刷新Id-Token,例如:五分钟、两小时刷新一次? -Id-Token 刷新间隔越短,其安全性越高,每个Token的默认有效期为一天,在一天后再次获取会自动产生一个新的Token - -!> 需要注意的一点是:Id-Token默认的自刷新机制,并不能做到高并发可用,多个服务一起触发Token刷新可能会造成毫秒级的短暂服务失效,其只能适用于 项目开发阶段 或 低并发业务场景 - -因此在微服务架构下,我们需要有专门的机制主动刷新Id-Token,保证其高可用 - -例如,我们可以专门起一个服务,使用定时任务来刷新Id-Token -``` java -/** - * Id-Token,定时刷新 - */ -@Configuration -public class SaIdTokenRefreshTask { - // 从 0 分钟开始 每隔 5 分钟执行一次 Id-Token - @Scheduled(cron = "0 0/5 * * * ? ") - public void refreshToken(){ - SaIdUtil.refreshToken(); - } -} -``` - -以上的cron表达式刷新间隔可以配置为`五分钟`、`十分钟` 或 `两小时`,只要低于Id-Token的有效期(默认为一天)即可。 - -##### 3、如果网关携带token转发的请求在落到子服务的节点上时,恰好刷新了token,导致鉴权未通过怎么办? -Id-Token 模块在设计时,充分考虑到了这一点,在每次刷新 Token 时,旧 Token 会被作为次级 Token 存储起来, -只要网关携带的 Token 符合新旧 Token 其一即可通过认证,直至下一次刷新,新 Token 再次作为次级 Token 将此替换掉 - - - - - - diff --git a/sa-token-doc/doc/micro/import-intro.md b/sa-token-doc/doc/micro/import-intro.md deleted file mode 100644 index 1e771d246..000000000 --- a/sa-token-doc/doc/micro/import-intro.md +++ /dev/null @@ -1,62 +0,0 @@ - -# 微服务中使用Sa-Token 依赖引入说明 - ---- - -虽然在 [开始] 章节已经说明了依赖引入规则,但是交流群里不少小伙伴提出bug解决到最后发现都是因为依赖引入错误导致的,此处再次重点强调一下: - -> **在微服务架构中使用Sa-Token时,网关和内部服务要分开引入Sa-Token依赖(不要直接在顶级父pom中引入Sa-Token)** - -总体来讲,我们需要关注的依赖就是两个:`sa-token-spring-boot-starter` 和 `sa-token-reactor-spring-boot-starter`, - -``` xml - - - cn.dev33 - sa-token-spring-boot-starter - ${sa.top.version} - -``` - -``` xml - - - cn.dev33 - sa-token-reactor-spring-boot-starter - ${sa.top.version} - -``` - - -至于怎么分辨我们需要引入哪个呢?这个要看你使用的基础框架: - -对于内部基础服务来讲,我们一般都是使用SpringBoot默认的web模块:SpringMVC, -因为这个SpringMVC是基于Servlet模型的,在这里我们需要引入的是`sa-token-spring-boot-starter` - -对于网关服务,大体来讲分为两种: -- 一种是基于Servlet模型的,如:Zuul,我们需要引入的是:`sa-token-spring-boot-starter`,详细戳:[在SpringBoot环境集成](/start/example) -- 一种是基于Reactor模型的,如:SpringCloud Gateway、ShenYu、Fizz Gateway 等等,我们需要引入的是:`sa-token-reactor-spring-boot-starter`,**并且注册全局过滤器!**,详细戳:[在WebFlux环境集成](/start/webflux-example) - -注:切不可直接在一个项目里同时引入这两个依赖,否则会造成项目无法启动 - -另外,我们需要引入Redis集成包,因为我们的网关和子服务主要通过Redis来同步数据 -``` xml - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - - - org.apache.commons - commons-pool2 - -``` -详细参考:[集成 Redis](/up/integ-redis) - - - - - - - diff --git a/sa-token-doc/doc/more/common-action.md b/sa-token-doc/doc/more/common-action.md deleted file mode 100644 index 90e3d2cf4..000000000 --- a/sa-token-doc/doc/more/common-action.md +++ /dev/null @@ -1,98 +0,0 @@ -# 常用类、方法 -本篇介绍Sa-Token中一些常用的全局对象、类 - ---- - -### SaManager -SaManager 负责管理 Sa-Token 所有运行时对象 -``` java -SaManager.getConfig(); // 获取全局配置对象 -SaManager.getSaTokenDao(); // 获取数据持久化对象 -SaManager.getStpInterface(); // 获取权限认证对象 -SaManager.getSaTokenAction(); // 获取框架行为对象 -SaManager.getSaTokenContext(); // 获取上下文处理对象 -SaManager.getSaTokenListener(); // 获取侦听器对象 -SaManager.getStpLogic("type"); // 获取指定账号类型的StpLogic对象 -``` - - -### SaHolder -Sa-Token上下文持有类,通过此类快速获取当前环境的相关对象 -``` java -SaHolder.getRequest(); // 获取当前请求的 [Request] 对象 -SaHolder.getResponse(); // 获取当前请求的 [Response] 对象 -SaHolder.getStorage(); // 获取当前请求的 [存储器] 对象 -``` - - -### SaRouter -路由匹配工具类,详细戳:[路由拦截式鉴权](/use/route-check) - - -### SaFoxUtil -Sa-Token内部工具类,包含一些工具方法 -``` java -SaFoxUtil.printSaToken(); // 打印 Sa-Token 版本字符画 -SaFoxUtil.getRandomString(8); // 生成指定长度的随机字符串 -SaFoxUtil.isEmpty(str); // 指定字符串是否为null或者空字符串 -SaFoxUtil.getMarking28(); // 以当前时间戳和随机int数字拼接一个随机字符串 -SaFoxUtil.formatDate(date); // 将日期格式化为yyyy-MM-dd HH:mm:ss字符串 -SaFoxUtil.searchList(); // 从集合里查询数据 -SaFoxUtil.vagueMatch(patt, str); // 字符串模糊匹配 -``` - -### SaTokenConfigFactory -配置对象工厂类,通过此类你可以方便的根据properties配置文件创建一个配置对象 - -1、首先在项目根目录,创建一个配置文件:`sa-token.properties` - -``` java -# token名称 (同时也是cookie名称) -tokenName=satoken -# token有效期,单位s 默认30天, -1代表永不过期 -timeout=2592000 -# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 -activityTimeout=-1 -# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) =-1 -isConcurrent=true -# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) -isShare=true -# token风格 -isLog=false -``` - -2、然后使用以下代码获取配置对象 -``` java -// 设置配置文件地址 -SaTokenConfigFactory.configPath = "sa-token.properties"; - -// 获取配置信息到 config 对象 -SaTokenConfig config = SaTokenConfigFactory.createConfig(); - -// 注入到 SaManager 中 -SaManager.setConfig(config); -``` - - -### SpringMVCUtil -SpringMVC操作的工具类,位于包:`sa-token-spring-boot-starter` -``` java -SpringMVCUtil.getRequest(); // 获取当前会话的 request -SpringMVCUtil.getResponse(); // 获取当前会话的 response -``` - - -### SaReactorHolder & SaReactorSyncHolder -Sa-Token集成Reactor时的 ServerWebExchange 工具类,位于包:`sa-token-reactor-spring-boot-starter` -``` java -// 异步方式获取 ServerWebExchange 对象 -SaReactorHolder.getContext().map(e -> { - System.out.println(e); -}); - -// 同步方式获取 ServerWebExchange 对象 -ServerWebExchange e = SaReactorSyncHolder.getContext(); -System.out.println(e); -``` - - diff --git a/sa-token-doc/doc/more/common-questions.md b/sa-token-doc/doc/more/common-questions.md deleted file mode 100644 index 1a632a9f7..000000000 --- a/sa-token-doc/doc/more/common-questions.md +++ /dev/null @@ -1,99 +0,0 @@ -# 常见问题 -本篇整理大家在群聊里经常提问的一些问题,如有补充,欢迎提交pr - ---- - -### 加了注解进行鉴权认证,不生效? -注解鉴权功能默认关闭,两种方式任选其一进行打开:注册注解拦截器、集成`AOP模块`, -如果已经打开仍然没有效果,加群说明一下复现步骤 - - -### 整合Redis时,除了引入pom依赖,还需要做其它的吗? -引入pom依赖后,在框架层面你无须做其它事情,但是你需要为项目指定一下`Redis`的连接信息,参考此文件:[application-dev](https://gitee.com/click33/sa-plus/blob/master/sp-server/src/main/resources/application-dev.yml) - - -### 登录方法需要我自己实现吗? -是的,不同于`shiro`等框架,`Sa-Token`不会在登录流程中强插一脚,开发者比对完用户的账号和密码之后,只需要调用`StpUtil.login(id)`通知一下框架即可 - - -### 一个User对象存进Session后,再取出来时报错:无法从User类型转换成User类型? -群员亲测,当你打开热部署模式后,先存进去的对象,再热刷新后再取出,会报错,关闭热刷新即可解决 - - -### 框架抛出的权限不足异常,我想根据自定义提示信息,可以吗? -可以,在全局异常拦截器里捕获`NotPermissionException`,可以通过`getCode()`获取没有通过认证的权限码,可以据此自定义返回信息 - - -### 我的项目权限模型不是RBAC模型,很复杂,可以集成吗? -无论什么模型,只要能把一个用户具有的所有权限塞到一个List里返回给框架,就能集成 - - -### SaRouter.match 有多个路径需要排除怎么办? -可以点进去源码看一下,`SaRouter.match`方法有多个重载,可以放一个集合, 例如:
-`SaRouter.match(Arrays.asList("/**"), Arrays.asList("/login", "/reg"), () -> StpUtil.checkLogin());` - - -### 为什么StpUtil.login() 不能直接写入一个User对象? -`StpUtil.login()`只是为了给当前会话做个唯一标记,通常写入`UserId`即可,如果要存储User对象,可以使用`StpUtil.getSession()`获取Session对象进行存储 - - -### 前后台分离模式下和普通模式有何不同? -主要是失去了`Cookie`无法自动化保存和提交`token秘钥`,可以参考章节:[前后台分离](/up/not-cookie) - - -### 前后台分离时,前端提交的header参数是叫token还是satoken还是tokenName? -默认是satoken,如果想换一个名字,更改一下配置文件的`tokenName`即可 - - -### Springboot环境下采用自定义拦截器排除了某个路径仍然被拦截了? -可能是404了,SpringBoot环境下如果访问接口404后,会被重定向到`/error`,然后被再次拦截,如果是其它原因,欢迎加群反馈 - - -### 权限可以做成动态的吗? -权限本来就是动态的,只有jwt那种模式才是非动态的 - - -### 集成jwt后为什么在 getSession 时提示 jwt has not session ? -`jwt`的招牌便是无须借助服务端完成会话管理,如果集成`jwt`后再次使用`Session`功能,那将又回到了传统`Session`模式,属于自断招牌,此种技术组合没有任何意义,因此jwt集成模式不提供`Session`功能,如果需要`Session`功能,就不要集成`jwt` - - -### 怎么关闭默认的Cookie模式呢? -在配置文件将`isReadCookie`值配置为`false` - - -### 怎么关掉每次启动时的字符画打印? -在配置文件将`isPrint`值配置为`false` - - -### StpUtil.getSession()必须登录后才能调用吗?如果我想在用户未登录之前存储一些数据应该怎么办? -`StpUtil.getSession()`获取的是`User-Session`,必须登录后才能使用,如果需要在未登录状态下也使用Session功能,请使用`Token-Session`
-步骤:先在配置文件里将`tokenSessionCheckLogin`配置为`false`,然后通过`StpUtil.getTokenSession()`获取Session - - -### 我只使用header来传输token,还需要打开Cookie模式吗? -不需要,如果只使用header来传输token,可以在配置文件关闭Cookie模式,例:`isReadCookie=false` - - -### 我想让用户修改密码后立即掉线重新登录,应该怎么做? -框架内置 [强制指定账号下线] 的APi,在执行修改密码逻辑之后调用此API即可: `StpUtil.logout()` - - -### 整合Redis时先选择了默认jdk序列化,后又改成jackson序列化,程序开始报错,SerializationException? -两者的序列化算法不一致导致的反序列化失败,如果要更改序列化方式,则需要先将Redis中历史数据清除,再做更新 - - -### 代码鉴权、注解鉴权、路由拦截鉴权,我该如何选择? -这个问题没有标准答案,这里只能给你提供一些建议,从鉴权粒度的角度来看: -1. 路由拦截鉴权:粒度最粗,只能粗略的拦截一个模块进行权限认证 -2. 注解鉴权:粒度较细,可以详细到方法级,比较灵活 -3. 代码鉴权:粒度最细,不光可以控制到方法级,甚至可以if语句决定是否鉴权 - -So:从鉴权粒度的角度来看,需要针对一个模块鉴权的时候,就用路由拦截鉴权,需要控制到方法级的时候,就用注解鉴权,需要根据条件判断是否鉴权的时候,就用代码鉴权 - - -### 还是有不明白到的地方? -请在`gitee` 、 `github` 提交 `issues`,或者加入qq群交流(群链接在[首页](README?id=交流群)) - - -### 我能为这个框架贡献代码吗? -**可以**,如果有好的想法,请直接提交pr步骤 diff --git a/sa-token-doc/doc/more/link.md b/sa-token-doc/doc/more/link.md deleted file mode 100644 index fea0851b7..000000000 --- a/sa-token-doc/doc/more/link.md +++ /dev/null @@ -1,36 +0,0 @@ -# Sa-Token 相关项目 - ---- - -### 使用 Sa-Token 的开源项目: - -[**[ sa-plus ]** 一个基于springboot架构的快速开发框架,内置代码生成器](https://gitee.com/click33/sa-plus) - -[**[ jthink ]** 一个基于springboot+sa-token+thymeleaf的博客系统](https://gitee.com/wtsoftware/jthink) - -[**[ dcy-fast ]** 一个基于springboot+sa-token+mybatis-plus的后台管理系统,前端vue-element-admin,并且内置代码生成器](https://gitee.com/dcy421/dcy-fast) - -[**[ helio-starters ]** 基于JDK15 + Spring Boot 2.4 + Sa-Token + Mybatis-Plus的单体Boot版脚手架和微服务Cloud版脚手架,带有配套后台管理前端模板及代码生成器](https://gitee.com/uncarbon97/helio-starters) - -[**[ Sa-Token-Study ]** 以demo示例的方式讲解 Sa-Token 源码涉及到的技术点,连载中……](https://gitee.com/click33/sa-token-study) - -如果您的开源项目使用了Sa-Token,欢迎提交... - -
- - -### 推荐项目: - -[[ OkHttps ] - 一个轻量级http通信框架,API设计无比优雅,支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps) - -[[ hasor ] - 轻量级ioc/aop框架,采用"微内核+插件"的设计思想](https://gitee.com/zycgit/hasor) - -[[ sa-admin ] - 一个多窗口后台模板,流畅、易上手、提高生产力](https://gitee.com/ejlchina-zhxu/okhttps) - -[[ vue-next-admin ] - 一套为开发者快速开发准备的基于 vue2.x 越看越精彩的后台管理系统一站式平台模板](https://gitee.com/lyt-top/vue-next-admin) - -[[ 小诺快速开发平台 ] - 基于SpringBoot2 + AntDesignVue全新快速开发平台,同时拥有三个版本](https://xiaonuo.vip/index#pricing) - -
-虚位以待... - diff --git a/sa-token-doc/doc/more/update-log.md b/sa-token-doc/doc/more/update-log.md deleted file mode 100644 index aa9aafda9..000000000 --- a/sa-token-doc/doc/more/update-log.md +++ /dev/null @@ -1,259 +0,0 @@ -# 更新日志 - - -### 2021-7-24 @v1.24.0 -- 修复:修复部分场景下Alone-Redis插件导致项目无法启动的问题 -- 优化:增加对SpringBoot1.x版本的兼容性 -- 新增:SaOAuth2Util新增checkScope函数,用于校验令牌是否具备指定权限 -- 新增:OAuth2.0模块新增revoke接口,用于提前回收 Access-Token 令牌 -- 新增:新增`Sa-Id-Token` 模块,解决微服务内部调用鉴权 **[重要]** -- 文档:新增OAuth2.0模块常用方法说明 -- 优化:大幅度优化文档示例 - - -### 2021-7-19 @v1.23.0 -- 新增:Sa-Token-OAuth2 模块正式发布 **[重要]** -- 修复:修复jwt集成demo无法正确注册StpLogic的bug -- 修复:修复登录时某些场景下Session续期可能不正常的bug -- 优化:代码注释优化,文档优化 - - -### 2021-7-10 @v1.22.0 -- 新增:SaSsoConfig 部分属性增加set连缀风格 -- 优化:SaSsoUtil 可定制化底层的 `StpLogic` -- 新增:新增 `SaSsoHandle` 大幅度简化单点登录整合步骤 **[重要]** -- 新增:新增Sa-Token在线测评,链接:[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx) **[重要]** -- 新增:Sa-Token-Quick-Login 插件新增拦截与放行路径配置 -- 优化:大幅度优化文档示例 - - -### 2021-7-2 @v1.21.0 -- 新增:新增Token二级认证 **[重要]** -- 新增:新增`Sa-Token-Alone-Redis`独立Redis插件 **[重要]** -- 新增:新增SSO三种模式,彻底解决所有场景下的单点登录问题 **[重要]** -- 新增:新增多账号模式下,注解合并示例 **[重要]** -- 新增:新增`SaRouter.back()`函数,用于停止匹配返回结果 -- 不兼容更新重构: - - 更改yml配置前缀:原`[spring.sa-token.]` 改为 `[sa-token.]`,目前版本暂时向下兼容,请尽快更新 - - -### 2021-6-17 @v1.20.0 -- 新增:新增Solon适配插件,感谢大佬 `@刘西东` 提供的pr **[重要]** -- 新增:新增`SaRouter.stop()`函数,用于一次性跳出匹配链功能 **[重要]** -- 新增:新增单元测试 **[重要]** -- 新增:新增临时令牌验证模块 **[重要]** -- 新增:新增`sa-token-temp-jwt`模块整合jwt临时令牌鉴权 **[重要]** -- 新增:会话 `SaSession.get()` 增加缓存API,简化代码 -- 新增:新增框架调查问卷 -- 修复:修复同时引入 `Spring Cloud Bus` 与 `Sa-Token` 冲突的问题 **[重要]** -- 修复:修复`SaServletFilter`异常函数中无法自定义`Content-Type`的问题 -- 文档:新增微服务依赖引入说明 -- 文档:新增认证流程图 -- 不兼容更新重构: - - 方法:`StpUtil.setLoginId(id)` -> `StpUtil.login(id)` - - 方法:`StpUtil.getLoginKey()` -> `StpUtil.getLoginType()` (注意其它所有地方的`LoginKey`均已更改为`loginType`) - - 工具类:`SaRouterUtil` -> `SaRouter` - - 配置类:`allowConcurrentLogin` -> `isConcurrent` - - 配置类:`isV` -> `isPrint` - - 为保证平滑更新,旧API仍旧保留,但已增加`@Deprecated`注解,请尽快更新至新API - - -### 2021-5-10 @v1.19.0 -- 新增:注解鉴权新增定制loginType功能 **[重要]** -- 重构:重构目录结构,抽离`plugin`模块 **[重要]** -- 新增:新增 `sa-token-quick-login` 插件,零代码集成登录功能 **[重要]** -- 优化:所有函数式接口增加`@FunctionalInterface`注解,感谢群友`@MrXionGe`提供的建议 -- 优化:文档优化... - - -### 2021-4-24 @v1.18.0 -- 新增:新增权限通配符功能,灵活设置权限 **[重要]** -- 修复:修复自动续签处的逻辑错误 -- 新增:新增Web开发常见漏洞防护建议 -- 修复:修复`SaRequest`中缺少`getMethod()`的bug -- 修复:修复自动续签时的逻辑错误,感谢群成员`@N`的建议 -- 新增:全局过滤器新增 `beforAuth` 前置函数 -- 修复:修复在带有上下文的项目中无法正确获取请求路径的bug,感谢群成员`@dlwlrma`提供的建议 -- 新增:新增`SaHolder`上下文持有类,可方便的在上下文中读写数据 -- 重构:`SaTokenManager` -> `SaManager` -- 重构:`SaTokenInsideUtil` -> `SaFoxUtil` - - -### 2021-4-17 @v1.17.0 -- 修复:在WebFlux环境中引入Redis集成包无法启动的问题 -- 修复:修复JWT集成示例中版本升级API的变更 -- 优化:优化启动时字符画打印 -- 文档:新增集成环境说明 -- 文档:新增功能介绍图 -- 新增:全局过滤器增加限定[拦截路径]与[排除路径]功能 -- 重构:全局过滤器执行函数放到成员变量里,连缀风格配置 -- 新增:新增全局侦听器,可在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作 **[重要]** - - -### 2021-4-12 @v1.16.0 -- 新增:新增账号封禁功能,指定时间内账号无法登陆 **[重要]** -- 新增:核心包脱离`ServletAPI`,彻底零依赖! **[重要]** -- 新增:新增基于`ThreadLocal`的上下文容器 **[重要]** -- 新增:新增`Reactor`响应式编程支持,`WebFlux`集成! **[重要]** -- 新增:新增全局过滤器,解决拦截器无法拦截静态资源的问题 **[重要]** -- 新增:新增微服务网关鉴权方案!可接入`ShenYu`、`Gateway`等网关组件! **[重要]** -- 新增:AOP切面定义`Order`顺序为`-100`,可保证在多个自定义切面前执行 -- 文档:新增推荐公众号列表 - - -### 2021-3-23 @v1.15.0 -- 新增:文档添加源码涉及技术栈说明 -- 优化:优化路由拦截器模块文档,更简洁的示例 -- 修复:修复非web环境下的错误提示,Request->Response -- 修复:修复Cookie注入时path判断错误,感谢@zhangzi0291提供的PR -- 新增:文档集成Redis章节新增redis配置示例说明,感谢群友 `@-)` 提供的建议 -- 新增:增加token前缀模式,可在配置token读取前缀,适配`Bearer token`规范 **[重要]** -- 优化:`SaTokenManager`初始化Bean去除`initXxx`方法,优化代码逻辑 -- 新增:`SaTokenManager`新增`stpLogicMap`集合,记录所有`StpLogic`的初始化,方便查找 -- 新增:`Session`新增timeout操作API,可灵活修改Session的剩余有效时间 -- 新增:token前缀改为强制校验模式,如果配置了前缀,则前端提交token时必须带有 -- 优化:精简`SaRouteInterceptor`,只保留自定义验证和默认的登陆验证,去除冗余功能 -- 优化:`SaRouterUtil`迁移到core核心包,优化依赖架构 -- 优化:默认Dao实现类里`Timer定时器`改为子线程 + sleep 模拟 -- 新增:`Session`新增各种类型转换API,可快速方便存取值 **[重要]** -- 升级注意: - - `SaRouterUtil`类迁移到核心包,注意更换import地址 - - `SaRouteInterceptor`去出冗余API,详情参考路由鉴权部分 - - -### 2021-3-12 @v1.14.0 -- 新增:新增`SaLoginModel`登录参数Model,适配 [记住我] 模式 **[重要]** -- 新增:新增 `StpUtil.login()` 时指定token有效期,可灵活控制用户的一次登录免验证时长 -- 新增:新增Cookie时间判断,在`timeout`设置为-1时,`Cookie`有效期将为`Integer.MAX_VALUE` **[重要]** -- 新增:新增密码加密工具类,可快速MD5、SHA1、SHA256、AES、RSA加密 **[重要]** -- 新增:新增 OAuth2.0 模块 **[重要]** -- 新增:`SaTokenConfig`配置类所有set方法支持链式调用 -- 新增:`SaOAuth2Config` sa-token oauth2 配置类所有set方法新增支持链式调用 -- 优化:`StpLogic`类所有`getKey`方法重名为`splicingKey`,更语义化的函数名称 -- 新增:`IsRunFunction`新增`noExe`函数,用于指定当`isRun`值为`false`时执行的函数 -- 新增:`SaSession`新增数据存取值操作API -- 优化:优化`SaTokenDao`接口,增加Object操作API -- 优化:jwt示例`createToken`方法去除默认秘钥判断,只在启动项目时打印警告 -- 文档:常见问题新增示例(修改密码后如何立即掉线) -- 文档:权限认证文档新增[如何把权限精确搭到按钮级]示例说明 -- 文档:优化文档,部分模块添加图片说明 - - -### 2021-2-9 @v1.13.0 -- 优化:优化源码注释与文档 -- 新增:文档集成Gitalk评论系统 -- 优化:源码包`Maven`版本号更改为变量形式 -- 修复:文档处方法名`getPermissionList`错误的bug -- 修复:修复`StpUtil.getTokenInfo()`会触发自动续签的bug -- 修复:修复接口 `SaTokenDao` 的 `searchData` 函数注释错误 -- 新增:`SaSession`的创建抽象到`SaTokenAction`接口,方便按需重写 -- 新建:框架内异常统一继承 `SaTokenException` 方便在异常处理时分辨处理 -- 新增:`SaSession`新增`setId()`与`setCreateTime()`方法,方便部分框架的序列化 -- 新增:新增`autoRenew`配置,用于控制是否打开自动续签模式 -- 新增:同域模式下的单点登录 **[重要]** -- 新增:完善分布式会话的文档说明 - - -### 2021-1-12 @v1.12.0 -- 新增:提供JWT集成示例 **[重要]** -- 新增:新增路由式鉴权,可方便的根据路由匹配鉴权 **[重要]** -- 新增:新增身份临时切换功能,可在一个代码段内将会话临时切换为其它账号 **[重要]** -- 优化:将`SaCheckInterceptor.java`更名为`SaAnnotationInterceptor.java`,更语义化的名称 -- 优化:优化文档 -- 升级:v1.12.1,新增`SaRouterUtil`工具类,更方便的路由鉴权 **[重要]** - - -### 2021-1-10 @v1.11.0 -- 新增:提供AOP注解鉴权方案 **[重要]** -- 优化自动生成token的算法 - - -### 2021-1-9 @v1.10.0 -- 新增:提供查询所有会话方案 **[重要]** -- 修复:修复token设置为永不过期时无法正常被顶下线的bug,感谢github用户 @zjh599245299 提出的bug - - -### 2021-1-6 @v1.9.0 -- 优化:`spring-boot-starter-data-redis` 由 `2.3.7.RELEASE` 改为 `2.3.3.RELEASE` -- 修复:补上注解拦截器里漏掉验证`@SaCheckRole`的bug -- 新增:新增同端互斥登录,像QQ一样手机电脑同时在线,但是两个手机上互斥登录 **[重要]** - - -### 2021-1-2 @v1.8.0 -- 优化:优化源码注释 -- 修复:修复部分文档错别字 -- 修复:修复项目文件夹名称错误 -- 优化:优化文档配色,更舒服的代码展示 -- 新增:提供`sa-token`集成 `redis` 的 `spring-boot-starter` 方案 **[重要]** -- 新增:新增集成 `redis` 时,以`jackson`作为序列化方案 **[重要]** -- 新增:dao层默认实现增加定时清理过期数据功能 **[重要]** -- 新增:新增`token专属session`, 更灵活的会话管理 **[重要]** -- 新增:增加配置,指定在获取`token专属session`时是否必须登录 -- 新增:在无token时自动创建会话,完美兼容`token-session`会话模型! **[重要]** -- 修改:权限码限定必须为String类型 -- 优化:注解验证模式由boolean属性改为枚举方式 -- 删除:`StpUtil`删除部分冗长API,保持API清爽性 -- 新增:新增角色验证 (角色验证与权限验证已完全分离) **[重要]** -- 优化:移除`StpUtil.kickoutByLoginId()`API,由`logoutByLoginId`代替 -- 升级:开源协议修改为`Apache-2.0` - - -### 2020-12-24 @v1.7.0 -- 优化:项目架构改为maven多模块形式,方便增加新模块 **[重要]** -- 优化:与`springboot`的集成改为`springboot-starter`模式,无需`@SaTokenSetup`注解即可完成自动装配 **[重要]** -- 新增:新增`activity-timeout`配置,可控制token临时过期与续签功能 **[重要]** -- 新增:`timeout`过期时间新增-1值,代表永不过期 -- 新增:`StpUtil.getTokenInfo()`改为对象形式,新增部分常用字段 -- 优化:解决在无cookie模式下,不集成redis时会话无法主动过期的问题 -- 修复:修复文档首页样式问题 - - -### 2020-12-17 @v1.6.0 -- 新增:花式token生成方案 **[重要]** -- 优化:优化`readme.md` -- 修复:修复`SaCookieOper`与`SaTokenAction`无法自动注入的问题 - - -### 2020-12-16 @v1.5.1 -- 新增:细化未登录异常类型,提供五种场景值:未提供token、token无效、token已过期 、token已被顶下线、token已被踢下线 **[重要]** -- 修复:修复`StpUtil.getSessionByLoginId(String loginId)`方法转换key出错的bug,感谢群友 @(#°Д°)、@一米阳光 发现的bug -- 优化:修改方法`StpUtil.getSessionByLoginId(Object loginId)`的isCreate值默认为true -- 修改:`方法delSaSession`修改为`deleteSaSession`,更加语义化的函数名称 -- 新增:新增`StpUtil.getTokenName()`方法,更语义化的获取tokenName -- 新增:新增`SaTokenAction`框架行为Bean,方便重写逻辑 -- 优化:`Cookie操作`改为接口代理模式,使其可以被重写 -- 优化:文档里集成redis部分增加redis的pom依赖示例 -- 修复:登录验证-> `StpUtil.getLoginId_defaultNull()` 修复方法名错误的问题 -- 优化:优化`readme.md` -- 升级:开源协议修改为`MIT` - - -### 2020-9-7 @v1.4.0 -- 优化:修改一些函数、变量名称,使其更符合阿里java代码规范 -- 优化:`tokenValue`的读取优先级改为:`request` > `body` > `header` > `cookie` **[重要]** -- 新增:新增`isReadCookie`配置,决定是否从`cookie`里读取`token`信息 -- 优化:如果`isReadCookie`配置为`false`,那么在登录时也不会把`cookie`写入`cookie` -- 新增:新增`getSessionByLoginId(Object loginId, boolean isCreate)`方法 -- 修复:修复文档部分错误,修正群号码 - - -### 2020-5-2 @v1.3.0 -- 新增:新增 `StpUtil.checkLogin()` 方法,更符合语义化的鉴权方法 -- 新增:注册拦截器时可设置 `StpLogic` ,方便不同模块不同鉴权方式 -- 新增:抛出异常时增加 `loginType` 区分,方便多账号体系鉴权处理 -- 修复:修复启动时的版本字符画版本号打印不对的bug -- 修复:修复文档部分不正确之处 -- 新增:新增文档的友情链接 - - -### 2020-3-7 @v1.2.0 -- 新增:新增注解式验证,可在路由方法中使用注解进行权限验证 **[重要]** -- 参考:[注解式验证](use/at-check) - - -### 2020-2-12 @v1.1.0 -- 修复:修复`StpUtil.getLoginId(T defaultValue)`取值转换错误的bug - - -### 2020-2-4 @v1.0.0 -- 第一个版本出炉 diff --git a/sa-token-doc/doc/oauth2/oauth2-api.md b/sa-token-doc/doc/oauth2/oauth2-api.md deleted file mode 100644 index 658e5eb07..000000000 --- a/sa-token-doc/doc/oauth2/oauth2-api.md +++ /dev/null @@ -1,262 +0,0 @@ -# Sa-Token-OAuth2 Server端 API列表 -基于官方仓库的搭建示例,`OAuth2-Server`端会暴露出以下API,`OAuth2-Client`端可据此文档进行对接 - ---- - -## 1、模式一:授权码(Authorization Code) - -### 1.1、获取授权码 - -根据以下格式构建URL,引导用户访问 (复制时请注意删减掉相应空格和换行符) -``` url -http://sa-oauth-server.com:8001/oauth2/authorize - ?response_type=code - &client_id={value} - &redirect_uri={value} - &scope={value} - $state={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| response_type | 是 | 返回类型,这里请填写:code | -| client_id | 是 | 应用id | -| redirect_uri | 是 | 用户确认授权后,重定向的url地址 | -| scope | 否 | 具体请求的权限,多个用逗号隔开 | -| state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加 | - -注意点: -1. 如果用户在Server端尚未登录:会被转发到登录视图,你可以参照文档或官方示例自定义登录页面 -2. 如果scope参数为空,或者请求的权限用户近期已确认过,则无需用户再次确认,达到静默授权的效果,否则需要用户手动确认,服务器才可以下放code授权码 - -用户确认授权之后,会被重定向至`redirect_uri`,并追加code参数与state参数,形如: -``` url -redirect_uri?code={code}&state={state} -``` - -Code授权码具有以下特点: -1. 每次授权产生的Code码都不一样 -2. Code码用完即废,不能二次使用 -3. 一个Code的有效期默认为五分钟,超时自动作废 -4. 每次授权产生新Code码,会导致旧Code码立即作废,即使旧Code码尚未使用 - - -### 1.2、根据授权码获取Access-Token -获得Code码后,我们可以通过以下接口,获取到用户的`Access-Token`、`Refresh-Token`、`openid`等关键信息 - -``` url -http://sa-oauth-server.com:8001/oauth2/token - ?grant_type=authorization_code - &client_id={value} - &client_secret={value} - &code={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| grant_type | 是 | 授权类型,这里请填写:authorization_code | -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| code | 是 | 步骤1.1中获取到的授权码 | - -接口返回示例: - -``` js -{ - "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 - "msg": "ok", - "data": { - "access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ", // Access-Token值 - "refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66", // Refresh-Token值 - "expires_in": 7199, // Access-Token剩余有效期,单位秒 - "refresh_expires_in": 2591999, // Refresh-Token剩余有效期,单位秒 - "client_id": "1001", // 应用id - "scope": "userinfo", // 此令牌包含的权限 - "openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__" // openid - } -} -``` - - -### 1.3、根据 Refresh-Token 刷新 Access-Token (如果需要的话) -Access-Token的有效期较短,如果每次过期都需要重新授权的话,会比较影响用户体验,因此我们可以在后台通过`Refresh-Token` 刷新 `Access-Token` - -``` url -http://sa-oauth-server.com:8001/oauth2/refresh - ?grant_type=refresh_token - &client_id={value} - &client_secret={value} - &refresh_token={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| grant_type | 是 | 授权类型,这里请填写:refresh_token | -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| refresh_token | 是 | 步骤1.2中获取到的`Refresh-Token`值 | - -接口返回值同章节1.2,此处不再赘述 - - -### 1.4、回收 Access-Token (如果需要的话) -在Access-Token过期前主动将其回收 - -``` url -http://sa-oauth-server.com:8001/oauth2/revoke - ?client_id={value} - &client_secret={value} - &access_token={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| access_token | 是 | 步骤1.2中获取到的`Access-Token`值 | - -返回值样例: -``` js -{ - "code": 200, - "msg": "ok", - "data": null -} -``` - - -### 1.5、根据 Access-Token 获取相应用户的账号信息 -注:此接口为官方仓库模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数 - -``` url -http://sa-oauth-server.com:8001/oauth2/userinfo?access_token={value} -``` - -返回值样例: -``` js -{ - "code": 200, - "msg": "ok", - "data": { - "nickname": "shengzhang_", // 账号昵称 - "avatar": "http://xxx.com/1.jpg", // 头像地址 - "age": "18", // 年龄 - "sex": "男", // 性别 - "address": "山东省 青岛市 城阳区" // 所在城市 - } -} -``` - - -## 2、模式二:隐藏式(Implicit) - -根据以下格式构建URL,引导用户访问: -``` url -http://sa-oauth-server.com:8001/oauth2/authorize - ?response_type=token - &client_id={value} - &redirect_uri={value} - &scope={value} - $state={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| response_type | 是 | 返回类型,这里请填写:token | -| client_id | 是 | 应用id | -| redirect_uri | 是 | 用户确认授权后,重定向的url地址 | -| scope | 否 | 具体请求的权限,多个用逗号隔开 | -| state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加 | - -此模式会越过授权码的步骤,直接返回Access-Token到前端页面,形如: -``` url -redirect_uri#token=xxxx-xxxx-xxxx-xxxx -``` - -## 3、模式三:密码式(Password) -首先在Client端构建表单,让用户输入Server端的账号和密码,然后在Client端访问接口 -``` url -http://sa-oauth-server.com:8001/oauth2/token - ?grant_type=password - &client_id={value} - &username={value} - &password={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| grant_type| 是 | 返回类型,这里请填写:password| -| client_id | 是 | 应用id | -| username | 是 | 用户的Server端账号 | -| password | 否 | 用户的Server端密码 | - -接口返回示例: - -``` js -{ - "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 - "msg": "ok", - "data": { - "access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ", // Access-Token值 - "refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66", // Refresh-Token值 - "expires_in": 7199, // Access-Token剩余有效期,单位秒 - "refresh_expires_in": 2591999, // Refresh-Token剩余有效期,单位秒 - "client_id": "1001", // 应用id - "scope": "", // 此令牌包含的权限 - "openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__" // openid - } -} -``` - - -## 4、模式四:凭证式(Client Credentials) -以上三种模式获取的都是用户的 `Access-Token`,代表用户对第三方应用的授权, -在OAuth2.0中还有一种针对 Client级别的授权, 即:`Client-Token`,代表应用自身的资源授权 - -在Client端的后台访问以下接口: - -``` url -http://sa-oauth-server.com:8001/oauth2/client_token - ?grant_type=client_credentials - &client_id={value} - &client_secret={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| grant_type | 是 | 返回类型,这里请填写:client_credentials| -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| scope | 否 | 申请权限 | - -接口返回值样例: -``` js -{ - "code": 200, - "msg": "ok", - "data": { - "client_token": "HmzPtaNuIqGrOdudWLzKJRSfPadN497qEJtanYwE7ZvHQWDy0jeoZJuDIiqO", // Client-Token 值 - "expires_in": 7199, // Token剩余有效时间,单位秒 - "client_id": "1001", // 应用id - "scope": null // 包含权限 - } -} -``` - -注:`Client-Token`具有延迟作废特性,即:在每次获取最新`Client-Token`的时候,旧`Client-Token`不会立即过期,而是作为`Past-Token`再次储存起来, -资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”, 保证了服务的高可用 - diff --git a/sa-token-doc/doc/oauth2/oauth2-dev.md b/sa-token-doc/doc/oauth2/oauth2-dev.md deleted file mode 100644 index 4952d237f..000000000 --- a/sa-token-doc/doc/oauth2/oauth2-dev.md +++ /dev/null @@ -1,46 +0,0 @@ -# Sa-Token-OAuth2 Server端 二次开发用到的所有函数说明 - -官方示例只提供了基本的授权流程,以及userinfo资源的开放,如果您需要开放更多的接口,则二次开发时用到以下相关API方法 - ---- - -## Sa-OAuth2 模块常用方法 - -``` java -// 根据 id 获取 Client 信息, 如果 Client 为空,则抛出异常 -SaOAuth2Util.checkClientModel(clientId); - -// 获取 Access-Token,如果Access-Token为空则抛出异常 -SaOAuth2Util.checkAccessToken(accessToken); - -// 获取 Client-Token,如果Client-Token为空则抛出异常 -SaOAuth2Util.checkClientToken(clientToken); - -// 获取 Access-Token 所代表的LoginId -SaOAuth2Util.getLoginIdByAccessToken(accessToken); - -// 校验:指定 Access-Token 是否具有指定 Scope -SaOAuth2Util.checkScope(accessToken, scopes); - -// 根据 code码 生成 Access-Token -SaOAuth2Util.generateAccessToken(code); - -// 根据 Refresh-Token 生成一个新的 Access-Token -SaOAuth2Util.refreshAccessToken(refreshToken); - -// 构建 Client-Token -SaOAuth2Util.generateClientToken(clientId, scope); - -// 回收 Access-Token -SaOAuth2Util.revokeAccessToken(accessToken); - -// 持久化:用户授权记录 -SaOAuth2Util.saveGrantScope(clientId, loginId, scope); - -// 获取:Refresh-Token Model -SaOAuth2Util.getRefreshToken(refreshToken); -``` - -详情请参考源码:[码云:SaOAuth2Util.java](https://gitee.com/dromara/sa-token/blob/dev/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Util.java) - - diff --git a/sa-token-doc/doc/oauth2/oauth2-server.md b/sa-token-doc/doc/oauth2/oauth2-server.md deleted file mode 100644 index 6941b7d41..000000000 --- a/sa-token-doc/doc/oauth2/oauth2-server.md +++ /dev/null @@ -1,178 +0,0 @@ -# 搭建OAuth2-Server - ---- - -### 1、准备工作 -首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: -``` url -127.0.0.1 sa-oauth-server.com -127.0.0.1 sa-oauth-client.com -``` - - -### 2、引入依赖 -创建SpringBoot项目 `sa-token-demo-oauth2-server`(不会的同学自行百度或参考仓库示例),添加pom依赖: - -``` xml - - - cn.dev33 - sa-token-spring-boot-starter - ${sa.top.version} - - - - - cn.dev33 - sa-token-oauth2 - ${sa.top.version} - -``` - -### 3、开放服务 -1、新建 `SaOAuth2TemplateImpl` -``` java -/** - * Sa-Token OAuth2.0 整合实现 - */ -@Component -public class SaOAuth2TemplateImpl extends SaOAuth2Template { - - // 根据 id 获取 Client 信息 - @Override - public SaClientModel getClientModel(String clientId) { - // 此为模拟数据,真实环境需要从数据库查询 - if("1001".equals(clientId)) { - return new SaClientModel() - .setClientId("10001") - .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") - .setAllowUrl("*") - .setContractScope("userinfo"); - } - return null; - } - - // 根据ClientId 和 LoginId 获取openid - @Override - public String getOpenid(String clientId, Object loginId) { - // 此为模拟数据,真实环境需要从数据库查询 - return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"; - } - -} -``` - -2、新建`SaOAuth2ServerController` -``` java -/** - * Sa-OAuth2 Server端 控制器 - */ -@RestController -public class SaOAuth2ServerController { - - // 处理所有OAuth相关请求 - @RequestMapping("/oauth2/*") - public Object request() { - System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl()); - return SaOAuth2Handle.serverRequest(); - } - - // Sa-OAuth2 定制化配置 - @Autowired - public void setSaOAuth2Config(SaOAuth2Config cfg) { - cfg. - // 配置:未登录时返回的View - setNotLoginView(() -> { - String msg = "当前会话在SSO-Server端尚未登录,请先访问" - + " doLogin登录 " - + "进行登录之后,刷新页面开始授权"; - return msg; - }). - // 配置:登录处理函数 - setDoLoginHandle((name, pwd) -> { - if("sa".equals(name) && "123456".equals(pwd)) { - StpUtil.login(10001); - return SaResult.ok(); - } - return SaResult.error("账号名或密码错误"); - }). - // 配置:确认授权时返回的View - setConfirmView((clientId, scope) -> { - String msg = "

应用 " + clientId + " 请求授权:" + scope + "

" - + "

请确认: 确认授权

" - + "

确认之后刷新页面

"; - return msg; - }) - ; - } - - // 全局异常拦截 - @ExceptionHandler - public SaResult handlerException(Exception e) { - e.printStackTrace(); - return SaResult.error(e.getMessage()); - } - -} -``` -注意:在`setDoLoginHandle`函数里如果要获取name, pwd以外的参数,可通过`SaHolder.getRequest().getParam("xxx")`来获取 - -3、创建启动类: -``` java -/** - * 启动:Sa-OAuth2 Server端 - */ -@SpringBootApplication -public class SaOAuth2ServerApplication { - public static void main(String[] args) { - SpringApplication.run(SaOAuth2ServerApplication.class, args); - System.out.println("\nSa-Token-OAuth Server端启动成功"); - } -} -``` -启动项目 - - -### 4、访问测试 - -1、由于暂未搭建Client端,我们可以使用Sa-Token官网作为重定向URL进行测试: -``` url -http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-token.dev33.cn/&scope=userinfo -``` - -2、由于首次访问,我们在OAuth-Server端暂未登录,会被转发到登录视图 - -![sa-oauth2-server-login-view](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-server-login-view.png 's-w-sh') - -3、点击doLogin进行登录之后刷新页面,会提示我们确认授权 -![sa-oauth2-server-login-view](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-server-scope.png 's-w-sh') - -4、点击确认授权之后刷新页面,我们会被重定向至 redirect_uri 页面,并携带了code参数 - -![sa-oauth2-server-code](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-server-code.png 's-w-sh') - -4、我们拿着code参数,访问以下地址: -``` url -http://sa-oauth-server.com:8001/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code} -``` - -将得到 `Access-Token`、`Refresh-Token`、`openid`等授权信息 - -![sa-oauth2-server-token](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-server-token.png 's-w-sh') - -测试完毕 - - -### 5、运行官方示例 -以上代码只是简单模拟了一下OAuth2.0的授权流程,现在,我们运行一下官方示例,里面有制作好的UI界面 - -- OAuth2-Server端: `/sa-token-demo/sa-token-demo-oauth2-server/` [源码链接](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-oauth2-server)
-- OAuth2-Client端: `/sa-token-demo/sa-token-demo-oauth2-client/` [源码链接](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-oauth2-client)
- -依次启动`OAuth2-Server` 与 `OAuth2-Client`,然后从浏览器访问:[http://sa-oauth-client.com:8002](http://sa-oauth-client.com:8002) - -![sa-oauth2-client-index](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-client-index.png 's-w-sh') - -如图,可以针对OAuth2.0四种模式进行详细测试 - - diff --git a/sa-token-doc/doc/oauth2/readme.md b/sa-token-doc/doc/oauth2/readme.md deleted file mode 100644 index 11022c34c..000000000 --- a/sa-token-doc/doc/oauth2/readme.md +++ /dev/null @@ -1,31 +0,0 @@ -# Sa-Token-OAuth2.0 模块 - ---- - -### 什么是OAuth2.0?解决什么问题? - -简单来讲,OAuth2.0的应用场景可以理解为单点登录的升级版,单点登录解决了多个系统间会话的共享,OAuth2.0在此基础上增加了应用之间的权限控制 -(SO:有些系统采用OAuth2.0模式实现了单点登录,但这总给人一种“杀鸡焉用宰牛刀”的感觉) - -有关OAuth2.0的设计思想网上教程较多,此处不再重复赘述,详细可参考博客: -[OAuth2.0 简单解释](https://www.ruanyifeng.com/blog/2019/04/oauth_design.html) - - -Sa-OAuth2 模块基于 [RFC-6749 标准](https://tools.ietf.org/html/rfc6749) 编写,通过Sa-OAuth2你可以非常轻松的实现系统的OAuth2.0授权认证 - - -### OAuth2.0 四种模式 - -基于不同的使用场景,OAuth2.0设计了四种模式: - -1. 授权码(Authorization Code):OAuth2.0标准授权步骤,Server端向Client端下放Code码,Client端再用Code码换取授权Token -2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server端使用URL重定向方式直接将Token下放到Client端页面 -3. 密码式(Password):Client直接拿着用户的账号密码换取授权Token -4. 客户端凭证(Client Credentials):Server端针对Client级别的Token,代表应用自身的资源授权 - -![https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-setup.png](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-setup.png) - -接下来我们将通过简单示例演示如何在Sa-OAuth2中完成这四种模式的对接: [搭建OAuth2-Server](/oauth2/oauth2-server) - - - diff --git a/sa-token-doc/doc/plugin/alone-redis.md b/sa-token-doc/doc/plugin/alone-redis.md deleted file mode 100644 index 9dd817c38..000000000 --- a/sa-token-doc/doc/plugin/alone-redis.md +++ /dev/null @@ -1,100 +0,0 @@ -# Sa-Token-Alone-Redis 独立Redis插件 ---- - -Sa-Token默认的Redis集成方式会把权限数据和业务缓存放在一起,但在部分场景下我们需要将他们彻底分离开来,比如: - -> 搭建两个Redis服务器,一个专门用来做业务缓存,另一台专门存放Sa-Token权限数据 - -要将Sa-Token的数据单独抽离出来很简单,你只需要为Sa-Token单独配置一个Redis连接信息即可 - ---- - - -### 1、首先引入Alone-Redis依赖 - -``` xml - - - cn.dev33 - sa-token-alone-redis - ${sa.top.version} - -``` - - -### 2、然后在application.yml中增加配置 -``` yml -# Sa-Token配置 -sa-token: - # Token名称 - token-name: satoken - # Token有效期 - timeout: 2592000 - # Token风格 - token-style: uuid - - # 配置Sa-Token单独使用的Redis连接 - alone-redis: - # Redis数据库索引(默认为0) - database: 2 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - password: - # 连接超时时间(毫秒) - timeout: 10ms - -spring: - # 配置业务使用的Redis连接 - redis: - # Redis数据库索引(默认为0) - database: 0 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - password: - # 连接超时时间(毫秒) - timeout: 10ms -``` - -具体可参考示例:[码云:application.yml](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml) - - -### 3、测试 -新建Controller测试一下 -``` java -@RestController -@RequestMapping("/test/") -public class TestController { - - @Autowired - StringRedisTemplate stringRedisTemplate; - - // 测试Sa-Token缓存 - @RequestMapping("login") - public AjaxJson login(@RequestParam(defaultValue="10001") String id) { - System.out.println("--------------- 测试Sa-Token缓存"); - StpUtil.login(id); - return AjaxJson.getSuccess(); - } - - // 测试业务缓存 - @RequestMapping("test") - public AjaxJson test() { - System.out.println("--------------- 测试业务缓存"); - stringRedisTemplate.opsForValue().set("hello", "Hello World"); - return AjaxJson.getSuccess(); - } - -} -``` - -分别访问两个接口,观察Redis中增加的数据 - -![alone-redis](https://oss.dev33.cn/sa-token/doc/alone-redis.png 's-w') - -测试完毕! diff --git a/sa-token-doc/doc/plugin/aop-at.md b/sa-token-doc/doc/plugin/aop-at.md deleted file mode 100644 index 26b2339cb..000000000 --- a/sa-token-doc/doc/plugin/aop-at.md +++ /dev/null @@ -1,30 +0,0 @@ -# AOP注解鉴权 ---- - -在 [注解式鉴权](/use/at-check) 章节,我们非常轻松的实现了注解鉴权, -但是默认的拦截器模式却有一个缺点,那就是无法在`Controller层`以外的代码使用进行校验 - -因此Sa-Token提供AOP插件,你只需在`pom.xml`里添加如下依赖,便可以在任意层级使用注解鉴权 - -``` xml - - - cn.dev33 - sa-token-spring-aop - ${sa.top.version} - -``` - - -#### 注意点: -- 使用拦截器模式,只能把注解写在`Controller层`,使用AOP模式,可以将注解写在任意层级
-- **拦截器模式和AOP模式不可同时集成**,否则会在`Controller层`发生一个注解校验两次的bug - - - - - - - - - diff --git a/sa-token-doc/doc/plugin/dao-extend.md b/sa-token-doc/doc/plugin/dao-extend.md deleted file mode 100644 index d3b1deb87..000000000 --- a/sa-token-doc/doc/plugin/dao-extend.md +++ /dev/null @@ -1,22 +0,0 @@ -# 持久层扩展 ---- - -对于权限框架来讲,最容易碰到的扩展点便是数据存储方式,为了方便对接不同的缓存中间件,Sa-Token将所有数据持久化操作抽象到SaTokenDao接口, -开发者要对接不同的平台只需要实现此接口即可,接口签名:[SaTokenDao.java](https://gitee.com/dromara/sa-token/blob/dev/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java) - -框架已提供的集成包包括: - -- 默认方式:储存在内存中,位于core核心包 -- sa-token-dao-redis:Redis集成包,使用jdk默认序列化方式 -- sa-token-dao-redis-jackson:Redis集成包,使用jackson序列化方式 - -有关Redis集成,详细参考:[集成Redis](/up/integ-redis),更多存储方式欢迎提交PR - - - - - - - - - diff --git a/sa-token-doc/doc/plugin/temp-token.md b/sa-token-doc/doc/plugin/temp-token.md deleted file mode 100644 index 979b23ce3..000000000 --- a/sa-token-doc/doc/plugin/temp-token.md +++ /dev/null @@ -1,72 +0,0 @@ -# 临时Token令牌验证 - ---- - -### 适用场景 - -在部分业务场景,我们需要一种临时授权的能力,即:一个token的有效期并不需要像登录有效期那样需要[七天、三十天],而是仅仅需要 [五分钟、半小时] - -举个比较明显的例子:超链接邀请机制 - -比如说你在一个游戏中创建一个公会 `(id=10014)`,现在你想邀请你的好朋友加入这个公会,在你点击 **`[邀请]`** 按钮时,系统为你生成一个连接: - -``` xml -http://xxx.com/apply?id=10014 -``` - -接着,你的好朋友点击这个链接,加入了你的工会 - -那么,系统是如何识别这个链接对应的工会是10014呢?很明显,我们可以观察出,这个链接的尾部有个id参数值为10014,这便是系统识别的关键 - -此时你可能眉头一紧,就这么简单?那我如果手动更改一下尾部的参数改成10015,然后我再一点,岂不是就可以偷偷加入别人的工会了? - -你想的没错,如果这个游戏的架构设计者采用上述方案完成功能的话,这个邀请机制就轻松的被你攻破了 - -但是很明显,正常的商业项目一般不会拉跨到这种地步,比较常见的方案是,对这个公会id做一个token映射,最终你看到链接一般是这样的: - -``` xml -http://xxx.com/apply?token=oEwQBnglXDoGraSJdGaLooPZnGrk -``` - -后面那一串字母是乱打出来的,目的是为了突出它的随机性,即:使用一个随机的token来代替明文显示真正的数据 - -在用户点击这个链接之后,服务器便可根据这个token解析出真正公会id (10014) ,至于伪造?全是随机的你怎么伪造?你又不知道10015会随机出一个什么样的Token - -而且为了安全性,这个token的有效期一般不会太长,给你预留五分钟、半小时的时间足够你点击它即可 - - -### 相关API - -**[sa-token-temp临时验证模块]** 已内嵌到核心包,无需引入其它依赖即可使用 - -``` java -// 根据 value 创建一个 token -String token = SaTempUtil.createToken("10014", 200); - -// 解析 token 获取 value,并转换为指定类型 -String value = SaTempUtil.parseToken(token, String.class); - -// 获取指定 token 的剩余有效期,单位:秒 -SaTempUtil.getTimeout(token); -``` - - -### 集成jwt -提到 [临时Token验证],你是不是想到一个专门干这件事的框架?对,就是JWT! - -**[sa-token-temp]** 模块允许以JWT作为逻辑内核完成工作,你只需要引入以下依赖,所有上层API保持不变 - -``` xml - - cn.dev33 - sa-token-temp-jwt - ${sa.top.version} - -``` - -并在配置文件中配置上jwt秘钥 **`(必填!)`** -``` java -sa-token: - # sa-token-temp-jwt 模块的秘钥 (随便乱摁几个字母就行了) - jwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk -``` diff --git a/sa-token-doc/doc/sso/_sidebar.md b/sa-token-doc/doc/sso/_sidebar.md deleted file mode 100644 index caca6efdf..000000000 --- a/sa-token-doc/doc/sso/_sidebar.md +++ /dev/null @@ -1,12 +0,0 @@ - - -- **单点登录** - - [单点登录简述](/sso/readme) - - [SSO模式一 共享Cookie同步会话](/sso/sso-type1) - - [SSO模式二 URL重定向传播会话](/sso/sso-type2) - - [SSO模式三 Http请求获取会话](/sso/sso-type3) - - - - diff --git a/sa-token-doc/doc/sso/readme.md b/sa-token-doc/doc/sso/readme.md deleted file mode 100644 index e1b5bc667..000000000 --- a/sa-token-doc/doc/sso/readme.md +++ /dev/null @@ -1,40 +0,0 @@ -# Sa-Token-SSO 单点登录模块 - ---- - -### 什么是单点登录?解决什么问题? - -举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, -为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统 - -单点登录——就是为了解决这个问题而生! - -简而言之,单点登录可以做到:**`在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。`** - - -### 架构选型 -对于单点登录,网上教程大多以CAS模式为主,其实对于不同的系统架构,实现单点登录的步骤也大为不同,Sa-Token由简入难将其划分为三种模式: - -| 系统架构 | 采用模式 | 简介 | 文档链接 | -| :-------- | :-------- | :-------- | :-------- | -| 前端同域 + 后端同 Redis | 模式一 | 共享Cookie同步会话 | [文档](/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso1) | -| 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [文档](/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso2-server) | -| 前端不同域 + 后端 不同Redis | 模式三 | Http请求获取会话 | [文档](/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-sso3-server) | - - -1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com` -2. 后端同Redis:就是指多个系统可以连接同一个Redis,其它的缓存数据中心亦可。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案,详情戳:[Alone独立Redis插件](http://sa-token.dev33.cn/doc/index.html#/plugin/alone-redis) -3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成) -4. 技术选型一定要根据系统架构对症下药,切不可胡乱选择 - - -### Sa-Token-SSO 特性 -1. API简单易用,文档介绍详细,且提供直接可用的集成示例 -2. 支持三种模式,不论是否跨域、是否共享Redis,都可以完美解决 -3. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝`Ticket劫持`、`Token窃取`等常见攻击手段(文档讲述攻击原理和防御手段) -4. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:`http://a.com?id=1&name=2`,登录成功之后就变成了:`http://a.com?id=1`,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面 -5. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合…… -6. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发 - - - diff --git a/sa-token-doc/doc/sso/sso-cd.md b/sa-token-doc/doc/sso/sso-cd.md deleted file mode 100644 index 878cc3e86..000000000 --- a/sa-token-doc/doc/sso/sso-cd.md +++ /dev/null @@ -1,132 +0,0 @@ -# Sa-Token-SSO整合-常见问题总结 - ---- - - -### 一、何时引导用户去登录? - -以下解决方案三选一: - -##### 1.1、前端按钮跳转 -前端页面准备一个**`[登录]`**按钮,当用户点击按钮时,跳转到登录接口 -``` js -登录 -``` - -##### 1.2、后端拦截重定向 -在后端注册全局过滤器(或拦截器、或全局异常处理),拦截需要登录后才能访问的页面资源,将未登录的访问重定向至登录接口 -``` java -/** - * Sa-Token 配置类 - */ -@Configuration -public class SaTokenConfigure implements WebMvcConfigurer { - /** 注册 [Sa-Token全局过滤器] */ - @Bean - public SaServletFilter getSaServletFilter() { - return new SaServletFilter() - .addInclude("/**") - .addExclude("/sso/*", "/favicon.ico") - .setAuth(r -> { - if(StpUtil.isLogin() == false) { - String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString()); - SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back)); - SaRouter.back(); - } - }) - ; - } -} -``` - -##### 1.3、后端拦截 + 前端跳转 -首先,后端仍需要提供拦截,但是不直接引导用户重定向,而是返回未登录的提示信息 -``` java -/** - * Sa-Token 配置类 - */ -@Configuration -public class SaTokenConfigure implements WebMvcConfigurer { - /** 注册 [Sa-Token全局过滤器] */ - @Bean - public SaServletFilter getSaServletFilter() { - return new SaServletFilter() - .addInclude("/**") - .addExclude("/sso/*", "/favicon.ico") - .setAuth(r -> { - if(StpUtil.isLogin() == false) { - // 与前端约定好,code=401时代表会话未登录 - SaRouter.back(SaResult.ok().setCode(401)); - } - }) - ; - } -} -``` - -前端接受到返回结果 `code=401` 时,开始跳转至登录接口 -``` js -if(res.code == 401) { - location.href = '/sso/login?back=' + encodeURIComponent(location.href); -} -``` - -这种方案比较适合以 Ajax 访问的 RestAPI 接口重定向 - - - - -### 二、定制化开发 - -##### 2.1、如何自定义登录视图? -- 方式一:在demo示例中直接更改页面代码 -- 方式二:在配置中配置登录视图地址 - -``` java -cfg.sso -// 配置:未登录时返回的View -.setNotLoginView(() -> { - return new ModelAndView("xxx.html"); -}) -``` - -##### 2.2、如何自定义登录API的接口? -根据需求点选择解决方案: - -**2.2.1、如果只是想在 setDoLoginHandle 函数里获取除 name、pwd 以外的参数?** -``` java -// 在任意代码处获取前端提交的参数 -String xxx = SaHolder.getRequest().getParam("xxx"); -``` - -**2.2.2、想完全自定义一个接口来接受前端登录请求?** -``` java -// 直接定义一个拦截路由为 `/sso/doLogin` 的接口即可 -@RequestMapping("/sso/doLogin") -public SaResult ss(String name, String pwd) { - System.out.println("------ 请求进入了自定义的API接口 ---------- "); - if("sa".equals(name) && "123456".equals(pwd)) { - StpUtil.login(10001); - return SaResult.ok("登录成功!"); - } - return SaResult.error("登录失败!"); -} -``` - -**2.2.3、不想使用`/sso/doLogin`这个接口,想自定义一个API地址?** - -答:直接在前端更改点击按钮时 Ajax 的请求地址即可 - - -### 三、常见疑问 - -##### 问:在模式一与模式二中,Client端 必须通过 Alone-Redis 插件来访问Redis吗? - -答:不必须,只是推荐,权限缓存与业务缓存分离后会减少SSO-Redis的访问压力,且可以避免多个Client端的缓存读写冲突 - - -##### 问:将旧有系统改造为单点登录时,应该注意哪些? -建议不要把其中一个系统改造为SSO服务端,而是新起一个项目作为Server端,所有旧有项目全部作为Client端与此对接 - - - diff --git a/sa-token-doc/doc/sso/sso-type1.md b/sa-token-doc/doc/sso/sso-type1.md deleted file mode 100644 index dfa8cbbc2..000000000 --- a/sa-token-doc/doc/sso/sso-type1.md +++ /dev/null @@ -1,122 +0,0 @@ -# SSO模式一 共享Cookie同步会话 - -如果我们的多个系统可以做到:前端同域、后端同Redis,那么便可以使用 **`[共享Cookie同步会话]`** 的方式做到单点登录 - ---- - -### 0、解决思路? - -首先我们分析一下多个系统之间,为什么无法同步登录状态? -1. 前端的`Token`无法在多个系统下共享 -2. 后端的`Session`无法在多个系统间共享 - -所以单点登录第一招,就是对症下药: -1. 使用`共享Cookie`来解决Token共享问题 -2. 使用`Redis`来解决Session共享问题 - -所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名`stp.com`下的Cookie,在`s1.stp.com`、`s2.stp.com`等子域名都是可以共享访问的 - -而共享Redis,并不需要我们把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **[权限缓存与业务缓存分离]** 的解决方案,详情戳:[Alone独立Redis插件](/plugin/alone-redis) - -> PS:这里建议不要用B项目去连接A项目的Redis,也不要A项目连接B项目的Redis,而是抽离出一个单独的 SSO-Redis,A 和 B 一起连接这个 SSO-Redis - -OK,所有理论就绪,下面开始实战 - -> Sa-Token整合同域单点登录非常简单,相比于正常的登录,你只需增加配置 `sa-token.cookie-domain=xxx.com` 指定一下Cookie写入时的父级域名即可
-> 整合示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso1/`,如遇到难点可结合源码进行测试学习 - - - -### 1、准备工作 - -首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: -``` url -127.0.0.1 s1.stp.com -127.0.0.1 s2.stp.com -127.0.0.1 s3.stp.com -``` - -### 2、指定Cookie的作用域 -在`s1.stp.com`访问服务器,其Cookie也只能写入到`s1.stp.com`下,为了将Cookie写入到其父级域名`stp.com`下,我们需要新增配置: -``` yml -sa-token: - # 写入Cookie时显式指定的作用域, 用于单点登录二级域名共享Cookie - cookie-domain: stp.com -``` - -### 3、新增测试Controller -新建`SsoController.java`控制器,写入代码: -``` java -/** - * 测试: 同域单点登录 - * @author kong - */ -@RestController -@RequestMapping("/sso/") -public class SsoController { - - // 测试:进行登录 - @RequestMapping("doLogin") - public AjaxJson doLogin(@RequestParam(defaultValue = "10001") String id) { - System.out.println("---------------- 进行登录 "); - StpUtil.login(id); - return AjaxJson.getSuccess("登录成功: " + id); - } - - // 测试:是否登录 - @RequestMapping("isLogin") - public AjaxJson isLogin() { - System.out.println("---------------- 是否登录 "); - boolean isLogin = StpUtil.isLogin(); - return AjaxJson.getSuccess("是否登录: " + isLogin); - } - -} -``` - -``` java -// 启动类 -@SpringBootApplication -public class SaSsoApplication { - public static void main(String[] args) { - SpringApplication.run(SaSsoApplication.class, args); - System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); - } -} -``` - - -### 4、访问测试 -启动项目,依次访问: -- [http://s1.stp.com:8081/sso/isLogin](http://s1.stp.com:8081/sso/isLogin) -- [http://s2.stp.com:8081/sso/isLogin](http://s2.stp.com:8081/sso/isLogin) -- [http://s3.stp.com:8081/sso/isLogin](http://s3.stp.com:8081/sso/isLogin) - -均返回以下结果: - -![sso-type1-wd.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type1-wd.png 's-w-sh') - -现在访问任意节点的登录接口:[http://s1.stp.com:8081/sso/doLogin](http://s1.stp.com:8081/sso/doLogin) - -![sso-type1-login.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type1-login.png 's-w-sh') - -然后再次刷新上面三个测试接口,均可以得到以下结果: - -![sso-type1-yd.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type1-yd.png 's-w-sh') - -测试完毕 - - -### 5、跨域模式下的解决方案 - -如上,我们使用极其简单的步骤实现了同域下的单点登录,聪明如你😏,马上想到了这种模式有着一个不小的限制: - -> 所有子系统的域名,必须同属一个父级域名 - -如果我们的子系统在完全不同的域名下,我们又该怎么完成单点登录功能呢? - -且往下看,[SSO模式二:URL重定向传播会话](/sso/sso-type2) - - - - diff --git a/sa-token-doc/doc/sso/sso-type2.md b/sa-token-doc/doc/sso/sso-type2.md deleted file mode 100644 index 74522ab3e..000000000 --- a/sa-token-doc/doc/sso/sso-type2.md +++ /dev/null @@ -1,374 +0,0 @@ -# SSO模式二 URL重定向传播会话 - -如果我们的多个系统:部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 **`[URL重定向传播会话]`** 的方式做到单点登录 - - -### 0、解题思路 - -首先我们再次复习一下多个系统之间,为什么无法同步登录状态? - -1. 前端的`Token`无法在多个系统下共享 -2. 后端的`Session`无法在多个系统间共享 - -关于第二点,我们已经在"SSO模式一"章节中阐述,使用 [Alone独立Redis插件](/plugin/alone-redis) 做到权限缓存直连SSO-Redis数据中心,在此不再赘述 - -而第一点,才是我们解决问题的关键所在,在跨域模式下,意味着"共享Cookie方案"的失效,我们必须采用一种新的方案来传递Token - -1. 用户在 子系统 点击`[登录]`按钮 -2. 用户跳转到子系统登录页面,并携带`back参数`记录初始页面URL -3. 子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带`redirect参数`记录子系统的登录页URL -4. 用户在SSO认证中心尚未登录,开始登录 -5. 用户在SSO认证中心登录成功,重定向至子系统的登录页URL,并携带`ticket码` -6. 子系统使用ticket码从`SSO-Redis`中获取账号id,并在子系统登录此账号会话 -7. 子系统将用户再次重定向至最初始的`back`页面 - -整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击`[登录]`按钮,由于此用户在SSO认证中心已有会话登录, -所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。 - -下面我们按照步骤依次完成上述过程 - -### 1、准备工作 -首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: -``` url -127.0.0.1 sa-sso-server.com -127.0.0.1 sa-sso-client1.com -127.0.0.1 sa-sso-client2.com -127.0.0.1 sa-sso-client3.com -``` - - -### 2、搭建SSO-Server认证中心 - -> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso2-server/`,如遇到难点可结合源码进行测试学习 - -##### 2.1、创建SSO-Server端项目 -创建SpringBoot项目 `sa-token-demo-sso-server`(不会的同学自行百度或参考仓库示例),添加pom依赖: - -``` xml - - - cn.dev33 - sa-token-spring-boot-starter - ${sa.top.version} - - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - - - org.apache.commons - commons-pool2 - -``` - -##### 2.2、创建SSO-Server端认证接口 -``` java -/** - * Sa-Token-SSO Server端 Controller - */ -@RestController -public class SsoServerController { - - // SSO-Server端:处理所有SSO相关请求 - @RequestMapping("/sso/*") - public Object ssoRequest() { - return SaSsoHandle.serverRequest(); - } - - // 配置SSO相关参数 - @Autowired - private void configSso(SaTokenConfig cfg) { - cfg.sso - // 配置:未登录时返回的View - .setNotLoginView(() -> { - String msg = "当前会话在SSO-Server端尚未登录,请先访问" - + " doLogin登录 " - + "进行登录之后,刷新页面开始授权"; - return msg; - }) - // 配置:登录处理函数 - .setDoLoginHandle((name, pwd) -> { - // 此处仅做模拟登录,真实环境应该查询数据进行登录 - if("sa".equals(name) && "123456".equals(pwd)) { - StpUtil.login(10001); - return SaResult.ok("登录成功!"); - } - return SaResult.error("登录失败!"); - }) - ; - } - -} -``` -注意:在`setDoLoginHandle`函数里如果要获取name, pwd以外的参数,可通过`SaHolder.getRequest().getParam("xxx")`来获取 - -##### 2.3、application.yml配置 -``` yml -# 端口 -server: - port: 9000 - -# Sa-Token配置 -sa-token: - # SSO-相关配置 - sso: - # Ticket有效期 (单位: 秒),默认五分钟 - ticket-timeout: 300 - # 所有允许的授权回调地址 (此处为了方便测试配置为*,线上生产环境一定要配置为详细地地址) - allow-url: "*" - -spring: - # Redis配置 - redis: - # Redis数据库索引(默认为0) - database: 1 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - password: -``` -注意点:`allow-url`为了方便测试配置为`*`,线上生产环境一定要配置为详细URL地址 (详见下方“配置域名校验”) - -##### 2.4、创建SSO-Server端启动类 -``` java -@SpringBootApplication -public class SaSsoServerApplication { - public static void main(String[] args) { - SpringApplication.run(SaSsoServerApplication.class, args); - System.out.println("\nSa-Token-SSO 认证中心启动成功"); - } -} -``` - - -### 3、搭建SSO-Client应用端 - -> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso2-client/`,如遇到难点可结合源码进行测试学习 - -##### 3.1、创建SSO-Client端项目 -创建一个SpringBoot项目 `sa-token-demo-sso-client`,添加pom依赖: -``` xml - - - cn.dev33 - sa-token-spring-boot-starter - ${sa.top.version} - - - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - - - org.apache.commons - commons-pool2 - - - - - cn.dev33 - sa-token-alone-redis - ${sa.top.version} - -``` - - -##### 3.2、创建SSO-Client端认证接口 -``` java - -/** - * Sa-Token-SSO Client端 Controller - */ -@RestController -public class SsoClientController { - - // SSO-Client端:首页 - @RequestMapping("/") - public String index() { - String str = "

Sa-Token SSO-Client 应用端

" + - "

当前会话是否登录:" + StpUtil.isLogin() + "

" + - "

登录 " + - "注销

"; - return str; - } - - // SSO-Client端:处理所有SSO相关请求 - @RequestMapping("/sso/*") - public Object ssoRequest() { - return SaSsoHandle.clientRequest(); - } - -} -``` - -##### 3.3、配置SSO认证中心地址 -你需要在 `application.yml` 配置如下信息: -``` yml -# 端口 -server: - port: 9001 - -# sa-token配置 -sa-token: - # SSO-相关配置 - sso: - # SSO-Server端 单点登录地址 - auth-url: http://sa-sso-server.com:9000/sso/auth - # 是否打开单点注销接口 - is-slo: true - - # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) - alone-redis: - # Redis数据库索引 (默认为0) - database: 1 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - password: -``` -注意点:`sa-token.alone-redis` 的配置需要和SSO-Server端连接同一个Redis(database也要一样) - -##### 3.4、写启动类 -``` java -@SpringBootApplication -public class SaSsoClientApplication { - public static void main(String[] args) { - SpringApplication.run(SaSsoClientApplication.class, args); - System.out.println("\nSa-Token-SSO Client端启动成功"); - } -} -``` -启动项目 - - -### 4、测试访问 - -(1) 依次启动SSO-Server与SSO-Client端,然后从浏览器访问:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/) - -![sso-client-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client-index.png 's-w-sh') - -(2) 首次打开,提示当前未登录,我们点击**`登录`** 按钮,页面会被重定向到登录中心 - -![sso-server-auth.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-auth.png 's-w-sh') - -(3) SSO-Server提示我们在认证中心尚未登录,我们点击 **`doLogin登录`**按钮进行模拟登录 - -![sso-server-dologin.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-dologin.png 's-w-sh') - -(4) SSO-Server认证中心登录成功,我们回到刚才的页面刷新页面 - -![sso-client-index-ok.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client-index-ok.png 's-w-sh') - -(5) 页面被重定向至`Client`端首页,并提示登录成功,至此,`Client1`应用已单点登录成功! - -(6) 我们再次访问`Client2`:[http://sa-sso-client2.com:9001/](http://sa-sso-client2.com:9001/) - -![sso-client2-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client2-index.png 's-w-sh') - -(7) 提示未登录,我们点击**`登录`**按钮,会直接提示登录成功 - -![sso-client2-index-ok.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client2-index-ok.png 's-w-sh') - -(8) 同样的方式,我们打开`Client3`,也可以直接登录成功:[http://sa-sso-client3.com:9001/](http://sa-sso-client3.com:9001/) - -![sso-client3-index-ok.png](https://oss.dev33.cn/sa-token/doc/sso/sso-client3-index-ok.png 's-w-sh') - -至此,测试完毕! - -可以看出,除了在`Client1`端我们需要手动登录一次之外,在`Client2端`和`Client3端`都是可以无需验证,直接登录成功的。 - -我们可以通过 F12控制台 Netword跟踪整个过程 - -![sso-genzong](https://oss.dev33.cn/sa-token/doc/sso/sso-genzong.png 's-w-sh') - - -### 5、运行官方仓库 - -以上示例,虽然完整的复现了单点登录的过程,但是页面还是有些简陋,我们可以运行一下官方仓库的示例,里面有制作好的登录页面 - -> 下载官方示例,依次运行 `/sa-token-demo/sa-token-demo-sso-client/` 和 `/sa-token-demo/sa-token-demo-sso-server/`,访问:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/) - -![sso-server-login-hua](https://oss.dev33.cn/sa-token/doc/sso/sso-server-login-hua.png 's-w-sh') - -默认测试密码:`sa / 123456`,其余流程保持不变 - - -### 6、配置域名校验 - -##### 6.1、Ticket劫持攻击 -在以上的SSO-Server端示例中,配置项 `sa-token.sso.allow-url=*` 意为配置所有允许的Client端授权地址,不在此配置项中的URL将无法单点登录成功 - -以上示例为了方便测试被配置为*,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被ticket劫持的风险 - -假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL - -> [http://sa-sso-server.com:9000/sso/auth?redirect=https://www.baidu.com/](http://sa-sso-server.com:9000/sso/auth?redirect=https://www.baidu.com/) - -当不知情的小红被诱导访问了这个URL时,它将被重定向至百度首页 - -![sso-ticket-jc](https://oss.dev33.cn/sa-token/doc/sso/sso-ticket-jc.png 's-w-sh') - -可以看到,代表着用户身份的ticket码也显现到了URL之中,借此漏洞,攻击者完全可以构建一个URL将小红的ticket码自动提交到攻击者自己的服务器,伪造小红身份登录网站 - -##### 6.2、防范方法 - -造成此漏洞的直接原因就是SSO-Server认证中心没有对 `redirect地址` 进行任何的限制,防范的方法也很简单,就是对`redirect参数`进行校验,如果其不在指定的URL列表中时,拒绝下放ticket - -我们将其配置为一个具体的URL:`allow-url=http://sa-sso-client1.com:9001/sso/login`,再次访问上述连接: - -![sso-feifa-rf](https://oss.dev33.cn/sa-token/doc/sso/sso-feifa-rf.png 's-w-sh') - -域名没有通过校验,拒绝授权! - -##### 6.3、配置安全性参考表 - -| 配置方式 | 举例 | 安全性 | 建议 | -| :-------- | :-------- | :-------- | :-------- | -| 配置为* | `*` | | **禁止在生产环境下使用** | -| 配置到域名 | `http://sa-sso-client1.com/*` | | 不建议在生产环境下使用 | -| 配置到详细地址| `http://sa-sso-client1.com:9001/sso/login` | | 可以在生产环境下使用 | - - -##### 6.4、疑问:为什么不直接回传Token,而是先回传ticket,再用ticket去查询对应的账号id? -Token作为长时间有效的会话凭证,在任何时候都不应该直接在暴露URL之中(虽然Token直接的暴露本身不会造成安全漏洞,但会为很多漏洞提供可乘之机) - -因此Sa-Token-SSO选择先回传ticket,再由ticket获取账号id,且ticket一次性用完即废,提高安全性 - - - -### 7、跨Redis的单点登录 -以上流程解决了跨域模式下的单点登录,但是后端仍然采用了共享Redis来同步会话,如果我们的架构设计中Client端与Server端无法共享Redis,又该怎么完成单点登录? - -这就要采用模式三了,且往下看:[Http请求获取会话](/sso/sso-type3) - - - - - - - - - - - diff --git a/sa-token-doc/doc/sso/sso-type3.md b/sa-token-doc/doc/sso/sso-type3.md deleted file mode 100644 index a97af5fd6..000000000 --- a/sa-token-doc/doc/sso/sso-type3.md +++ /dev/null @@ -1,211 +0,0 @@ -# SSO模式三 Http请求获取会话 - -如果既无法做到前端同域,也无法做到后端同Redis,那么可以使用模式三完成单点登录 - -> 阅读本篇之前请务必先熟读SSO模式二!因为模式三仅仅属于模式二的一个特殊场景,熟读模式二有助于您快速理解本章内容 - - -### 0、问题分析 -我们先来分析一下,当后端不使用共享Redis时,会对架构产生哪些影响 - -1. Client端 无法直连 Redis 校验 ticket,取出账号id -2. Client端 无法与 Server端 共用一套会话,需要自行维护子会话 -3. 由于不是一套会话,所以无法“一次注销,全端下线”,需要额外编写代码完成单点注销 - -所以模式三的主要目标:也就是在 模式二的基础上 解决上述 三个难题 - -> 模式三的Demo示例地址:
-> SSO-Server端: `/sa-token-demo/sa-token-demo-sso3-server/` [源码链接](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-sso3-server)
-> SSO-Client端: `/sa-token-demo/sa-token-demo-sso3-client/` [源码链接](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-sso3-client)
-> 如遇难点可参考示例 - - -### 1、SSO-Server认证中心开放ticket校验接口 -既然Client端无法直连Redis校验ticket,那就在Server端开放ticket校验接口,然后Client端通过http请求获取数据 - -##### 1.1、添加依赖 -首先在Server端和Client端均添加以下依赖(如果不需要单点注销功能则Server端可不引入) -``` xml - - - com.ejlchina - okhttps - 3.1.1 - -``` -> OkHttps是一个轻量级http请求工具,详情参考:[OkHttps](https://gitee.com/ejlchina-zhxu/okhttps) - -##### 1.2、认证中心开放接口 -在SSO-Server端的`application.yml`中,新增以下配置: -``` yml -sa-token: - sso: - # 使用Http请求校验ticket - is-http: true -``` -此配置项的作用是开放ticket校验接口,让Client端通过http请求获取会话 - -##### 1.3、Client端新增配置 -在SSO-Client端的`SsoClientController`中,新增以下配置 -``` java -// 配置SSO相关参数 -@Autowired -private void configSso(SaTokenConfig cfg) { - cfg.sso - // 配置Http请求处理器 - .setSendHttp(url -> { - return OkHttps.sync(url).get().getBody().toString(); - }) - ; -} -``` - -``` yml -sa-token: - sso: - # 使用Http请求校验ticket - is-http: true - # SSO-Server端 ticket校验地址 - check-ticket-url: http://sa-sso-server.com:9000/sso/checkTicket -``` - -##### 1.5 启动项目测试 -启动SSO-Server、SSO-Client,访问测试:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/) -> 注:如果已测试运行模式二,可先将Redis中的数据清空,以防旧数据对测试造成干扰 - - -### 2、获取Userinfo -除了账号id,我们可能还需要将用户的昵称、头像等信息从 Server端 带到 Client端,即:用户资料的同步。要解决这个需求,我们只需: - -##### 2.1、在Server端自定义接口,查询用户资料 -``` java -// 自定义接口:获取userinfo -@RequestMapping("/sso/userinfo") -public Object userinfo(String loginId, String secretkey) { - System.out.println("---------------- 获取userinfo --------"); - - // 校验调用秘钥 - SaSsoUtil.checkSecretkey(secretkey); - - // 自定义返回结果(模拟) - return SaResult.ok() - .set("id", loginId) - .set("name", "linxiaoyu") - .set("sex", "女") - .set("age", 18); -} -``` - -##### 2.2、在Client端调用此接口查询userinfo -首先在yml中配置接口地址 -``` yml -sa-token: - sso: - # SSO-Server端 查询userinfo地址 - userinfo-url: http://sa-sso-server.com:9000/sso/userinfo -``` - -然后在`SsoClientController`中新增接口 -``` java -// 查询我的账号信息 -@RequestMapping("/sso/myinfo") -public Object myinfo() { - Object userinfo = SaSsoUtil.getUserinfo(StpUtil.getLoginId()); - System.out.println("--------info:" + userinfo); - return userinfo; -} -``` - -访问测试:[http://sa-sso-client2.com:9001/sso/myinfo](http://sa-sso-client2.com:9001/sso/myinfo) - - - -### 3、无刷单点注销 - -有了单点登录就必然要有单点注销,网上给出的大多数解决方案是将注销请求重定向至SSO-Server中心,逐个通知Client端下线 - -在某些场景下,页面的跳转可能造成不太好的用户体验,Sa-Token-SSO 允许你以 `REST API` 的形式构建接口,做到页面无刷新单点注销 - -1. Client端校验ticket的时候将注销回调地址发送到Server端 -2. Server端将注销回调地址存储到Set集合 -3. Client端向Server端发送单点注销请求 -4. Server端遍历Set集合,逐个通知Client端下线 -5. Server端注销下线 -6. 单点注销完成 - -##### 2.1、SSO-Server认证中心增加配置 -在 `SsoServerController` 中新增配置 -``` java -// 配置SSO相关参数 -@Autowired -private void configSso(SaTokenConfig cfg) { - cfg.sso - // ... (其它配置保持不变) - // 配置Http请求处理器 - .setSendHttp(url -> { - // 此处为了提高响应速度这里可将sync换为async - return OkHttps.sync(url).get(); - }) - ; -} -``` - -并在 `application.yml` 下新增配置: -``` yml -sa-token: - sso: - # 打开单点注销功能 - is-slo: true - # API调用秘钥(用于SSO模式三的单点注销功能) - secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor -``` - -##### 2.2、SSO-Client端新增配置 - -在 `application.yml` 增加配置:`API调用秘钥` 和 `单点注销接口URL` -``` yml -sa-token: - sso: - # 打开单点注销功能 - is-slo: true - # 单点注销地址 - slo-url: http://sa-sso-server.com:9000/sso/logout - # 接口调用秘钥 - secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor -``` - -##### 2.3 启动测试 -启动SSO-Server、SSO-Client,访问测试:[http://sa-sso-client1.com:9001/](http://sa-sso-client1.com:9001/), -我们主要的测试点在于 `单点注销`,正常登陆即可 - -![sso-type3-client-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-client-index.png 's-w-sh') - -点击 **`[注销]`** 按钮,即可单点注销成功 - - - -![sso-type3-slo-index.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-slo-index.png 's-w-sh') - -PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用Ajax调用接口即可做到无刷单点登录退出 - -例如我们使用 [APIPost接口测试工具](https://www.apipost.cn/) 可以做到同样的效果: - -![sso-slo-apipost.png](https://oss.dev33.cn/sa-token/doc/sso/sso-slo-apipost.png 's-w-sh') - -测试完毕! - - - - -### 4、后记 -当我们熟读三种模式的单点登录之后,其实不难发现:所谓单点登录,其本质就是多个系统之间的会话共享 - -当我们理解这一点之后,三种模式的工作原理也浮出水面: - -- 模式一:采用共享Cookie来做到前端Token的共享,从而达到后端的Session会话共享 -- 模式二:采用URL重定向,以ticket码为授权中介,做到多个系统间的会话传播 -- 模式三:采用Http请求主动查询会话,做到Client端与Server端的会话同步 - - - - diff --git a/sa-token-doc/doc/start/download.md b/sa-token-doc/doc/start/download.md deleted file mode 100644 index 14de70956..000000000 --- a/sa-token-doc/doc/start/download.md +++ /dev/null @@ -1,129 +0,0 @@ -# 集成 Sa-Token - ------- - -## Maven依赖 -在项目中直接通过 `pom.xml` 引入 Sa-Token 的依赖即可(四选一): - - - - -如果你使用的框架基于 ServletAPI 构建( SpringMVC、SpringBoot、Zuul等 ),请引入此包 -``` xml - - - cn.dev33 - sa-token-spring-boot-starter - ${sa.top.version} - -``` - - -注:如果你使用的框架基于 Reactor 模型构建(Netty、WebFlux、ShenYu、SC Gateway等),请引入此包 -``` xml - - - cn.dev33 - sa-token-reactor-spring-boot-starter - ${sa.top.version} - -``` - - -注:如果你的项目没有使用Spring,但是Web框架是基于 ServletAPI 规范的,可以引入此包 -``` xml - - - cn.dev33 - sa-token-servlet - ${sa.top.version} - -``` - - -注:如果你的项目既没有使用 SpringMVC、WebFlux,也不是基于 ServletAPI 规范,那么可以引入core核心包 -``` xml - - - cn.dev33 - sa-token-core - ${sa.top.version} - -``` - - - -## Gradle依赖 - - -``` xml -implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' -``` - -``` xml -implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' -``` - -``` xml -implementation 'cn.dev33:sa-token-servlet:${sa.top.version}' -``` - -``` xml -implementation 'cn.dev33:sa-token-core:${sa.top.version}' -``` - - -注:JDK版本:`v1.8+`,SpringBoot:`建议2.0以上` - - -## 获取源码 -如果你想深入了解Sa-Token,你可以通过`Gitee`或者`GitHub`来获取源码 (**学习测试请拉取master分支**,dev为正在开发的分支,有很多特性并不稳定) -- **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) -- 开源不易,求鼓励,给个`star`吧 -- 源码目录介绍: - -``` js -── sa-token - ├── sa-token-core // [核心] Sa-Token 核心模块 - ├── sa-token-starter // [整合] Sa-Token 与其它框架整合 - ├── sa-token-servlet // [整合] Sa-Token 整合 Servlet容器实现类包 - ├── sa-token-spring-boot-starter // [整合] Sa-Token 整合 SpringBoot 快速集成 - ├── sa-token-reactor-spring-boot-starter // [整合] Sa-Token 整合 Reactor响应式编程 快速集成 - ├── sa-token-solon-plugin // [整合] Sa-Token 整合 Solon 快速集成 - ├── sa-token-plugin // [插件] Sa-Token 插件合集 - ├── sa-token-dao-redis // [插件] Sa-Token 整合 Redis (使用jdk默认序列化方式) - ├── sa-token-dao-redis-jackson // [插件] Sa-Token 整合 Redis (使用jackson序列化方式) - ├── sa-token-spring-aop // [插件] Sa-Token 整合 SpringAOP 注解鉴权 - ├── sa-token-temp-jwt // [插件] Sa-Token 整合 jwt 临时令牌鉴权 - ├── sa-token-quick-login // [插件] Sa-Token 快速注入登录页插件 - ├── sa-token-alone-redis // [插件] Sa-Token 独立Redis插件,实现[权限缓存与业务缓存分离] - ├── sa-token-oauth2 // [插件] Sa-Token 实现 OAuth2.0 模块 - ├── sa-token-demo // [示例] Sa-Token 示例合集 - ├── sa-token-demo-springboot // [示例] Sa-Token 整合 SpringBoot - ├── sa-token-demo-webflux // [示例] Sa-Token 整合 WebFlux - ├── sa-token-demo-jwt // [示例] Sa-Token 集成 jwt - ├── sa-token-demo-solon // [示例] Sa-Token 集成 Solon - ├── sa-token-demo-quick-login // [示例] Sa-Token 集成 quick-login 模块 - ├── sa-token-demo-alone-redis // [示例] Sa-Token 集成 alone-redis 模块 - ├── sa-token-demo-sso1 // [示例] Sa-Token 集成 SSO单点登录-模式一 - ├── sa-token-demo-sso2-server // [示例] Sa-Token 集成 SSO单点登录-模式二 认证中心 - ├── sa-token-demo-sso2-client // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端 - ├── sa-token-demo-sso3-server // [示例] Sa-Token 集成 SSO单点登录-模式三 认证中心 - ├── sa-token-demo-sso3-client // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 - ├── sa-token-demo-oauth2-server // [示例] Sa-Token 集成 OAuth2.0 (服务端) - ├── sa-token-demo-oauth2-client // [示例] Sa-Token 集成 OAuth2.0 (客户端) - ├── sa-token-doc // [文档] Sa-Token 开发文档 - ├──pom.xml // [依赖] 顶级pom文件 -``` - - - - -## jar包下载 -[点击下载:sa-token-1.6.0.jar](https://oss.dev33.cn/sa-token/sa-token-1.6.0.jar) - -(注意:当前仅提供`v1.6.0`版本jar包下载,更多版本请前往maven中央仓库获取,[直达链接](https://search.maven.org/search?q=sa-token)) - - - diff --git a/sa-token-doc/doc/start/example.md b/sa-token-doc/doc/start/example.md deleted file mode 100644 index 602d282f6..000000000 --- a/sa-token-doc/doc/start/example.md +++ /dev/null @@ -1,115 +0,0 @@ -# SpringBoot 集成 Sa-Token 示例 - -本篇将带你从零开始集成Sa-Token,从而让你快速熟悉Sa-Token的使用姿势
-整合示例在官方仓库的`/sa-token-demo/sa-token-demo-springboot`文件夹下,如遇到难点可结合源码进行测试学习 - ---- - -### 1、创建项目 -在IDE中新建一个SpringBoot项目,例如:`sa-token-demo-springboot`(不会的同学请自行百度或者参考github示例) - - -### 2、添加依赖 -在 `pom.xml` 中添加依赖: - -``` xml - - - cn.dev33 - sa-token-spring-boot-starter - ${sa.top.version} - -``` - - -### 3、设置配置文件 -你可以**零配置启动项目** ,但同时你也可以在`application.yml`中增加如下配置,定制性使用框架: - -``` java -server: - # 端口 - port: 8081 - -# Sa-Token配置 -sa-token: - # token名称 (同时也是cookie名称) - token-name: satoken - # token有效期,单位s 默认30天, -1代表永不过期 - timeout: 2592000 - # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 - activity-timeout: -1 - # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) - is-concurrent: true - # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) - is-share: false - # token风格 - token-style: uuid - # 是否输出操作日志 - is-log: false -``` - -如果你习惯于 `application.properties` 类型的配置文件,那也很好办:
-百度: [springboot properties与yml 配置文件的区别](https://www.baidu.com/s?ie=UTF-8&wd=springboot%20properties%E4%B8%8Eyml%20%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E7%9A%84%E5%8C%BA%E5%88%AB) - - -### 4、创建启动类 -在项目中新建包 `com.pj` ,在此包内新建主类 `SaTokenDemoApplication.java`,输入以下代码: - -``` java -@SpringBootApplication -public class SaTokenDemoApplication { - public static void main(String[] args) throws JsonProcessingException { - SpringApplication.run(SaTokenDemoApplication.class, args); - System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig()); - } -} -``` - -### 5、创建测试Controller -``` java -@RestController -@RequestMapping("/user/") -public class UserController { - - // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 - @RequestMapping("doLogin") - public String doLogin(String username, String password) { - // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 - if("zhang".equals(username) && "123456".equals(password)) { - StpUtil.login(10001); - return "登录成功"; - } - return "登录失败"; - } - - // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin - @RequestMapping("isLogin") - public String isLogin(String username, String password) { - return "当前会话是否登录:" + StpUtil.isLogin(); - } - -} -``` - -### 6、运行 -启动代码,从浏览器依次访问上述测试接口: - -![运行结果](https://oss.dev33.cn/sa-token/doc/test-do-login.png) - -![运行结果](https://oss.dev33.cn/sa-token/doc/test-is-login.png) - - - - -### 详细了解 -通过这个示例,你已经对Sa-Token有了初步的了解,那么现在开始详细了解一下它都有哪些 [能力](/use/login-auth) 吧 - - - - - - - diff --git a/sa-token-doc/doc/start/webflux-example.md b/sa-token-doc/doc/start/webflux-example.md deleted file mode 100644 index fd6a8beec..000000000 --- a/sa-token-doc/doc/start/webflux-example.md +++ /dev/null @@ -1,116 +0,0 @@ -# Spring WebFlux 集成 Sa-Token 示例 - -**Reactor** 是一种非阻塞的响应式模型,本篇将 **WebFlux** 以为例,展示 Sa-Token 与 Reactor 响应式模型架相整合的示例, -**你可以用同样方式去对接其它Reactor模型框架(Netty、ShenYu、SpringCloud Gateway等)** - -整合示例在官方仓库的`/sa-token-demo/sa-token-demo-webflux`文件夹下,如遇到难点可结合源码进行测试学习 - -!> WebFlux常用于微服务网关架构中,如果您的应用基于单体架构且非 Reactor 模型,可以先跳过本章 - ---- - -### 1、创建项目 -在IDE中新建一个SpringBoot项目,例如:`sa-token-demo-webflux`(不会的同学请自行百度或者参考github示例) - - -### 2、添加依赖 -在 `pom.xml` 中添加依赖: - -``` xml - - - cn.dev33 - sa-token-reactor-spring-boot-starter - ${sa.top.version} - -``` - - -### 3、创建启动类 -在项目中新建包 `com.pj` ,在此包内新建主类 `SaTokenDemoApplication.java`,输入以下代码: - -``` java -@SpringBootApplication -public class SaTokenDemoApplication { - public static void main(String[] args) throws JsonProcessingException { - SpringApplication.run(SaTokenDemoApplication.class, args); - System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig()); - } -} -``` - -### 4、创建全局过滤器 -新建`SaTokenConfigure.java`,注册Sa-Token的全局过滤器 -``` java -/** - * [Sa-Token 权限认证] 全局配置类 - */ -@Configuration -public class SaTokenConfigure { - /** - * 注册 [Sa-Token全局过滤器] - */ - @Bean - public SaReactorFilter getSaReactorFilter() { - return new SaReactorFilter() - // 指定 [拦截路由] - .addInclude("/**") - // 指定 [放行路由] - .addExclude("/favicon.ico") - // 指定[认证函数]: 每次请求执行 - .setAuth(r -> { - System.out.println("---------- sa全局认证"); - // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); - }) - // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 - .setError(e -> { - System.out.println("---------- sa全局异常 "); - return AjaxJson.getError(e.getMessage()); - }) - ; - } -} -``` -?> 你只需要按照此格式复制代码即可,有关过滤器的详细用法,会在之后的章节详细介绍 - - -### 5、创建测试Controller -``` java -@RestController -@RequestMapping("/user/") -public class UserController { - - // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 - @RequestMapping("doLogin") - public String doLogin(String username, String password) { - // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 - if("zhang".equals(username) && "123456".equals(password)) { - StpUtil.login(10001); - return "登录成功"; - } - return "登录失败"; - } - - // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin - @RequestMapping("isLogin") - public String isLogin(String username, String password) { - return "当前会话是否登录:" + StpUtil.isLogin(); - } - -} -``` - -### 6、运行 -启动代码,从浏览器依次访问上述测试接口: - -![运行结果](https://oss.dev33.cn/sa-token/doc/test-do-login.png) - -![运行结果](https://oss.dev33.cn/sa-token/doc/test-is-login.png) - - -**注意事项** - -更多使用示例请参考官方仓库demo - - - diff --git a/sa-token-doc/doc/up/global-filter.md b/sa-token-doc/doc/up/global-filter.md deleted file mode 100644 index b5087a5e8..000000000 --- a/sa-token-doc/doc/up/global-filter.md +++ /dev/null @@ -1,106 +0,0 @@ -# 全局过滤器 ---- - -### 组件简述 - -之前的章节中,我们学习了“根据拦截器实现路由拦截鉴权”,其实在大多数web框架中,使用过滤器可以实现同样的功能,本章我们就利用Sa-Token全局过滤器来实现路由拦截器鉴权。 - -首先我们先梳理清楚一个问题,既然拦截器已经可以实现路由鉴权,为什么还要用过滤器再实现一遍呢?简而言之: -1. 相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描 -2. 过滤器可以拦截静态资源,方便我们做一些权限控制 -3. 部分Web框架根本就没有提供拦截器功能,但几乎所有的Web框架都会提供过滤器机制 - -但是过滤器也有一些缺点,比如: -1. 由于太过底层,导致无法率先拿到`HandlerMethod`对象,无法据此添加一些额外功能 -2. 由于拦截的太全面了,导致我们需要对很多特殊路由(如`/favicon.ico`)做一些额外处理 -3. 在Spring中,过滤器中抛出的异常无法进入全局`@ExceptionHandler`,我们必须额外编写代码进行异常处理 - -Sa-Token同时提供过滤器和拦截器机制,不是为了让谁替代谁,而是为了让大家根据自己的实际业务合理选择,拥有更多的发挥空间。 - - -### 在 SpringBoot 中注册过滤器 -同拦截器一样,为了避免不必要的性能浪费,Sa-Token全局过滤器默认处于关闭状态,若要使用过滤器组件,首先你需要注册它到项目中: -``` java -/** - * [Sa-Token 权限认证] 配置类 - * @author kong - */ -@Configuration -public class SaTokenConfigure { - - /** - * 注册 [Sa-Token全局过滤器] - */ - @Bean - public SaServletFilter getSaServletFilter() { - return new SaServletFilter() - - // 指定 拦截路由 与 放行路由 - .addInclude("/**").addExclude("/favicon.ico") - - // 认证函数: 每次请求执行 - .setAuth(r -> { - System.out.println("---------- 进入Sa-Token全局认证 -----------"); - - // 登录验证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 - SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin()); - - // 更多拦截处理方式,请参考“路由拦截式鉴权”章节 - }) - - // 异常处理函数:每次认证函数发生异常时执行此函数 - .setError(e -> { - System.out.println("---------- 进入Sa-Token异常处理 -----------"); - return AjaxJson.getError(e.getMessage()); - }) - - // 前置函数:在每次认证函数之前执行 - .setBeforeAuth(r -> { - // ---------- 设置一些安全响应头 ---------- - SaHolder.getResponse() - // 服务器名称 - .setServer("sa-server") - // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 - .setHeader("X-Frame-Options", "SAMEORIGIN") - // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 - .setHeader("X-XSS-Protection", "1; mode=block") - // 禁用浏览器内容嗅探 - .setHeader("X-Content-Type-Options", "nosniff") - ; - }) - ; - } - -} -``` - -##### 注意事项: -- 在`[认证函数]`里,你可以写和拦截器里一致的代码,进行路由匹配鉴权,参考:[路由拦截鉴权](/use/route-check) -- 由于过滤器中抛出的异常不进入全局异常处理,所以你必须提供`[异常处理函数]`来处理`[认证函数]`里抛出的异常 -- 在`[异常处理函数]`里的返回值,将作为字符串输出到前端,如果需要定制化返回数据,请注意其中的格式转换 - - -### 在 WebFlux 中注册过滤器 -`Spring WebFlux`中不提供拦截器机制,因此若你的项目需要路由鉴权功能,过滤器是你唯一的选择,在`Spring WebFlux`注册过滤器的流程与上述流程几乎完全一致, -除了您需要将过滤器名称由`SaServletFilter`更换为`SaReactorFilter`以外,其它所有步骤均可参考以上示例 -``` java -/** - * [Sa-Token 权限认证] 配置类 - * @author kong - */ -@Configuration -public class SaTokenConfigure { - - /** - * 注册 [Sa-Token全局过滤器] - */ - @Bean - public SaReactorFilter getSaReactorFilter() { - return new SaReactorFilter() - // 其它代码... - ; - } - -} -``` - \ No newline at end of file diff --git a/sa-token-doc/doc/up/global-listener.md b/sa-token-doc/doc/up/global-listener.md deleted file mode 100644 index ff5a83f52..000000000 --- a/sa-token-doc/doc/up/global-listener.md +++ /dev/null @@ -1,72 +0,0 @@ -# 全局侦听器 - -接口`SaTokenListener`是Sa-Token的全局侦听器,通过实现此接口,你可以在用户登陆、退出、被踢下线等关键性操作时进行一些AOP操作 - -框架对此侦听器的默认实现是log日志输出,你可以通过配置`sa-token.is-log=true`开启 - -下面我们演示一下如何自定义侦听器的实现: - ---- - - -### 自定义侦听器实现 - -新建`MySaTokenListener.java`,继承`SaTokenListener`接口,并添加上注解`@Component`,保证此类被`SpringBoot`扫描到 -``` java -/** - * 自定义侦听器的实现 - */ -@Component -public class MySaTokenListener implements SaTokenListener { - - /** 每次登录时触发 */ - @Override - public void doLogin(String loginType, Object loginId, SaLoginModel loginModel) { - // ... - } - - /** 每次注销时触发 */ - @Override - public void doLogout(String loginType, Object loginId, String tokenValue) { - // ... - } - - /** 每次被踢下线时触发 */ - @Override - public void doLogoutByLoginId(String loginType, Object loginId, String tokenValue, String device) { - // ... - } - - /** 每次被顶下线时触发 */ - @Override - public void doReplaced(String loginType, Object loginId, String tokenValue, String device) { - // ... - } - - /** 每次被封禁时触发 */ - @Override - public void doDisable(String loginType, Object loginId, long disableTime) { - // ... - } - - /** 每次被解封时触发 */ - @Override - public void doUntieDisable(String loginType, Object loginId) { - // ... - } - - /** 每次创建Session时触发 */ - @Override - public void doCreateSession(String id) { - // ... - } - - /** 每次注销Session时触发 */ - @Override - public void doLogoutSession(String id) { - // ... - } - -} -``` - diff --git a/sa-token-doc/doc/up/integ-redis.md b/sa-token-doc/doc/up/integ-redis.md deleted file mode 100644 index f2cac8c59..000000000 --- a/sa-token-doc/doc/up/integ-redis.md +++ /dev/null @@ -1,90 +0,0 @@ -# Sa-Token 集成 Redis ---- - -Sa-token默认将数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如: - -1. 重启后数据会丢失 -2. 无法在分布式环境中共享数据 - -为此,Sa-Token提供了扩展接口,你可以轻松将会话数据存储在 `Redis`、`Memcached`等专业的缓存中间件中, -做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性 - -以下是官方提供的Redis集成包: - ---- - -### 1. Sa-Token 整合 Redis (使用jdk默认序列化方式) -``` xml - - - cn.dev33 - sa-token-dao-redis - ${sa.top.version} - -``` -优点:兼容性好,缺点:Session序列化后基本不可读,对开发者来讲等同于乱码 - - -### 2. Sa-Token 整合 Redis(使用jackson序列化方式) -``` xml - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - -``` -优点:Session序列化后可读性强,可灵活手动修改,缺点:兼容性稍差 - - -### 集成Redis请注意: - - -**1. 无论使用哪种序列化方式,你都必须为项目提供一个Redis实例化方案,例如:** -``` xml - - - org.apache.commons - commons-pool2 - -``` - -**2. 引入了依赖,我还需要为Redis配置连接信息吗?**
-需要!只有项目初始化了正确的Redis实例,`Sa-Token`才可以使用Redis进行数据持久化,参考以下`yml配置`: -``` java -# 端口 -spring: - # redis配置 - redis: - # Redis数据库索引(默认为0) - database: 1 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - # password: - # 连接超时时间(毫秒) - timeout: 1000ms - lettuce: - pool: - # 连接池最大连接数 - max-active: 200 - # 连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - # 连接池中的最大空闲连接 - max-idle: 10 - # 连接池中的最小空闲连接 - min-idle: 0 -``` - - -**3. 集成Redis后,是我额外手动保存数据,还是框架自动保存?**
-框架自动保存。集成`Redis`只需要引入对应的`pom依赖`即可,框架所有上层API保持不变 - - -

-更多框架的集成方案正在更新中... (欢迎大家提交pr) - - - diff --git a/sa-token-doc/doc/up/many-account.md b/sa-token-doc/doc/up/many-account.md deleted file mode 100644 index e9b72be6d..000000000 --- a/sa-token-doc/doc/up/many-account.md +++ /dev/null @@ -1,189 +0,0 @@ -# 多账号验证 ---- - -### 0、需求场景 -有的时候,我们会在一个项目中设计两套账号体系,比如一个电商系统的 `user表` 和 `admin表`, -在这种场景下,如果两套账号我们都使用 `StpUtil` 类的API进行登录鉴权,那么势必会发生逻辑冲突 - -在Sa-Token中,这个问题的模型叫做:多账号体系验证 - -要解决这个问题,我们必须有一个合理的机制将这两套账号的授权给区分开,让它们互不干扰才行 - - -### 1、演进思路 -假如说我们的 user表 和 admin表 都有一个 id=10001 的账号,它们对应的登录代码:`StpUtil.login(10001)` 是一样的, -那么问题来了:在`StpUtil.getLoginId()`获取到的账号id如何区分它是User用户,还是Admin用户? - -你可能会想到为他们加一个固定前缀,比如`StpUtil.login("User_" + 10001)`、`StpUtil.login("Admin_" + 10001)`,这样确实是可以解决问题的, -但是同样的:你需要在`StpUtil.getLoginId()`时再裁剪掉相应的前缀才能获取真正的账号id,这样一增一减就让我们的代码变得无比啰嗦 - -那么,有没有从框架层面支持的,更优雅的解决方案呢? - - -### 2、解决方案 - -前面几篇介绍的api调用,都是经过 StpUtil 类的各种静态方法进行授权验证, -而如果我们深入它的源码,[点此阅览](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java)
-就会发现,此类并没有任何代码逻辑,唯一做的事就是对成员变量`stpLogic`的各个API包装一下进行转发 - -这样做有两个优点: -- StpLogic 类的所有函数都可以被重写,按需扩展 -- 在构造方法时随意传入一个不同的 `loginType`,就可以再造一套账号登录体系 - - -### 3、操作示例 - -比如说,对于原生`StpUtil`类,我们只做`admin账号`权限验证,而对于`user账号`,我们则: -1. 新建一个新的权限验证类,比如: `StpUserUtil.java` -2. 将`StpUtil.java`类的全部代码复制粘贴到 `StpUserUtil.java`里 -3. 更改一下其 `LoginType`, 比如: - -``` java -public class StpUserUtil { - - /** - * 账号体系标识 - */ - public static final String TYPE = "user"; // 将 LoginType 从`login`改为`user` - - // 其它代码 ... - -} -``` -4. 接下来就可以像调用`StpUtil.java`一样调用 `StpUserUtil.java`了,这两套账号认证的逻辑是完全隔离的 - -> 成品样例参考:[码云 StpUserUtil.java](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at/StpUserUtil.java) - - -### 4、在多账号模式下使用注解鉴权 -框架默认的注解鉴权 如`@SaCheckLogin` 只针对原生`StpUtil`进行鉴权 - -例如,我们在一个方法上加上`@SaCheckLogin`注解,这个注解只会放行通过`StpUtil.login(id)`进行登录的会话, -而对于通过`StpUserUtil.login(id)`进行登录的都会话,则始终不会通过校验 - -那么如何告诉`@SaCheckLogin`要鉴别的是哪套账号的登录会话呢?很简单,你只需要指定一下注解的type属性即可: - -``` java -// 通过type属性指定此注解校验的是我们自定义的`StpUserUtil`,而不是原生`StpUtil` -@SaCheckLogin(type = StpUserUtil.TYPE) -@RequestMapping("info") -public String info() { - return "查询用户信息"; -} -``` - -注:`@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")`同理,亦可根据type属性指定其校验的账号体系,此属性默认为`""`,代表使用原生`StpUtil`账号体系 - - -### 5、使用注解合并简化代码 -交流群里有同学反应,虽然可以根据 `@SaCheckLogin(type = "user")` 指定账号类型,但几十上百个注解都加上这个的话,还是有些繁琐,代码也不够优雅,有么有更改的解决方案? - -我们期待一种`[注解继承/合并]`的能力,即:自定义一个注解,标注上`@SaCheckLogin(type = "user")`,然后在方法上标注这个自定义注解,效果等同于标注`@SaCheckLogin(type = "user")` - -很遗憾,JDK默认的注解处理器并没有提供这种`[注解继承/合并]`的能力,不过好在我们可以利用Spring的注解处理器,达到同样的目的 - -1. 重写Sa-Token默认的注解处理器 - -``` java -/** - * 继承Sa-Token行为Bean默认实现, 重写部分逻辑 - */ -@Component -public class MySaTokenAction extends SaTokenActionDefaultImpl { - - /** - * 重写Sa-Token的注解处理器,加强注解合并功能 - */ - @Override - protected void validateAnnotation(AnnotatedElement target) { - - // 校验 @SaCheckLogin 注解 - if(AnnotatedElementUtils.isAnnotated(target, SaCheckLogin.class)) { - SaCheckLogin at = AnnotatedElementUtils.getMergedAnnotation(target, SaCheckLogin.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckRole 注解 - if(AnnotatedElementUtils.isAnnotated(target, SaCheckRole.class)) { - SaCheckRole at = AnnotatedElementUtils.getMergedAnnotation(target, SaCheckRole.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckPermission 注解 - if(AnnotatedElementUtils.isAnnotated(target, SaCheckPermission.class)) { - SaCheckPermission at = AnnotatedElementUtils.getMergedAnnotation(target, SaCheckPermission.class); - SaManager.getStpLogic(at.type()).checkByAnnotation(at); - } - - // 校验 @SaCheckSafe 注解 - if(AnnotatedElementUtils.isAnnotated(target, SaCheckSafe.class)) { - SaCheckSafe at = AnnotatedElementUtils.getMergedAnnotation(target, SaCheckSafe.class); - SaManager.getStpLogic(null).checkByAnnotation(at); - } - } - -} -``` - -2. 自定义一个注解 - -``` java -/** - * 登录认证(User版):只有登录之后才能进入该方法 - *

可标注在函数、类上(效果等同于标注在此类的所有方法上) - */ -@SaCheckLogin(type = "user") -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.TYPE}) -public @interface SaUserCheckLogin { - -} -``` - -3. 接下来就可以使用我们的自定义注解了 - -``` java -// 使用 @SaUserCheckLogin 的效果等同于使用:@SaCheckLogin(type = "user") -@SaUserCheckLogin -@RequestMapping("info") -public String info() { - return "查询用户信息"; -} -``` - -注:其它注解 `@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")`同理,完整示例参考:[码云:自定义注解](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/at) - - - - -### 6、同端多登陆 -假设我们不仅需要在后台同时集成两套账号,我们还需要在一个客户端同时登陆两套账号(业务场景举例:一个APP中可以同时登陆商家账号和用户账号) - -如果我们不做任何特殊处理的话,在客户端会发生`token覆盖`,新登录的token会覆盖掉旧登录的token从而导致旧登录失效 - -那么如何解决这个问题?
-很简单,我们只要更改一下 `StpUserUtil` 的 `TokenName` 即可,参考示例如下: - -``` java -public class StpUserUtil { - - // 使用匿名子类 重写`stpLogic对象`的一些方法 - public static StpLogic stpLogic = new StpLogic("user") { - // 重写 StpLogic 类下的 `splicingKeyTokenName` 函数,返回一个与 `StpUtil` 不同的token名称, 防止冲突 - @Override - public String splicingKeyTokenName() { - return super.splicingKeyTokenName() + "-user"; - } - // 同理你可以按需重写一些其它方法 ... - }; - - // ... - -} -``` - -再次调用 `StpUserUtil.login(10001)` 进行登录授权时,token的名称将不再是 `satoken`,而是我们重写后的 `satoken-user` - - - -> 不同体系账号在登录时设置不同的token有效期等信息,详见[登录时指定token有效期](/up/remember-me?id=登录时指定token有效期) \ No newline at end of file diff --git a/sa-token-doc/doc/up/mock-person.md b/sa-token-doc/doc/up/mock-person.md deleted file mode 100644 index b6432a868..000000000 --- a/sa-token-doc/doc/up/mock-person.md +++ /dev/null @@ -1,59 +0,0 @@ -# 模拟他人 ---- - - -以上介绍的api都是操作当前账号,对当前账号进行各种鉴权操作,你可能会问,我能不能对别的账号进行一些操作?
-比如:查看账号10001有无某个权限码、获取 账号id=10002 的 `User-Session`,等等... - -Sa-Token在api设计时充分考虑了这一点,暴露出多个api进行此类操作 - - -## 有关操作其它账号的api - -``` java -// 获取指定账号10001的`tokenValue`值 -StpUtil.getTokenValueByLoginId(10001); - -// 将账号10001的会话注销登录(踢人下线) -StpUtil.logoutByLoginId(10001); - -// 获取账号10001的Session对象, 如果session尚未创建, 则新建并返回 -StpUtil.getSessionByLoginId(10001); - -// 获取账号10001的Session对象, 如果session尚未创建, 则返回null -StpUtil.getSessionByLoginId(10001, false); - -// 获取账号10001是否含有指定角色标识 -StpUtil.hasRole(10001, "super-admin"); - -// 获取账号10001是否含有指定权限码 -StpUtil.hasPermission(10001, "user:add"); -``` - - - -## 临时身份切换 - -有时候,我们需要直接将当前会话的身份切换为其它账号,比如: -``` java -// 将当前会话[身份临时切换]为其它账号 -StpUtil.switchTo(10044); - -// 此时再调用此方法会返回 10044 (我们临时切换到的账号id) -StpUtil.getLoginId(); - -// 结束 [身份临时切换] -StpUtil.endSwitch(); -``` - -你还可以: 直接在一个代码段里方法内,临时切换身份为指定loginId(此方式无需手动调用`StpUtil.endSwitch()`关闭身份切换) -``` java -System.out.println("------- [身份临时切换]调用开始..."); -StpUtil.switchTo(10044, () -> { - System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); - System.out.println("获取当前登录账号id: " + StpUtil.getLoginId()); -}); -System.out.println("------- [身份临时切换]调用结束..."); -``` - - diff --git a/sa-token-doc/doc/up/mutex-login.md b/sa-token-doc/doc/up/mutex-login.md deleted file mode 100644 index 308f0c576..000000000 --- a/sa-token-doc/doc/up/mutex-login.md +++ /dev/null @@ -1,44 +0,0 @@ -# 同端互斥登录 - -如果你经常使用腾讯QQ,就会发现它的登录有如下特点:它可以手机电脑同时在线,但是不能在两个手机上同时登录一个账号
-同端互斥登录,指的就是像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线 - ---- - -## 具体API - -在 Sa-Token 中如何做到同端互斥登录?
-首先在配置文件中,将 `isConcurrent` 配置为false,然后调用登录等相关接口时声明设备标识即可: - - -#### 指定设备标识登录 -``` java -// 指定`账号id`和`设备标识`进行登录 -StpUtil.login(10001, "PC"); -``` -调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-4` - - -#### 指定设备标识强制注销 -``` java -// 指定`账号id`和`设备标识`进行强制注销 (踢人下线) -StpUtil.logoutByLoginId(10001, "PC"); -``` -如果第二个参数填写null或不填,代表将这个账号id所有在线端踢下线,被踢出者再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-5` - - -#### 查询当前登录的设备标识 -``` java -// 返回当前token的登录设备 -StpUtil.getLoginDevice(); -``` - - -#### Id 反查 Token -``` java -// 获取指定loginId指定设备端的tokenValue -StpUtil.getTokenValueByLoginId(10001, "APP"); -``` - - -> 不同设备账号在登录时设置不同的token有效期等信息, 详见[登录时指定token有效期](/up/remember-me?id=登录时指定token有效期) \ No newline at end of file diff --git a/sa-token-doc/doc/up/not-cookie.md b/sa-token-doc/doc/up/not-cookie.md deleted file mode 100644 index 4aad88685..000000000 --- a/sa-token-doc/doc/up/not-cookie.md +++ /dev/null @@ -1,89 +0,0 @@ -# 前后台分离(无Cookie模式) ---- - -### 何为无Cookie模式? - -无Cookie:特指不支持Cookie功能的终端,通俗来讲就是我们常说的 —— **前后台分离模式** - -常规PC端鉴权方法,一般由`Cookie模式`完成,而 Cookie 有两个特性: -1. 可由后端控制写入 -2. 每次请求自动提交 - -这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)
-而在app、小程序等前后台分离场景中,一般是没有 Cookie 这一功能的,此时大多数人都会一脸懵逼,咋进行鉴权啊? - -见招拆招,其实答案很简单: -- 不能后端控制写入了,就前端自己写入(难点在**后端如何将token传递到前端**) -- 每次请求不能自动提交了,那就手动提交(难点在**前端如何将token传递到后端**,同时**后端将其读取出来**) - - - -### 1、后端将 token 返回到前端 - -1. 首先调用 `StpUtil.login(id)` 进行登录 -2. 调用 `StpUtil.getTokenInfo()` 返回当前会话的token详细参数 - - 此方法返回一个对象,其有两个关键属性:`tokenName`和`tokenValue`(token 的名称和 token 的值) - - 将此对象传递到前台,让前端人员将这两个值保存到本地 - -### 2、前端将 token 提交到后端 -1. 无论是app还是小程序,其传递方式都大同小异 -2. 那就是,将 token 塞到请求`header`里 ,格式为:`{tokenName: tokenValue}` -3. 以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例: - -**方式1,简单粗暴** - -``` js -// 1、首先在登录时,将 tokenValue 存储在本地,例如: -uni.setStorageSync('tokenValue', tokenValue); - -// 2、在发起ajax请求的地方,获取这个值,并塞到header里 -uni.request({ - url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。 - header: { - "content-type": "application/x-www-form-urlencoded", - "satoken": uni.getStorageSync('tokenValue') // 关键代码, 注意参数名字是 satoken - }, - success: (res) => { - console.log(res.data); - } -}); -``` - -**方式2,更加灵活** - -``` js -// 1、首先在登录时,将tokenName和tokenValue一起存储在本地,例如: -uni.setStorageSync('tokenName', tokenName); -uni.setStorageSync('tokenValue', tokenValue); - -// 2、在发起ajax的地方,获取这两个值, 并组织到head里 -var tokenName = uni.getStorageSync('tokenName'); // 从本地缓存读取tokenName值 -var tokenValue = uni.getStorageSync('tokenValue'); // 从本地缓存读取tokenValue值 -var header = { - "content-type": "application/x-www-form-urlencoded" -}; -if (tokenName != undefined && tokenName != '') { - header[tokenName] = tokenValue; -} - -// 3、后续在发起请求时将 header 对象塞到请求头部 -uni.request({ - url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。 - header: header, - success: (res) => { - console.log(res.data); - } -}); -``` - -4. 只要按照如此方法将`token`值传递到后端,Sa-Token 就能像传统PC端一样自动读取到 token 值,进行鉴权 -5. 你可能会有疑问,难道我每个`ajax`都要写这么一坨?岂不是麻烦死了 - - 你当然不能每个 ajax 都写这么一坨,因为这种重复性代码都是要封装在一个函数里统一调用的 - - -### 其它解决方案? -如果你对 Cookie 非常了解,那你就会明白,所谓 Cookie ,本质上就是一个特殊的`header`参数而已, -而既然它只是一个 header 参数,我们就能手动模拟实现它,从而完成鉴权操作 - -这其实是对`无Cookie模式`的另一种解决方案,有兴趣的同学可以百度了解一下,在此暂不赘述 - diff --git a/sa-token-doc/doc/up/password-secure.md b/sa-token-doc/doc/up/password-secure.md deleted file mode 100644 index be563e0ad..000000000 --- a/sa-token-doc/doc/up/password-secure.md +++ /dev/null @@ -1,85 +0,0 @@ -# 密码加密 - -严格来讲,密码加密不属于 [权限认证] 的范畴,但是对于大多数系统来讲,密码加密又是安全认证不可或缺的部分, -所以,应大家要求,`Sa-Token`在`v1.14版本`添加密码加密模块,该模块非常简单,仅仅封装了一些常见的加密算法 - - - -### 摘要加密 -md5、sha1、sha256 -``` java -// md5加密 -SaSecureUtil.md5("123456"); - -// sha1加密 -SaSecureUtil.sha1("123456"); - -// sha256加密 -SaSecureUtil.sha256("123456"); - -// md5加盐加密: md5(md5(str) + md5(salt)) -SaSecureUtil.md5BySalt("123456", "salt"); -``` - - -### 对称加密 -AES加密 -``` java -// 定义秘钥和明文 -String key = "123456"; -String text = "Sa-Token 一个轻量级java权限认证框架"; - -// 加密 -String ciphertext = SaSecureUtil.aesEncrypt(key, text); -System.out.println("AES加密后:" + ciphertext); - -// 解密 -String text2 = SaSecureUtil.aesDecrypt(key, ciphertext); -System.out.println("AES解密后:" + text2); -``` - - -### 非对称加密 -RSA加密 -``` java -// 定义私钥和公钥 -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); -``` - -你可能会有疑问,私钥和公钥这么长的一大串,我怎么弄出来,手写吗?当然不是,调用以下方法生成即可 -``` java -// 生成一对公钥和私钥,其中Map对象 (private=私钥, public=公钥) -System.out.println(SaSecureUtil.rsaGenerateKeyPair()); -``` - - -### Base64编码与解码 -``` java -// 文本 -String text = "Sa-Token 一个轻量级java权限认证框架"; - -// 使用Base64编码 -String base64Text = SaBase64Util.encode(text); -System.out.println("Base64编码后:" + base64Text); - -// 使用Base64解码 -String text2 = SaBase64Util.decode(base64Text); -System.out.println("Base64解码后:" + text2); -``` - -
- -如需更多加密算法,可参考 [Hutool-crypto: 加密](https://hutool.cn/docs/#/crypto/%E6%A6%82%E8%BF%B0) - diff --git a/sa-token-doc/doc/up/remember-me.md b/sa-token-doc/doc/up/remember-me.md deleted file mode 100644 index 20bc9c17d..000000000 --- a/sa-token-doc/doc/up/remember-me.md +++ /dev/null @@ -1,81 +0,0 @@ -# [记住我] 模式 ---- - -如图所示,一般网站的登录界面都会有一个 **`[记住我]`** 按钮,当你勾选它后,即时你关闭浏览器再次打开网站,也依然会处于登录状态,无须重复验证密码 - -![../static/login-view.png](https://oss.dev33.cn/sa-token/doc/login-view.png) - -那么在Sa-Token中,如何做到 [ 记住我 ] 功能呢? - - -### 在sa-token中实现记住我功能 - -Sa-Token的登录授权,**默认就是`[记住我]`模式**,为了实现`[非记住我]`模式, 你需要在登录时如下设置: - -``` java -// 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录 -StpUtil.login(10001, false); -``` - -那么,Sa-Token实现`[记住我]`的具体原理是? - - -### 实现原理 -Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是: -- 临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失 -- 永久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失 - -利用Cookie的此特性,我们便可以轻松实现 [记住我] 模式: -- 勾选 [记住我] 按钮时:调用`StpUtil.login(10001, true)`,在浏览器写入一个`永久Cookie`储存 Token,此时用户即使重启浏览器 Token 依然有效 -- 不勾选 [记住我] 按钮时:调用`StpUtil.login(10001, false)`,在浏览器写入一个`临时Cookie`储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效 - - -### 前后台分离模式下如何实现[记住我]? - -此时机智的你😏很快发现一个问题,Cookie虽好,却无法在前后端分离环境下使用,那是不是代表上述方案在APP、小程序等环境中无效? - -准确的讲,答案是肯定的,任何基于Cookie的认证方案在前后台分离环境下都会失效(原因在于这些客户端默认没有实现Cookie功能),不过好在,这些客户端一般都提供了替代方案, -唯一遗憾的是,此场景中token的生命周期需要我们在前端手动控制 - -以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例,我们可以使用如下方式达到同样的效果: -``` js -// 使用本地存储保存token,达到 [永久Cookie] 的效果 -uni.setStorageSync("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); - -// 使用globalData保存token,达到 [临时Cookie] 的效果 -getApp().globalData.satoken = "xxxx-xxxx-xxxx-xxxx-xxx"; -``` - -如果你决定在PC浏览器环境下进行前后台分离模式开发,那么更加简单: -``` js -// 使用 localStorage 保存token,达到 [永久Cookie] 的效果 -localStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); - -// 使用 sessionStorage 保存token,达到 [临时Cookie] 的效果 -sessionStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); -``` - -Remember me, it's too easy! - - - -### 登录时指定token有效期 -登录时不仅可以指定是否为`[记住我]`模式,还可以指定一个特定的时间作为token有效时长,如下示例: -``` java -// 示例1: -// 指定token有效期(单位: 秒),如下所示token七天有效 -StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7)); - -// ----------------------- 示例2:所有参数 -// `SaLoginModel`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如: -StpUtil.login(10001, new SaLoginModel() - .setDevice("PC") // 此次登录的客户端设备标识, 用于[同端互斥登录]时指定此次登录的设备名称 - .setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) - .setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值) - ); -``` - - - - - diff --git a/sa-token-doc/doc/up/safe-auth.md b/sa-token-doc/doc/up/safe-auth.md deleted file mode 100644 index a7c57985f..000000000 --- a/sa-token-doc/doc/up/safe-auth.md +++ /dev/null @@ -1,50 +0,0 @@ -# 二级认证 - -在某些敏感操作下,我们需要对已登录的会话进行二次验证 - -比如 Gitee 的仓库删除操作,虽然我们已经登录了账号,当我们点击 **[删除]** 按钮时,还是需要再次输入一遍密码,这么做主要为了两点: - -1. 保证操作者是当前账号本人 -2. 增加操作步骤,防止误删除重要数据 - -这就是我们本篇要讲的 —— 二级认证,即:在已登录会话的基础上,进行再次验证,提高会话的安全性。 - - ---- - -## 具体API - -在`Sa-Token`中进行二级认证非常简单,只需要使用以下API: - -``` java -// 在当前会话 开启二级认证 -StpUtil.openSafe(120); - -// 获取:当前会话是否处于二级认证时间内 -StpUtil.isSafe(); - -// 检查当前会话是否已通过二级认证,如未通过则抛出异常 -StpUtil.checkSafe(); - -// 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) -StpUtil.getSafeTime(); - -// 在当前会话 结束二级认证 -StpUtil.closeSafe(); -``` - - -## 使用注解进行二级认证 -在一个方法上使用 `@SaCheckSafe` 注解,可以在代码进入之前此方法之前进行一次二级认证 -``` java -// 二级认证:必须二级认证之后才能进入该方法 -@SaCheckSafe -@RequestMapping("add") -public String add() { - return "用户增加"; -} -``` - -详细使用方法可参考:[注解鉴权](/use/at-check),此处不再赘述 - - diff --git a/sa-token-doc/doc/up/search-session.md b/sa-token-doc/doc/up/search-session.md deleted file mode 100644 index b431b8005..000000000 --- a/sa-token-doc/doc/up/search-session.md +++ /dev/null @@ -1,48 +0,0 @@ -# 会话治理 - -尽管框架将大部分操作提供了简易的封装,但在一些特殊场景下,我们仍需要绕过框架,直达数据底层进行一些操作
-Sa-Token提供以下API助你直接操作会话列表 - - ---- - -## 具体API - -``` java -// 查询所有token -StpUtil.searchTokenValue(String keyword, int start, int size); - -// 查询所有账号Session会话 -StpUtil.searchSessionId(String keyword, int start, int size); - -// 查询所有令牌Session会话 -StpUtil.searchTokenSessionId(String keyword, int start, int size); -``` - - -#### 参数详解: -- `keyword`: 查询关键字,只有包括这个字符串的token值才会被查询出来 -- `start`: 数据开始处索引, 值为-1时代表一次性取出所有数据 -- `size`: 要获取的数据条数 - -使用示例: -``` java -// 查询value包括1000的所有token,结果集从第0条开始,返回10条 -List tokenList = StpUtil.searchTokenValue("1000", 0, 10); -for (String token : tokenList) { - System.out.println(token); -} -``` - - -
- -#### 注意事项: -由于会话查询底层采用了遍历方式获取数据,当数据量过大时此操作将会比较耗时,有多耗时呢?这里提供一份参考数据: -- 单机模式下:百万会话取出10条token平均耗时 `0.255s` -- Redis模式下:百万会话取出10条token平均耗时 `3.322s` - -请根据业务实际水平合理调用API - - -> 如果需要实时获取当前登录人数或者需要在用户退出后自动触发某事件等, 建议采用websocket技术 \ No newline at end of file diff --git a/sa-token-doc/doc/up/token-prefix.md b/sa-token-doc/doc/up/token-prefix.md deleted file mode 100644 index 92ad69e82..000000000 --- a/sa-token-doc/doc/up/token-prefix.md +++ /dev/null @@ -1,30 +0,0 @@ -# 自定义 Token 前缀 - -### 需求场景 - -在某些系统中,前端提交token时会在前面加个固定的前缀,例如: - -``` js -{ - "satoken": "Bearer xxxx-xxxx-xxxx-xxxx" -} -``` - -此时后端如果不做任何特殊处理,框架将会把`Bearer `视为token的一部分,无法正常读取token信息,导致鉴权失败 - -为此,我们需要在yml中添加如下配置: -``` java -sa-token: - # token前缀 - token-prefix: Bearer -``` - -此时 Sa-Token 便可在读取 Token 时裁剪掉 `Bearer`,成功获取`xxxx-xxxx-xxxx-xxxx` - - -### 注意点 - -1. Token前缀 与 Token值 之间必须有一个空格 -2. 一旦配置了 Token前缀,则前端提交token时,必须带有前缀,否则会导致框架无法读取token -3. 由于`Cookie`中无法存储空格字符,也就意味配置token前缀后,Cookie鉴权方式将会失效,此时只能将token提交到`header`里进行传输 - diff --git a/sa-token-doc/doc/up/token-style.md b/sa-token-doc/doc/up/token-style.md deleted file mode 100644 index a42207047..000000000 --- a/sa-token-doc/doc/up/token-style.md +++ /dev/null @@ -1,108 +0,0 @@ -# 自定义 Token 风格 - -本篇介绍token生成的各种风格,以及自定义token生成策略 - ---- - - -## 内置风格 - -Sa-Token默认的token生成策略是uuid风格,其模样类似于:`623368f0-ae5e-4475-a53f-93e4225f16ae`
-如果你对这种风格不太感冒,还可以将token生成设置为其他风格 - -怎么设置呢?只需要在yml配置文件里设置 `sa-token.token-style=风格类型` 即可,其有多种取值: - -``` java -// 1. token-style=uuid —— uuid风格 (默认风格) -"623368f0-ae5e-4475-a53f-93e4225f16ae" - -// 2. token-style=simple-uuid —— 同上,uuid风格, 只不过去掉了中划线 -"6fd4221395024b5f87edd34bc3258ee8" - -// 3. token-style=random-32 —— 随机32位字符串 -"qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W" - -// 4. token-style=random-64 —— 随机64位字符串 -"v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc" - -// 5. token-style=random-128 —— 随机128位字符串 -"nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj" - -// 6. token-style=tik —— tik风格 -"gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__" -``` - - -## 自定义token生成策略 - -如果你觉着以上风格都不是你喜欢的类型,那么你还可以**自定义token生成策略**,来定制化token生成风格
- -怎么做呢?只需要重写`SaTokenAction`接口的`createToken`方法即可 - - -#### 参考步骤如下: -1、新建文件`MySaTokenAction.java`,继承`SaTokenActionDefaultImpl`默认实现类, 并添加上注解`@Component`,保证此类被`springboot`扫描到 -``` java -package com.pj.satoken; - -import org.springframework.stereotype.Component; -import cn.dev33.satoken.action.SaTokenActionDefaultImpl; - -/** - * 继承Sa-Token行为Bean默认实现, 重写部分逻辑 - */ -@Component -public class MySaTokenAction extends SaTokenActionDefaultImpl { - // 重写token生成策略 - @Override - public String createToken(Object loginId, String loginType) { - return SaFoxUtil.getRandomString(60); // 随机60位字符串 - } -} -``` - -2、再次调用 `StpUtil.login(10001)`方法进行登录,观察其生成的token样式: -``` html -gfuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH -``` - - - - - - - - - - \ No newline at end of file diff --git a/sa-token-doc/doc/use/at-check.md b/sa-token-doc/doc/use/at-check.md deleted file mode 100644 index 8444b4a6c..000000000 --- a/sa-token-doc/doc/use/at-check.md +++ /dev/null @@ -1,102 +0,0 @@ -# 注解式鉴权 ---- - -有同学表示:尽管使用代码鉴权非常方便,但是我仍希望把鉴权逻辑和业务逻辑分离开来,我可以使用注解鉴权吗?当然可以!
- -注解鉴权 —— 优雅的将鉴权与业务代码分离! - -- `@SaCheckLogin`: 登录认证 —— 只有登录之后才能进入该方法 -- `@SaCheckRole("admin")`: 角色认证 —— 必须具有指定角色标识才能进入该方法 -- `@SaCheckPermission("user:add")`: 权限认证 —— 必须具有指定权限才能进入该方法 -- `@SaCheckSafe`: 二级认证校验 —— 必须二级认证之后才能进入该方法 - -Sa-Token使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
-因此,为了使用注解鉴权,你必须手动将Sa-Token的全局拦截器注册到你项目中 - - - - -### 1、注册拦截器 -以`SpringBoot2.0`为例, 新建配置类`SaTokenConfigure.java` - -``` java -@Configuration -public class SaTokenConfigure implements WebMvcConfigurer { - // 注册Sa-Token的注解拦截器,打开注解式鉴权功能 - @Override - public void addInterceptors(InterceptorRegistry registry) { - // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关) - registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**"); - } -} -``` -保证此类被`springboot`启动类扫描到即可 - - -### 2、使用注解鉴权 -然后我们就可以愉快的使用注解鉴权: - -``` java -// 登录认证:只有登录之后才能进入该方法 -@SaCheckLogin -@RequestMapping("info") -public String info() { - return "查询用户信息"; -} - -// 角色认证:必须具有指定角色才能进入该方法 -@SaCheckRole("super-admin") -@RequestMapping("add") -public String add() { - return "用户增加"; -} - -// 权限认证:必须具有指定权限才能进入该方法 -@SaCheckPermission("user-add") -@RequestMapping("add") -public String add() { - return "用户增加"; -} - -// 二级认证:必须二级认证之后才能进入该方法 -@SaCheckSafe() -@RequestMapping("add") -public String add() { - return "用户增加"; -} -``` - -注:以上注解都可以加在类上,代表为这个类所有方法进行鉴权 - - -### 3、设定校验模式 -`@SaCheckRole`与`@SaCheckPermission`注解可设置校验模式,例如: -``` java -// 注解式鉴权:只要具有其中一个权限即可通过校验 -@RequestMapping("atJurOr") -@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) -public AjaxJson atJurOr() { - return AjaxJson.getSuccessData("用户信息"); -} -``` - - -mode有两种取值: -- `SaMode.AND`, 标注一组权限,会话必须全部具有才可通过校验 -- `SaMode.OR`, 标注一组权限,会话只要具有其一即可通过校验 - - - -### 4、在业务逻辑层使用注解鉴权 -疑问:我能否将注解写在其它架构层呢,比如业务逻辑层? - -使用拦截器模式,只能在`Controller层`进行注解鉴权,如需在任意层级使用注解鉴权,请参考:[AOP注解鉴权](/plugin/aop-at) - - - - - - - - diff --git a/sa-token-doc/doc/use/config.md b/sa-token-doc/doc/use/config.md deleted file mode 100644 index 9c2616451..000000000 --- a/sa-token-doc/doc/use/config.md +++ /dev/null @@ -1,149 +0,0 @@ -# 框架配置 -你可以**零配置启动框架**
-但同时你也可以通过配置,定制性使用框架,`Sa-Token`支持多种方式配置框架信息 - - - - - -### 方式1、在 application.yml 配置 - -``` java -# Sa-Token 配置 -sa-token: - # token名称 (同时也是cookie名称) - token-name: satoken - # token有效期,单位s 默认30天, -1代表永不过期 - timeout: 2592000 - # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 - activity-timeout: -1 - # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) - is-concurrent: true - # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) - is-share: true - # token风格 - token-style: uuid - # 是否输出操作日志 - is-log: false -``` - -如果你习惯于 `application.properties` 类型的配置文件,那也很好办: 百度: [springboot properties与yml 配置文件的区别](https://www.baidu.com/s?ie=UTF-8&wd=springboot%20properties%E4%B8%8Eyml%20%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E7%9A%84%E5%8C%BA%E5%88%AB) - -!> 注:旧版本配置前缀为`[spring.sa-token.]`,自v1.21.0开始,均改为`[sa-token.]`,目前版本暂时向下兼容,请尽快更新 - - -### 方式2、通过代码配置 -方式1: -``` java -/** - * Sa-Token代码方式进行配置 - */ -@Configuration -public class SaTokenConfigure { - - // 获取配置Bean (以代码的方式配置Sa-Token, 此配置会覆盖yml中的配置) - @Bean - @Primary - public SaTokenConfig getSaTokenConfigPrimary() { - SaTokenConfig config = new SaTokenConfig(); - config.setTokenName("satoken"); // token名称 (同时也是cookie名称) - config.setTimeout(30 * 24 * 60 * 60); // token有效期,单位s 默认30天 - config.setActivityTimeout(-1); // token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 - config.setIsConcurrent(true); // 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) - config.setIsShare(true); // 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) - config.setTokenStyle("uuid"); // token风格 - config.setIsLog(false); // 是否输出操作日志 - return config; - } - -} -``` - -方式2: -``` java -// 以代码的方式配置Sa-Token-Config -@Autowired -public void configSaToken(SaTokenConfig config) { - // config.setTokenName("satoken333"); // token名称 (同时也是cookie名称) - // ... -} -``` - -PS:两者的区别在于:**`方式1会覆盖yml中的配置,方式2会与yml中的配置合并`** - - ---- -### 所有可配置项 -| 参数名称 | 类型 | 默认值 | 说明 | -| :-------- | :-------- | :-------- | :-------- | -| tokenName | String | satoken | token名称 (同时也是cookie名称) | -| timeout | long | 2592000 | token有效期,单位/秒 默认30天,-1代表永久有效 [参考:token有效期详解](/fun/token-timeout) | -| activityTimeout | long | -1 | token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒, 默认-1 代表不限制 (例如可以设置为1800代表30分钟内无操作就过期) [参考:token有效期详解](/fun/token-timeout) | -| isConcurrent | Boolean | true | 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) | -| isShare | Boolean | true | 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) | -| isReadBody | Boolean | true | 是否尝试从请求体里读取token | -| isReadHead | Boolean | true | 是否尝试从header里读取token | -| isReadCookie | Boolean | true | 是否尝试从cookie里读取token | -| tokenStyle | String | uuid | token风格, [参考:自定义Token风格](/up/token-style) | -| dataRefreshPeriod | int | 30 | 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理 | -| tokenSessionCheckLogin | Boolean | true | 获取token专属session时是否必须登录 (如果配置为true,会在每次获取token专属session时校验是否登录) | -| autoRenew | Boolean | true | 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作) | -| tokenPrefix | Boolean | true | token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) [参考:自定义Token前缀](/up/token-prefix) | -| isPrint | Boolean | true | 是否在初始化配置时打印版本字符画 | -| isLog | Boolean | false | 是否打印操作日志 | -| jwtSecretKey | String | null | jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) | -| idTokenTimeout | long | 86400 | Id-Token的有效期 (单位: 秒) | -| sso | Object | new SaSsoConfig() | SSO 单点登录相关配置 | - - -### 单点登录相关配置 -| 参数名称 | 类型 | 默认值 | 所属端 | 说明 | -| :-------- | :-------- | :-------- | :-------- | :-------- | -| ticketTimeout | long | 300 | Server端 | Ticket有效期 (单位: 秒) | -| allowUrl | String | * | Server端 | 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket),[详情请参考 SSO模式二的域名校验章节] | -| secretkey | String | null | Server端 & Client端 | 调用秘钥 (用于SSO模式三单点注销的接口通信身份校验) | -| authUrl | String | null | Client端 | SSO-Server端 单点登录地址 | -| isHttp | Boolean | false | Server端 & Client端 | 是否使用http请求校验ticket值 | -| checkTicketUrl| String | null | Client端 | SSO-Server端 Ticket校验地址 | -| isSlo | Boolean | false | Client端 | 是否打开单点注销功能 | -| sloUrl | String | null | Client端 | SSO-Server端 单点注销地址 | -| ssoLogoutCall | String | null | Client端 | SSO-Client端 当前Client端的单点注销回调URL (为空时自动获取) | -| userinfoUrl | String | null | Client端 | SSO-Server端 查询userinfo地址 | - -配置示例: -``` yml -# Sa-Token 配置 -sa-token: - # SSO-相关配置 - sso: - # SSO-Server端 单点登录地址 - auth-url: http://sa-sso-server.com:9000/sso/auth -``` - - - -### OAuth2.0相关配置 -| 参数名称 | 类型 | 默认值 | 说明 | -| :-------- | :-------- | :-------- | :-------- | -| isCode | Boolean | true | 是否打开模式:授权码(Authorization Code) | -| isImplicit | Boolean | false | 是否打开模式:隐藏式(Implicit) | -| isPassword | Boolean | false | 是否打开模式:密码式(Password) | -| isClient | Boolean | false | 是否打开模式:凭证式(Client Credentials) | -| isNewRefresh | Boolean | false | 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token | -| codeTimeout | long | 300 | Code授权码 保存的时间(单位秒) 默认五分钟 | -| accessTokenTimeout | long | 7200 | Access-Token 保存的时间(单位秒) 默认两个小时 | -| refreshTokenTimeout | long | 2592000 | Refresh-Token 保存的时间(单位秒) 默认30 天 | -| clientTokenTimeout | long | 7200 | Client-Token 保存的时间(单位秒) 默认两个小时 | - -配置示例: -``` yml -# Sa-Token 配置 -sa-token: - token-name: satoken-server - # OAuth2.0 配置 - oauth2: - is-code: true - is-implicit: true - is-password: true - is-client: true -``` diff --git a/sa-token-doc/doc/use/dao-extend.md b/sa-token-doc/doc/use/dao-extend.md deleted file mode 100644 index 18c8097df..000000000 --- a/sa-token-doc/doc/use/dao-extend.md +++ /dev/null @@ -1,87 +0,0 @@ -# 持久层扩展 ---- - -Sa-token默认将会话数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如:重启后数据会丢失,无法在集群模式下共享数据 - -为此,Sa-Token将数据持久操作全部抽象到 `SaTokenDao` 接口中,保证大家对框架进行灵活扩展,比如我们可以将会话数据存储在 `Redis`、`Memcached`等专业的缓存中间件中,做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性 - -除了框架内部对`SaTokenDao`提供的基于内存的默认实现,官方仓库还提供了以下扩展方案:
- - -### 1. Sa-Token 整合 Redis (使用jdk默认序列化方式) -``` xml - - - cn.dev33 - sa-token-dao-redis - ${sa.top.version} - -``` -优点:兼容性好,缺点:Session序列化后基本不可读,对开发者来讲等同于乱码 - - -### 2. Sa-Token 整合 Redis (使用jackson序列化方式) -``` xml - - - cn.dev33 - sa-token-dao-redis-jackson - ${sa.top.version} - -``` -优点:Session序列化后可读性强,可灵活手动修改,缺点:兼容性稍差 - - -
- -### 集成Redis请注意: - - -**1. 无论使用哪种序列化方式,你都必须为项目提供一个Redis实例化方案,例如:** -``` xml - - - org.apache.commons - commons-pool2 - -``` - -**2. 引入了依赖,我还需要为Redis配置连接信息吗?**
-需要!只有项目初始化了正确的Redis实例,`Sa-Token`才可以使用Redis进行数据持久化,参考以下`yml配置`: -``` java -# 端口 -spring: - # redis配置 - redis: - # Redis数据库索引(默认为0) - database: 1 - # Redis服务器地址 - host: 127.0.0.1 - # Redis服务器连接端口 - port: 6379 - # Redis服务器连接密码(默认为空) - # password: - # 连接超时时间(毫秒) - timeout: 1000ms - lettuce: - pool: - # 连接池最大连接数 - max-active: 200 - # 连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - # 连接池中的最大空闲连接 - max-idle: 10 - # 连接池中的最小空闲连接 - min-idle: 0 -``` - - -**3. 集成Redis后,是我额外手动保存数据,还是框架自动保存?**
-框架自动保存。集成`Redis`只需要引入对应的`pom依赖`即可,框架所有上层API保持不变 - - -

-更多框架的集成方案正在更新中... (欢迎大家提交pr) - - - diff --git a/sa-token-doc/doc/use/jur-auth.md b/sa-token-doc/doc/use/jur-auth.md deleted file mode 100644 index 89d6186ac..000000000 --- a/sa-token-doc/doc/use/jur-auth.md +++ /dev/null @@ -1,216 +0,0 @@ -# 权限认证 ---- - - -### 核心思想 - -所谓权限认证,认证的核心就是一个账号是否拥有一个权限码
-有,就让你通过。没有?那么禁止访问! - -再往底了说,就是每个账号都会拥有一个权限码集合,我来校验这个集合中是否包含指定的权限码
-例如:当前账号拥有权限码集合:`["user-add", "user-delete", "user-get"]`,这时候我来校验权限 `"user-update"`,则其结果就是:**验证失败,禁止访问**
- -所以现在问题的核心就是: -1. 如何获取一个账号所拥有的的权限码集合 -2. 本次操作需要验证的权限码是哪个 - -### 获取当前账号权限码集合 -因为每个项目的需求不同,其权限设计也千变万化,因此【获取当前账号权限码集合】这一操作不可能内置到框架中, -所以 Sa-Token 将此操作以接口的方式暴露给你,以方便的你根据自己的业务逻辑进行重写 - -你需要做的就是新建一个类,实现`StpInterface`接口,例如以下代码: - -``` java -package com.pj.satoken; - -import java.util.ArrayList; -import java.util.List; -import org.springframework.stereotype.Component; -import cn.dev33.satoken.stp.StpInterface; - -/** - * 自定义权限验证接口扩展 - */ -@Component // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展 -public class StpInterfaceImpl implements StpInterface { - - /** - * 返回一个账号所拥有的权限码集合 - */ - @Override - public List getPermissionList(Object loginId, String loginType) { - // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 - List list = new ArrayList(); - list.add("101"); - list.add("user-add"); - list.add("user-delete"); - list.add("user-update"); - list.add("user-get"); - list.add("article-get"); - return list; - } - - /** - * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) - */ - @Override - public List getRoleList(Object loginId, String loginType) { - // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 - List list = new ArrayList(); - list.add("admin"); - list.add("super-admin"); - return list; - } - -} -``` - -可参考代码:[码云:StpInterfaceImpl.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/StpInterfaceImpl.java) - - - - - -### 权限认证 -然后就可以用以下api来鉴权了 - -``` java -// 当前账号是否含有指定权限, 返回true或false -StpUtil.hasPermission("user-update"); - -// 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException -StpUtil.checkPermission("user-update"); - -// 当前账号是否含有指定权限 [指定多个,必须全部验证通过] -StpUtil.checkPermissionAnd("user-update", "user-delete"); - -// 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] -StpUtil.checkPermissionOr("user-update", "user-delete"); -``` - -扩展:`NotPermissionException` 对象可通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常 - - -### 角色认证 -在Sa-Token中,角色和权限可以独立验证 - -``` java -// 当前账号是否含有指定角色标识, 返回true或false -StpUtil.hasRole("super-admin"); - -// 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException -StpUtil.checkRole("super-admin"); - -// 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] -StpUtil.checkRoleAnd("super-admin", "shop-admin"); - -// 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] -StpUtil.checkRoleOr("super-admin", "shop-admin"); -``` - -扩展:`NotRoleException` 对象可通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常 - - - -### 拦截全局异常 -有同学要问,鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?**当然不可以!**
-你可以创建一个全局异常拦截器,统一返回给前端的格式,参考:[码云:GlobalException.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/GlobalException.java) - - -### 权限通配符 -Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有`user*`的权限时,`user-add`、`user-delete`、`user-update`都将匹配通过 - -``` java -// 当拥有 user* 权限时 -StpUtil.hasPermission("user-add"); // true -StpUtil.hasPermission("user-update"); // true -StpUtil.hasPermission("art-add"); // false - -// 当拥有 *-delete 权限时 -StpUtil.hasPermission("user-add"); // false -StpUtil.hasPermission("user-delete"); // true -StpUtil.hasPermission("art-delete"); // true - -// 当拥有 *.js 权限时 -StpUtil.hasPermission("index.js"); // true -StpUtil.hasPermission("index.css"); // false -StpUtil.hasPermission("index.html"); // false -``` - -上帝权限:当一个账号拥有 `"*"` 权限时,他可以验证通过任何权限码 (角色认证同理) - - -### 如何把权限精确搭到按钮级? -权限精确到按钮级的意思就是指:**权限范围可以控制到页面上的每一个按钮是否显示** - -思路:如此精确的范围控制只依赖后端已经难以完成,此时需要前端进行一定的逻辑判断 - -1. 在登录时,把当前账号拥有的所有权限码一次性返回给前端 -2. 前端将权限码集合保存在`localStorage`或其它全局状态管理对象中 -3. 在需要权限控制的按钮上,使用js进行逻辑判断,例如在`vue`框架中我们可以使用如下写法: -``` js - -``` -其中:`arr`是当前用户拥有的权限码数组,`user:delete`是显示按钮需要拥有的权限码,`删除按钮`是用户拥有权限码才可以看到的内容 - - -注意:以上写法只为提供一个参考示例,不同框架有不同写法,开发者可根据项目技术栈灵活封装进行调用 - - -### 前端有了鉴权后端还需要鉴权吗? -**需要!** - -前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全,无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验! - - -### 将权限数据放在缓存里 -前面我们讲解了如何通过`StpInterface`接口注入权限数据,框架默认是不提供缓存能力的,如果你想减小数据库的访问压力,则需要将权限数据放到缓存中 - -参考示例: -``` java -/** - * 返回一个账号所拥有的权限码集合 - */ -@Override -public List getPermissionList(Object loginId, String loginType) { - - // 1. 获取这个账号所属角色id - long roleId = StpUtil.getSessionByLoginId(loginId).get("Role_Id", () -> { - return ...; // 从数据库查询这个账号所属的角色id - }); - - // 2. 获取这个角色id拥有的权限列表 - SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleId); - List list = roleSession.get("Permission_List", () -> { - return ...; // 从数据库查询这个角色id拥有的权限列表 - }); - - // 3. 返回 - return list; -} -``` -以上仅为代码示例,角色列表步骤同理 - -##### 疑问:为什么不直接缓存 `[账号id->权限列表]`的关系,而是 `[账号id -> 角色id -> 权限列表]`? - - - -答:`[账号id->权限列表]`的缓存方式虽然更加直接粗暴,却有一个严重的问题: - -- 通常我们系统的权限架构是RBAC模型:权限与用户没有直接的关系,而是:用户拥有指定的角色,角色再拥有指定的权限 -- 而这种'拥有关系'是动态的,是可以随时修改的,一旦我们修改了它们的对应关系,便要同步修改或清除对应的缓存数据 - -现在假设如下业务场景:我们系统中有十万个账号属于同一个角色,当我们变动这个角色的权限时,难道我们要同时清除这十万个账号的缓存信息吗? -这显然是一个不合理的操作,同一时间缓存大量清除容易引起Redis的缓存雪崩 - -而当我们采用 `[账号id -> 角色id -> 权限列表]` 的缓存模型时,则只需要清除或修改 `[角色id -> 权限列表]` 一条缓存即可 - -一言以蔽之:权限的缓存模型需要跟着权限模型走,角色缓存亦然 - - diff --git a/sa-token-doc/doc/use/kick.md b/sa-token-doc/doc/use/kick.md deleted file mode 100644 index 234f310e2..000000000 --- a/sa-token-doc/doc/use/kick.md +++ /dev/null @@ -1,56 +0,0 @@ -# 踢人下线 -所谓踢人下线,核心操作就是找到其指定`loginId`对应的`token`,并设置其失效 - -![踢下线](https://oss.dev33.cn/sa-token/doc/kickout.png) - ---- - - -### 根据账号id踢人 -让指定账号id的会话注销登录,例如: - -``` java -// 使账号id为10001的会话注销登录(踢人下线),待到10001再次访问系统时会抛出`NotLoginException`异常,场景值为-5 -StpUtil.logoutByLoginId(10001); -``` - -### 根据Token令牌踢人 -你还可以让指定token的会话注销登录 -``` java -// 使账号id为10001的会话注销登录 -StpUtil.logoutByTokenValue("xxxx-xxxx-xxxx-xxxx-xxxx"); -``` -此方法直接删除了`token->uid`的映射关系,对方再次访问时提示:`token无效`,场景值为-2 - - - -### 账号封禁 -对于违规账号,有时候我们仅仅将其踢下线还是远远不够的,我们还需要对其进行**账号封禁**防止其再次登录 - -``` java -// 封禁指定账号 -// 参数一:账号id -// 参数二:封禁时长,单位:秒 (86400秒=1天,此值为-1时,代表永久封禁) -StpUtil.disable(10001, 86400); - -// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁) -StpUtil.isDisable(10001); - -// 获取指定账号剩余封禁时间,单位:秒 -StpUtil.getDisableTime(10001); - -// 解除封禁 -StpUtil.untieDisable(10001); -``` - - -#### 注意点 -对于正在登录的账号,对其账号封禁时并不会使其立刻注销
-如果需要将其封禁后立即掉线,可采取先踢再封禁的策略,例如: -``` java -// 先踢下线 -StpUtil.logoutByLoginId(10001); -// 再封禁账号 -StpUtil.disable(10001, 86400); -``` - diff --git a/sa-token-doc/doc/use/login-auth.md b/sa-token-doc/doc/use/login-auth.md deleted file mode 100644 index 82c8eb504..000000000 --- a/sa-token-doc/doc/use/login-auth.md +++ /dev/null @@ -1,71 +0,0 @@ -# 登录认证 ---- - - -### 核心思想 - -所谓登录认证,说白了就是限制某些API接口必须登录后才能访问(例:查询我的账号资料)
-那么如何判断一个会话是否登录?框架会在登录成功后给你做个标记,每次登录认证时校验这个标记,有标记者视为已登录,无标记者视为未登录! - - -### 登录与注销 -根据以上思路,我们很容易想到以下api: - -``` java -// 标记当前会话登录的账号id -// 建议的参数类型:long | int | String, 不可以传入复杂类型,如:User、Admin等等 -StpUtil.login(Object id); - -// 当前会话注销登录 -StpUtil.logout(); - -// 获取当前会话是否已经登录,返回true=已登录,false=未登录 -StpUtil.isLogin(); - -// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException` -StpUtil.checkLogin() -``` - -##### `NotLoginException`异常对象扩展: -1. 通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常
-2. 通过 `getType()` 方法获取具体的场景值,详细参考章节:[未登录场景值](/fun/not-login-scene) - - -### 会话查询 -``` java -// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException` -StpUtil.getLoginId(); - -// 类似查询API还有: -StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型 -StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型 -StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型 - -// ---------- 指定未登录情形下返回的默认值 ---------- - -// 获取当前会话账号id, 如果未登录,则返回null -StpUtil.getLoginIdDefaultNull(); - -// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型) -StpUtil.getLoginId(T defaultValue); -``` - - -### 其它API -``` java -// 获取指定token对应的账号id,如果未登录,则返回 null -StpUtil.getLoginIdByToken(String tokenValue); - -// 获取当前`StpLogic`的token名称 -StpUtil.getTokenName(); - -// 获取当前会话的token值 -StpUtil.getTokenValue(); - -// 获取当前会话的token信息参数 -StpUtil.getTokenInfo(); -``` - -有关`TokenInfo`参数详解,请参考:[TokenInfo参数详解](/fun/token-info) - - diff --git a/sa-token-doc/doc/use/route-check.md b/sa-token-doc/doc/use/route-check.md deleted file mode 100644 index 6270d41fd..000000000 --- a/sa-token-doc/doc/use/route-check.md +++ /dev/null @@ -1,143 +0,0 @@ -# 路由拦截鉴权 ---- - -假设我们有如下需求: -> 项目中所有接口均需要登录验证,只有'登录接口'本身对外开放 - -我们怎么实现呢?给每个接口加上鉴权注解?手写全局拦截器?似乎都不是非常方便。
-在这个需求中我们真正需要的是一种基于路由拦截的鉴权模式, 那么在Sa-Token怎么实现路由拦截鉴权呢? - - - -### 1、注册路由拦截器 -以`SpringBoot2.0`为例, 新建配置类`SaTokenConfigure.java` -``` java -@Configuration -public class SaTokenConfigure implements WebMvcConfigurer { - // 注册拦截器 - @Override - public void addInterceptors(InterceptorRegistry registry) { - // 注册Sa-Token的路由拦截器,并排除登录接口或其他可匿名访问的接口地址 (与注解拦截器无关) - registry.addInterceptor(new SaRouteInterceptor()).addPathPatterns("/**").excludePathPatterns("/user/doLogin"); - } -} -``` -以上代码,我们注册了一个登录验证拦截器,并且排除了`/user/doLogin`接口用来开放登录(除了`/user/doLogin`以外的所有接口都需要登录才能访问)
-那么我们如何进行权限认证拦截呢,且往下看 - - -### 2、自定义权限验证规则 -你可以使用函数式编程自定义验证规则 - -``` java -@Configuration -public class SaTokenConfigure implements WebMvcConfigurer { - @Override - public void addInterceptors(InterceptorRegistry registry) { - // 注册路由拦截器,自定义验证规则 - registry.addInterceptor(new SaRouteInterceptor((req, res, handler)->{ - // 根据路由划分模块,不同模块不同鉴权 - SaRouter.match("/user/**", () -> StpUtil.checkPermission("user")); - SaRouter.match("/admin/**", () -> StpUtil.checkPermission("admin")); - SaRouter.match("/goods/**", () -> StpUtil.checkPermission("goods")); - SaRouter.match("/orders/**", () -> StpUtil.checkPermission("orders")); - SaRouter.match("/notice/**", () -> StpUtil.checkPermission("notice")); - SaRouter.match("/comment/**", () -> StpUtil.checkPermission("comment")); - })).addPathPatterns("/**"); - } -} -``` - -### 3、完整示例 -所有用法示例: - -``` java -@Configuration -public class SaTokenConfigure implements WebMvcConfigurer { - // 注册Sa-Token的拦截器 - @Override - public void addInterceptors(InterceptorRegistry registry) { - // 注册路由拦截器,自定义验证规则 - registry.addInterceptor(new SaRouteInterceptor((req, res, handler) -> { - - // 登录验证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 - SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin()); - - // 登录验证 -- 排除多个路径 - SaRouter.match(Arrays.asList("/**"), Arrays.asList("/user/doLogin", "/user/reg"), () -> StpUtil.checkLogin()); - - // 角色认证 -- 拦截以 admin 开头的路由,必须具备[admin]角色或者[super-admin]角色才可以通过认证 - SaRouter.match("/admin/**", () -> StpUtil.checkRoleOr("admin", "super-admin")); - - // 权限认证 -- 不同模块, 校验不同权限 - SaRouter.match("/user/**", () -> StpUtil.checkPermission("user")); - SaRouter.match("/admin/**", () -> StpUtil.checkPermission("admin")); - SaRouter.match("/goods/**", () -> StpUtil.checkPermission("goods")); - SaRouter.match("/orders/**", () -> StpUtil.checkPermission("orders")); - SaRouter.match("/notice/**", () -> StpUtil.checkPermission("notice")); - SaRouter.match("/comment/**", () -> StpUtil.checkPermission("comment")); - - // 匹配 restful 风格路由 - SaRouter.match("/article/get/{id}", () -> StpUtil.checkPermission("article")); - - // 检查请求方式 - SaRouter.match("/notice/**", () -> { - if(req.getMethod().equals(HttpMethod.GET.toString())) { - StpUtil.checkPermission("notice"); - } - }); - - // 提前退出 (执行SaRouter.stop()后会直接退出匹配链) - SaRouter.match("/test/back", () -> SaRouter.stop()); - - // 在多账号模式下,可以使用任意StpUtil进行校验 - SaRouter.match("/user/**", () -> StpUserUtil.checkLogin()); - - })).addPathPatterns("/**"); - } -} -``` - - -### 4、提前退出匹配链条 -使用 `SaRouter.stop()` 可以提前退出匹配链,例: - -``` java -// 原写法 -registry.addInterceptor(SaRouteInterceptor.createPermissionVal("user")).addPathPatterns("/user/**"); - -// 改为以下方式,效果同上 -registry.addInterceptor(new SaRouteInterceptor((req, res, handler) -> { - SaRouter.match("/**", () -> System.out.println("进入1")); - SaRouter.match("/**", () -> {System.out.println("进入2"); SaRouter.stop();}); - SaRouter.match("/**", () -> System.out.println("进入3")); -})).addPathPatterns("/**"); -``` -如上示例,代码运行至第2条匹配链时,会在stop函数处提前退出整个匹配函数,从而忽略掉剩余的所有match匹配 - -除了`stop()`函数,`SaRouter`还提供了 `SaRouter.back()` 函数,用于:停止匹配,结束执行,直接向前端返回结果 -``` java -SaRouter.match("/user/back", () -> SaRouter.back("执行back函数后将停止匹配,也不会进入Controller,而是直接将此参数作为返回值输出到前端")); -``` - - - - - - - - - \ No newline at end of file diff --git a/sa-token-doc/doc/use/session.md b/sa-token-doc/doc/use/session.md deleted file mode 100644 index c4b343528..000000000 --- a/sa-token-doc/doc/use/session.md +++ /dev/null @@ -1,163 +0,0 @@ -# Session会话 ---- - -### Session是什么? - -Session是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据,提高程序性能
-在 Sa-Token 中,Session 分为三种,分别是: - -- `User-Session`: 指的是框架为每个 账号id 分配的 Session -- `Token-Session`: 指的是框架为每个 token 分配的 Session -- `Custom-Session`: 指的是以一个 特定的值 作为SessionId,来分配的 Session - -> 有关User-Session与Token-Session的详细区别,请参考:[Session模型详解](/fun/session-model) - - -### User-Session -有关账号Session的API如下: -``` java -// 获取当前账号id的Session (必须是登录后才能调用) -StpUtil.getSession(); - -// 获取当前账号id的Session, 并决定在Session尚未创建时,是否新建并返回 -StpUtil.getSession(true); - -// 获取账号id为10001的Session -StpUtil.getSessionByLoginId(10001); - -// 获取账号id为10001的Session, 并决定在Session尚未创建时,是否新建并返回 -StpUtil.getSessionByLoginId(10001, true); - -// 获取SessionId为xxxx-xxxx的Session, 在Session尚未创建时, 返回null -StpUtil.getSessionBySessionId("xxxx-xxxx"); -``` - - -### Token-Session -有关令牌Session的API如下: -``` java -// 获取当前token的专属Session -StpUtil.getTokenSession(); - -// 获取指定token的专属Session -StpUtil.getTokenSessionByToken(token); -``` -在未登录状态下是否可以获取`Token-Session`?这取决于你配置的`tokenSessionCheckLogin`值是否为false,详见:[框架配置](/use/config?id=所有可配置项) - - -### 自定义Session -自定义Session指的是以一个`特定的值`作为SessionId来分配的`Session`, 借助自定义Session,你可以为系统中的任意元素分配相应的session
-例如以商品id作为key为每个商品分配一个Session,以便于缓存和商品相关的数据,其相关API如下: -``` java -// 查询指定key的Session是否存在 -SaSessionCustomUtil.isExists("goods-10001"); - -// 获取指定key的Session,如果没有,则新建并返回 -SaSessionCustomUtil.getSessionById("goods-10001"); - -// 获取指定key的Session,如果没有,第二个参数决定是否新建并返回 -SaSessionCustomUtil.getSessionById("goods-10001", false); - -// 删除指定key的Session -SaSessionCustomUtil.deleteSessionById("goods-10001"); -``` - - -### Session相关操作 -那么获取到的`SaSession`具体有哪些方法可供操作? -``` java -// 返回此Session的id -session.getId(); - -// 返回此Session的创建时间 (时间戳) -session.getCreateTime(); - -// 在Session上获取一个值 -session.getAttribute('name'); - -// 在Session上获取一个值,并指定取不到值时返回的默认值 -session.getAttribute('name', 'zhang'); - -// 在Session上写入一个值 -session.setAttribute('name', 'zhang'); - -// 在Session上移除一个值 -session.removeAttribute('name'); - -// 清空此Session的所有值 -session.clearAttribute(); - -// 获取此Session是否含有指定key (返回true或false) -session.containsAttribute('name'); - -// 获取此Session会话上所有key (返回Set) -session.attributeKeys(); - -// 返回此Session会话上的底层数据对象(如果更新map里的值,请调用session.update()方法避免产生脏数据) -session.getDataMap(); - -// 将这个Session从持久库更新一下 -session.update(); - -// 注销此Session会话 (从持久库删除此Session) -session.logout(); -``` - - -### 类型转换API -由于Session存取值默认的类型都是Object,因此我们通常会写很多不必要类型转换代码
-为了简化操作,Sa-Token自`v1.15.0`封装了存取值API的类型转换,你可以非常方便的调用以下方法: -``` java -// 写值 -session.set("name", "zhang"); - -// 写值 (只有在此key原本无值的时候才会写入) -session.setDefaultValue("name", "zhang"); - -// 取值 -session.get("name"); - -// 取值 (指定默认值) -session.get("name", ""); - -// 取值 (转String类型) -session.getString("name"); - -// 取值 (转int类型) -session.getInt("age"); - -// 取值 (转long类型) -session.getLong("age"); - -// 取值 (转double类型) -session.getDouble("result"); - -// 取值 (转float类型) -session.getFloat("result"); - -// 取值 (指定转换类型) -session.getModel("key", Student.class); - -// 取值 (指定转换类型, 并指定值为Null时返回的默认值) -session.getModel("key", Student.class, ); - -// 是否含有某个key -session.has("key"); -``` - - -### Session环境隔离说明 -有同学经常会把 `SaSession` 与 `HttpSession` 进行混淆,例如: -``` java -@PostMapping("/resetPoints") -public void reset(HttpSession session) { - // 在HttpSession上写入一个值 - session.setAttribute("name", 66); - // 在SaSession进行取值 - System.out.println(StpUtil.getSession().getAttribute("name")); // 输出null -} -``` -**要点:** -1. `SaSession` 与 `HttpSession` 没有任何关系,在`HttpSession`上写入的值,在`SaSession`中无法取出 -2. `HttpSession`并未被框架接管,在使用Sa-Token时,请在任何情况下均使用`SaSession`,不要使用`HttpSession` - diff --git a/sa-token-doc/fun/async--mock.md b/sa-token-doc/fun/async--mock.md new file mode 100644 index 000000000..cd2b1ee1b --- /dev/null +++ b/sa-token-doc/fun/async--mock.md @@ -0,0 +1,102 @@ +# 异步 & Mock 上下文 + +### 异步上下文 + +有一些方法(例如`StpUtil.isLogin()`)只可以在同步的 Web 上下文中才可以调用,如果在异步上下文中调用则会抛出异常: + +``` text +cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化 +``` + +这是因为这些方法需要从前端的 `HttpServletRequest` 中读取 Token 参数,而异步上下文通常不是一次“请求”,不具有 `HttpServletRequest` 的概念,所以无法成功调用。 + +一般哪些场景属于异步上下文? +- 通过 `new Thread(() -> { ... }).start()` 启动子线程。 +- 通过 `taskExecutor.execute(() -> { ... })` 线程池启动异步任务。 +- 通过 `@Async` 注解标注的方法。 +- 通过 `@Scheduled(cron = "")` 启动的定时任务。 +- 消息队列中消费消息的函数。 +- ... + +凡是不通过 web 请求调用触发的线程,在 Sa-Token 中均属于异步上下文,也可以称作 “非 Web 上下文”。 + +此时调用 `StpUtil.isLogin()`、`StpUtil.getLoginId()` 等需要 Web 上下文的 API,就会抛出上述异常。 + +如果你需要在 非 Web 上下文 中调用上述 API,则需要手动 mock 一个上下文,才可以调用成功: + +例如: +``` java +// 【异步】new Thread +@RequestMapping("isLogin2") +public SaResult isLogin2() { + System.out.println("是否登录:" + StpUtil.isLogin()); + String tokenValue = StpUtil.getTokenValue(); + new Thread(() -> { + SaTokenContextMockUtil.setMockContext(()->{ + StpUtil.setTokenValueToStorage(tokenValue); + System.out.println("是否登录:" + StpUtil.isLogin()); + }); + }).start(); + return SaResult.data(StpUtil.getTokenValue()); +} +``` + +参考上述方法,你需要先调用 `SaTokenContextMockUtil.setMockContext(() -> { ... })` Mock 出一个 Web 上下文填充到上下文管理器中, +然后在 Mock 上下文范围内调用 `StpUtil.setTokenValueToStorage(tokenValue)` 指定当前上下文的 token 值,其效果等同于在 web 上下文中前端提交了此 token 值。 + +更多使用姿势请参考仓库示例:[Async-TestController.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/test/TestController.java) + + +### 响应式上下文 + +在 WebFlux / Spring Cloud 等响应式环境下调用 Sa-Token 的同步 API 也有可能发生上下文异常: + +``` +cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化 + at cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff.getModelBox(SaTokenContextForThreadLocalStaff.java:73) ~[classes/:na] + Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: +Error has been observed at the following site(s): + *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaReactorFilter [DefaultWebFilterChain] + *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor [DefaultWebFilterChain] + *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor [DefaultWebFilterChain] + *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor [DefaultWebFilterChain] + *__checkpoint ⇢ HTTP GET "/test/isLogin" [ExceptionHandlingWebHandler] +``` + +如果是在自定义 Filter 中报的这个错,需要你在调用 Sa-Token 的同步 API 之前手动 set 一下上下文: +``` java +// 自定义过滤器 +@Component +public class MyFilter implements WebFilter { + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + try { + // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 + SaReactorSyncHolder.setContext(exchange); + System.out.println(StpUtil.isLogin()); + } + finally { + SaReactorSyncHolder.clearContext(); + } + return chain.filter(exchange); + } +} +``` + +参考:[MyFilter.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/MyFilter.java) + +在 Controller 里同理: + +``` java +@RequestMapping("isLogin2") +public SaResult isLogin2(ServerWebExchange exchange) { + SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{ + System.out.println("是否登录:" + StpUtil.isLogin()); + return SaResult.data(StpUtil.getTokenInfo()); + }); + return SaResult.data(res); +} +``` + +更多示例请参考: +[TestController.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/TestController.java) \ No newline at end of file diff --git a/sa-token-doc/fun/auth-flow.md b/sa-token-doc/fun/auth-flow.md new file mode 100644 index 000000000..de22eaa10 --- /dev/null +++ b/sa-token-doc/fun/auth-flow.md @@ -0,0 +1,15 @@ +# Sa-Token 功能结构图 + +--- + +### Sa-Token 功能结构图: + +sa-token-rz + +### Sa-Token 认证流程图: + +sa-token-rz + + + +PS:鼠标右键选择 **`[在新窗口打开图片]`** 即可高清模式查看图片。 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 000000000..fbf65d551 --- /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 getRoleList(Object loginId, String loginType) { + return Arrays.asList("admin", "super-admin", "ceo"); + } + + // 加载权限信息 + @Override + public List getPermissionList(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 + 1.39.0 + +``` + +`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-template + 1.39.0 + + + + + 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/cors-filter.md b/sa-token-doc/fun/cors-filter.md new file mode 100644 index 000000000..d6da28937 --- /dev/null +++ b/sa-token-doc/fun/cors-filter.md @@ -0,0 +1,9 @@ +# 解决跨域问题 + + + +参考1: [https://juejin.cn/post/7491603065944129590](https://juejin.cn/post/7491603065944129590) + +参考2: [https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g](https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g) + +参考3: [https://mp.weixin.qq.com/s/8aziIhqGCb_qsr8kLiqzmg](https://mp.weixin.qq.com/s/8aziIhqGCb_qsr8kLiqzmg) \ No newline at end of file diff --git a/sa-token-doc/fun/curr-domain.md b/sa-token-doc/fun/curr-domain.md new file mode 100644 index 000000000..534aa0e96 --- /dev/null +++ b/sa-token-doc/fun/curr-domain.md @@ -0,0 +1,83 @@ +# 解决反向代理 uri 丢失的问题 + +--- + +使用 `request.getRequestURL()` 可获取当前程序所在外网的访问地址,在 Sa-Token 中,其 `SaHolder.getRequest().getUrl()` 也正是借助此API完成, +有很多模块都用到了这个能力,比如SSO单点登录。 + +我们可以使用如下代码测试此API +``` java +// 显示当前程序所在外网的都访问地址 +@RequestMapping("test") +public String test() { + return "您访问的是:" + SaHolder.getRequest().getUrl(); +} +``` + +从浏览器访问此接口,我们可以看到: + +test-curr-domain.png + +此 API 在本地开发时一般可以正常工作,然而如果我们在部署时使用 Nginx 做了一层反向代理后,其最终结果可能会和我们预想的有一点偏差: + +test-curr-domain-fxdl.png + +不仅是 Nginx,所有包含路由转发的地方都有可能导致上述丢失 uri 的现象,解决方案也很简单,既然程序无法自动识别,我们改成手动获取即可,Sa-Token 提供两个方案: + + +### 方案一:Nginx转发时追加 header 参数 + +##### 1、首先在 Nginx 代理转发的地方增加参数 + +nginx-add-header.png + +重点是这一句:`proxy_set_header Public-Network-URL http://$http_host$request_uri;` + +##### 2、在程序中新增类 `CustomSaTokenContextForSpring.java`,重写获取uri的逻辑 + +``` java +@Primary +@Component +public class CustomSaTokenContextForSpring extends SaTokenContextForSpring { + + @Override + public SaRequest getRequest() { + return new SaRequestForServlet(SpringMVCUtil.getRequest()) { + @Override + public String getUrl() { + if(request.getHeader("Public-Network-URL") != null) { + return request.getHeader("Public-Network-URL"); + } + return request.getRequestURL().toString(); + } + }; + } + +} +``` + +其它逻辑保持不变,框架即可正确获取 uri 地址 + +> [!ATTENTION| label:风险警告] +> 注意:步骤一与步骤二需要同步存在,否则可能有前端假传 header 参数造成安全问题 + + +### 方案二:直接在yml中配置当前项目的网络访问地址 + +在 `application.yml` 中增加配置: + + + +``` yaml +sa-token: + # 配置当前项目的网络访问地址 + curr-domain: http://local.dev33.cn:8902/api +``` + +``` properties +# 配置当前项目的网络访问地址 +sa-token.curr-domain=http://local.dev33.cn:8902/api +``` + + +即可避免路由转发过程中丢失 uri 的问题 diff --git a/sa-token-doc/fun/custom-annotations.md b/sa-token-doc/fun/custom-annotations.md new file mode 100644 index 000000000..7b4c76c3f --- /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、第二步,创建注解处理器 + +实现 `SaAnnotationHandlerInterface` 接口,指定泛型为刚才自定义的注解 + +``` java +/** + * 注解 CheckAccount 的处理器 + */ +@Component +public class CheckAccountHandler implements SaAnnotationHandlerInterface { + + // 指定这个处理器要处理哪个注解 + @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 SaAnnotationHandlerInterface { + + @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/dynamic-router-check.md b/sa-token-doc/fun/dynamic-router-check.md new file mode 100644 index 000000000..2143b52e0 --- /dev/null +++ b/sa-token-doc/fun/dynamic-router-check.md @@ -0,0 +1,79 @@ +# 参考:把路由拦截鉴权动态化 + +框架提供的示例是硬代码写死的,不过稍微做一下更改,你就可以让他动态化 + +--- + +参考如下: + +``` java +@Override +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + SaRouter + .match("/**") + .notMatch(excludePaths()) + .check(r -> StpUtil.checkLogin()); + })).addPathPatterns("/**"); +} + +// 动态获取哪些 path 可以忽略鉴权 +public List excludePaths() { + // 此处仅为示例,实际项目你可以写任意代码来查询这些path + return Arrays.asList("/path1", "/path2", "/path3"); +} +``` + +如果不仅仅是登录校验,还需要鉴权,那也很简单: +``` java +@Override +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + // 遍历校验规则,依次鉴权 + Map rules = getAuthRules(); + for (String path : rules.keySet()) { + SaRouter.match(path, () -> StpUtil.checkPermission(rules.get(path))); + } + })).addPathPatterns("/**"); +} + +// 动态获取鉴权规则 +public Map getAuthRules() { + // key 代表要拦截的 path,value 代表需要校验的权限 + Map authMap = new LinkedHashMap<>(); + authMap.put("/user/**", "user"); + authMap.put("/admin/**", "admin"); + authMap.put("/article/**", "article"); + // 更多规则 ... + return authMap; +} +``` + + +--- + +错误的写法: + +``` java +@Override +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + StpUtil.checkLogin(); + })) + .addPathPatterns("/**") + .excludePathPatterns(excludePaths()); +} + +// 动态获取哪些 path 可以忽略鉴权 +public List excludePaths() { + // 此处仅为示例,实际项目你可以写任意代码来查询这些path + return Arrays.asList("/path1", "/path2", "/path3"); +} +``` + +错误点:因为 lambda 表达式之外的代码只会在启动时执行一次,所以 `excludePaths()` 方法是无法做到动态化读取的, +若要在项目运行时动态读写,必须把调用 `excludePaths()` 的时机放在 lambda 里。 + + + + diff --git a/sa-token-doc/fun/exception-code.md b/sa-token-doc/fun/exception-code.md new file mode 100644 index 000000000..b2df661e4 --- /dev/null +++ b/sa-token-doc/fun/exception-code.md @@ -0,0 +1,244 @@ +# 异常细分状态码 + +--- + +### 获取异常细分状态码 + +Sa-Token 中的基础异常类是 `SaTokenException`,在此基础上,又针对一些特定场景划分出诸如 `NotLoginException`、`NotPermissionException` 等。 + +但是框架中异常抛出点远远多于异常种类的划分,比如在 SSO 插件中,[ redirect 重定向地址无效 ] 和 [ ticket 参数值无效 ] 都会导致 SSO 授权的失败, +但是它们抛出的异常都是 `SaSsoException`,如果你需要对这两种异常情形做出不同的处理,仅仅判断异常的 ClassType 显然不够。 + +为了解决上述需求,Sa-Token 对每个异常抛出点都会指定一个特定的 code 值,就像这样: + +``` java +if(SaFoxUtil.isUrl(url) == false) { + throw new SaSsoException("无效redirect:" + url).setCode(SaSsoErrorCode.CODE_30001); +} +``` + +就像是打上一个特定的标记,不同异常情形标记的 code 码值也会不同,这就为你精细化异常处理提供了前提。 + +要在捕获异常时获取这个 code 码也非常简单: + +``` java +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(SaTokenException.class) + public SaResult handlerSaTokenException(SaTokenException e) { + + // 根据不同异常细分状态码返回不同的提示 + if(e.getCode() == 30001) { + return SaResult.error("redirect 重定向 url 是一个无效地址"); + } + if(e.getCode() == 30002) { + return SaResult.error("redirect 重定向 url 不在 allowUrl 允许的范围内"); + } + if(e.getCode() == 30004) { + return SaResult.error("提供的 ticket 是无效的"); + } + // 更多 code 码判断 ... + + // 默认的提示 + return SaResult.error("服务器繁忙,请稍后重试..."); + } +} +``` + +SaToken 中的所有异常都是继承于 `SaTokenException` 的,也就是说,所有异常你都可以通过 `e.getCode()` 的方式获取对应的异常细分状态码。 + + + + + +### 异常细分状态码-参照表 + +#### sa-token-code 核心包 + +| code码值 | 含义 | +| :-------- | :-------- | +| -1 | 代表这个异常在抛出时未指定异常细分状态码 | +| 10001 | 未能获取有效的上下文处理器 | +| 10002 | 未能获取有效的上下文 | +| 10003 | JSON 转换器未实现 | +| 10011 | 未能从全局 StpLogic 集合中找到对应 type 的 StpLogic | +| 10021 | 指定的配置文件加载失败 | +| 10022 | 配置文件属性无法正常读取 | +| 10031 | 重置的侦听器集合不可以为空 | +| 10032 | 注册的侦听器不可以为空 | +| 10301 | 提供的 Same-Token 是无效的 | +| 10311 | 表示未能通过 Http Basic 认证校验 | +| 10321 | 提供的 HttpMethod 是无效的 | +| 11001 | 未能读取到有效Token | +| 11002 | 登录时的账号id值为空 | +| 11003 | 更改 Token 指向的 账号Id 时,账号Id值为空 | +| 11011 | 未能读取到有效Token | +| 11012 | Token无效 | +| 11013 | Token已过期 | +| 11014 | Token已被顶下线 | +| 11015 | Token已被踢下线 | +| 11016 | Token已被冻结 | +| 11031 | 在未集成 sa-token-jwt 插件时调用 getExtra() 抛出异常 | +| 11041 | 缺少指定的角色 | +| 11051 | 缺少指定的权限 | +| 11061 | 当前账号未通过服务封禁校验 | +| 11062 | 提供要解禁的账号无效 | +| 11063 | 提供要解禁的服务无效 | +| 11064 | 提供要解禁的等级无效 | +| 11071 | 二级认证校验未通过 | +| 12001 | 请求中缺少指定的参数 | +| 12002 | 构建 Cookie 时缺少 name 参数 | +| 12003 | 构建 Cookie 时缺少 value 参数 | +| 12101 | Base64 编码异常 | +| 12102 | Base64 解码异常 | +| 12103 | URL 编码异常 | +| 12104 | URL 解码异常 | +| 12111 | md5 加密异常 | +| 12112 | sha1 加密异常 | +| 12113 | sha256 加密异常 | +| 12114 | AES 加密异常 | +| 12115 | AES 解密异常 | +| 12116 | RSA 公钥加密异常 | +| 12117 | RSA 私钥加密异常 | +| 12118 | RSA 公钥解密异常 | +| 12119 | RSA 私钥解密异常 | +| 12201 | 参与参数签名的秘钥不可为空 | +| 12202 | 给定的签名无效 | +| 12203 | timestamp 超出允许的范围 | + + +#### sa-token-servlet + +| code码值 | 含义 | +| :-------- | :-------- | +| 20001 | 转发失败 | +| 20002 | 重定向失败 | + + +#### sa-token-spring-boot-starter + +| code码值 | 含义 | +| :-------- | :-------- | +| 20101 | 企图在非 Web 上下文获取 Request、Response 等对象 | +| 20103 | 对象转 JSON 字符串失败 | +| 20104 | JSON 字符串转 Map 失败 | +| 20105 | 默认的 Filter 异常处理函数 | + + +#### sa-token-reactor-spring-boot-starter + +| code码值 | 含义 | +| :-------- | :-------- | +| 20203 | 对象转 JSON 字符串失败 | +| 20204 | JSON 字符串转 Map 失败 | +| 20205 | 默认的 Filter 异常处理函数 | + + +#### sa-token-solon-plugin + +| code码值 | 含义 | +| :-------- | :-------- | +| 20301 | 默认的拦截器异常处理函数 | +| 20302 | 默认的 Filter 异常处理函数 | + + +#### sa-token-sso 单点登录相关: + +| code码值 | 含义 | +| :-------- | :-------- | +| 30001 | `redirect` 重定向 url 是一个无效地址 | +| 30002 | `redirect` 重定向 url 不在 allowUrl 允许的范围内 | +| 30003 | 接口调用方提供的 `secretkey` 秘钥无效 | +| 30004 | 提供的 `ticket` 是无效的 | +| 30005 | 在模式三下,sso-client 调用 sso-server 端 校验ticket接口 时,得到的响应是校验失败 | +| 30006 | 在模式三下,sso-client 调用 sso-server 端 单点注销接口 时,得到的响应是注销失败 | +| 30007 | http 请求调用 提供的 `timestamp` 与当前时间的差距超出允许的范围 | +| 30008 | http 请求调用 提供的 `sign` 无效 | +| 30009 | 本地系统没有配置 `secretkey` 字段 | +| 30010 | 本地系统没有配置 http 请求处理器 | +| 30011 | 该 ticket 不属于当前 client | + + +#### sa-token-oauth2 相关: +| 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 | +| 30120 | redirect_uri 不一致 | +| 30122 | client_id 不一致 | +| 30125 | 无效 response_type | +| 30126 | 无效 grant_type | +| 30127 | 无效 state | +| 30141 | 系统暂未开放的授权模式 | +| 30142 | 应用暂未开放的授权模式 | +| 30151 | 无效的请求 Method | +| 30191 | 其它异常 | + + +#### sa-token-jwt 插件相关: + +| code码值 | 含义 | +| :-------- | :-------- | +| 30201 | 对 jwt 字符串解析失败 | +| 30202 | 此 jwt 的签名无效 | +| 30203 | 此 jwt 的 `loginType` 字段不符合预期 | +| 30204 | 此 jwt 已超时 | +| 30205 | 没有配置jwt秘钥 | +| 30206 | 登录时提供的账号id为空 | + + +#### sa-token-temp-jwt 插件相关: +| code码值 | 含义 | +| :-------- | :-------- | +| 30301 | jwt 模式没有提供秘钥 | +| 30302 | jwt 模式不可以删除 Token | +| 30303 | Token已超时 | + + +> [!WARNING| label:注意] +> 部分插件因异常抛出点较少,暂未做状态码细分处理 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/fun/firewall.md b/sa-token-doc/fun/firewall.md new file mode 100644 index 000000000..715116015 --- /dev/null +++ b/sa-token-doc/fun/firewall.md @@ -0,0 +1,117 @@ +# 防火墙 + +Sa-Token 内置防火墙组件 `SaFirewallStrategy`,用于拦截一些可能造成攻击的危险请求。 +例如当前端提交的 path 为 `/test//login` 时,框架将会强制截断请求,响应以下内容: + +``` txt +非法请求:/test//login +``` + +因为包含双斜杠的 path 请求通常被用于鉴权绕行攻击。类似的拦截规则还有很多, `SaFirewallStrategy` 采用 hooks 机制,允许开发者自由扩展拦截规则, +框架默认具有以下 hook 拦截规则: + +- `SaFirewallCheckHookForWhitePath`:请求 path 白名单放行。 +- `SaFirewallCheckHookForBlackPath`:请求 path 黑名单校验。 +- `SaFirewallCheckHookForPathDangerCharacter`:请求 path 危险字符校。 +- `SaFirewallCheckHookForPathBannedCharacter`:请求 path 禁止字符校验。 +- `SaFirewallCheckHookForDirectoryTraversal`:请求 path 目录遍历符检测。 +- `SaFirewallCheckHookForHost`:Host 检测。 +- `SaFirewallCheckHookForHttpMethod`:请求 Method 检测。 +- `SaFirewallCheckHookForHeader`:请求头检测。 +- `SaFirewallCheckHookForParameter`:请求参数检测。 + + +### 1、默认 hook 配置: + +假设我们想要增加请求 path 黑名单,可以使用如下代码: + +``` java +@Configuration +public class SaTokenConfigure { + @PostConstruct + public void saTokenPostConstruct() { + SaFirewallCheckHookForBlackPath.instance.resetConfig("/abc"); + } +} +``` + +现在从浏览器访问 `/abc`,将会被防火墙组件直接拦截: + +``` txt +非法请求:/abc +``` + + +除了 `SaFirewallCheckHookForBlackPath` 以外,其它所有 hook 均可通过此方式重载配置,在此暂不冗余演示。 + + +### 2、注册新的 hook 规则: +你可以使用如下代码注册新的 hook 规则: + +``` java +@PostConstruct +public void saTokenPostConstruct() { + // 注册新 hook 演示,拦截所有带有 pwd 参数的请求,拒绝响应 + SaFirewallStrategy.instance.registerHook((req, res, extArg)->{ + if(req.getParam("pwd") != null) { + throw new FirewallCheckException("请求中不可包含 pwd 参数"); + } + }); +} +``` + +除了注册新 hook 规则,你还可以移除默认 hook ,来删减你认为不必要存在的校验规则: + +``` java +// 移除指定类型的 hook 验证 +SaFirewallStrategy.instance.removeHook(SaFirewallCheckHookForHost.class); +``` + + +### 3、利用自动注入特性注册 hook +如果你的项目属于 IOC 环境(例如 SpringBoot 项目),还可以这样注册 hook: +``` java +// 自定义防火墙校验 hook +@Component +public class SaFirewallCheckHookForXxx implements SaFirewallCheckHook { + @Override + public void execute(SaRequest req, SaResponse res, Object extArg) { + System.out.println("----------- 自定义防火墙校验 hook "); + } +} +``` + + +### 4、指定异常处理: + +被防火墙拦截的请求不会做出格式化响应,因为通常这些请求为非正常业务请求,只需阻断即可,无需前端依照响应做出页面提示。 + +如果你的业务切实需要对防火墙拦截做出格式化响应,可以通过以下代码完成: + +``` java +@PostConstruct +public void saTokenPostConstruct() { + // 指定防火墙校验不通过时的处理方案 + SaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> { + System.out.println("防火墙校验不通过:" + e.getMessage()); + try { + HttpServletResponse response = (HttpServletResponse)res.getSource(); + response.setContentType("application/json;charset=UTF-8"); + String resJson = SaResult.error(e.getMessage()).toString(); + response.getWriter().print(resJson); + response.getWriter().flush(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }; +} +``` + +浏览器将得到以下 json 格式响应: + +``` js +{ + "code": 500, + "msg": "非法请求:/abc", + "data": null +} diff --git a/sa-token-doc/fun/git-pr.md b/sa-token-doc/fun/git-pr.md new file mode 100644 index 000000000..0fbc84412 --- /dev/null +++ b/sa-token-doc/fun/git-pr.md @@ -0,0 +1,103 @@ +# 如何更新在线文档 +1. 打开要修改的文档页面 +2. 滑动右侧页面滑块, 查看页面内容最下方, 评论区上方 +3. 找到这一行文字 + +在线编辑提示 + +4. 点击Gitee或GitHub按钮中的任意一个, 国内用户推荐使用 [Gitee](https://gitee.com) (请先注册登录后再往下浏览) +5. 此时会进入当前页面源码预览页面,找到下方按钮组 + +按钮组 + +6. 点击编辑按钮 +7. 此时进入待修改页面的源码页面, 按照markdown格式编辑为需要的结果(Ctrl+P可查看最终效果,再次按下可恢复源码界面) +8. 滑动到最下方点击提交审核即可 + + +# 如何提交代码 +## 环境安装过程 +1. 在本地[下载Git软件](https://pc.qq.com/detail/13/detail_22693.html)并安装 +2. 配置用户名和邮件地址(Gitee或GitHub上关联的邮箱) + +``` +git config --global user.name "这里替换为你在项目中希望展示的昵称" +git config --global user.email 这里替换为你的关联邮箱 +// 查看是否配置正确 +git config --list +``` + +3. 为了让Gitee服务器认可你的身份,需要配置一次SSH Key, 在本地生成密匙对, 公钥上传到Gitee服务器后台 +4. 具体方法见[Gitee如何配置SSH](https://gitee.com/help/articles/4181#article-header0), [Github如何配置SSH](https://docs.github.com/cn/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account) +5. 最小开发环境安装包括[Java JDK 8+](https://pc.qq.com/detail/0/detail_18360.html),[Maven 最新版](http://maven.apache.org/download.cgi) 和[idea IDE 社区版](https://www.jetbrains.com/zh-cn/idea/download/#section=windows) +6. 在idea 中[配置Java环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEjava%E7%8E%AF%E5%A2%83)和[配置maven环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEmaven%E7%8E%AF%E5%A2%83), 基础部分不再赘述 + +## 项目下载过程 +1. 点击[Gitee](https://gitee.com/dromara/sa-token)或[Github](https://github.com/dromara/sa-token)进入Sa-Token项目主页, 以下以Gitee为例,Github类似(请先注册登录后再往下浏览) +2. 找到页面右上角的按钮组, 点击Forked按钮 + +按钮组 + +3. 选择个人仓库并点击确认 +4. 此时在你的个人仓库中会多了一个Sa-Token项目 +5. 在新的Sa-Token项目中, 点击 克隆/下载 按钮, 点击弹出框里面的复制按钮 +6. 在本地某空文件夹下右键选择: git bash here + +git bash + +git bash 打开后的图 + +14. 在里面输入如下命令, 按换行后自动下载整个项目 + +``` +git clone 这里替换为复制后的链接 +``` + +## 项目载入过程 +1. 下载结束后, 开启 idea, 选择 File->Open... 选中项目下载后的Sa-Token文件夹(Trust Project 相信此项目, 否则不可编辑) +2. 这时项目就是可编辑状态, 修改完代码并测试完成后即可提交 + +## 项目暂存并提交远程 +### 方式一 +1. 在idea中打开项目进入Commit选项 + +本地暂存 + +2. 勾选需要本地暂存的文件 +3. 在同一页面的下方输入提示信息 + +提示信息 + +4. 点击Commit按钮暂存到本地, 点击Commit and Push按钮暂存之后提交到远程 +### 方式二 +1. 除了点击Commit and Push按钮外,还有一个地方可以提交git + +git按钮 + +2. 位置在idea右上方的工具栏里面 +3. 指向左下箭头为拉取项目,可以随时更新 +4. 打对号为本地暂存 +5. 指向右上箭头提交远程 +## 私人项目推送到主项目 +1. 提交后进入Gitee个人仓库中克隆的Sa-Token项目 +2. 找到下图的Pull Request按钮 + +工具栏 + +3. 点击提交, 进入如下页面 + +提交信息填写页面 + +4. 在这里,你可以选择要提交的分支,一般都是dev开发分支.可以填写合并信息,其他测试审查之类的可以不填写, 最后点击创建即可完成一次提交. + +## 远程项目更新 +1. 有时候主项目更新了,之前克隆的项目代码陈旧,如何处理? +2. 在个人仓库的Sa-Token项目主页面中, 找到下图的圆圈 + +更新按钮 + +3. 点击右侧圆圈按钮后Gitee会自动同步主项目, 这样就不用像我之前一样,删除项目又重新fork了. + +## 为什么在国内推荐Gitee +1. 近期Github下载网速较慢 +2. Gitee上中文界面方便操作 \ No newline at end of file diff --git a/sa-token-doc/fun/issue-template.md b/sa-token-doc/fun/issue-template.md new file mode 100644 index 000000000..f703028b5 --- /dev/null +++ b/sa-token-doc/fun/issue-template.md @@ -0,0 +1,93 @@ +# issue 提问模板 + +在线提问链接:[Gitee issue](https://gitee.com/dromara/sa-token/issues)、[GitHub issue](https://github.com/dromara/sa-token/issues) + +> [!TIP| label:请在新建 issue 时,尽量复制模板格式进行提交] +> 1. 提交之前率先参考 Sa-Token 常见问题解答 以及善用 Gitee issues 搜索功能,查阅问题是否已有答案,已存在的 issue 就不要再重复提交了。 +> 2. 问题已得到处理的 issue 请大家及时手动关闭,如果超过24小时没有追问,我们将默认提交者已找到解决方案,关闭issue。 +> 3. 有时候 issue 提交之后,没有得到及时回复,大家可以加入QQ群@管理员寻求帮助。 +> 4. 请大家新建 issue 时删除不必要的模板信息、精简语句、**做好代码排版**,对于不方便描述的业务场景,可参阅 Sa-Token 名词解释 方便组织语句,这样有助于减低大家的沟通成本。 +> 5. **代码截图要带上行号!报错信息要把异常堆栈截全!页面截图要把地址栏带上!Ajax请求要把请求地址、请求头、请求参数都截全!** + + +--- + + +### 预期不符: +``` js +### 使用版本: + + +### 涉及的功能模块: + + +### 测试步骤: ++ 我经过以下步骤测试: + ++ 得出以下结果: + ++ 其中第 xx 行的代码输出表现 和文档上描述的不一致: + ++ 我的理解是: + +请问,是我的理解不对,还是文档出了问题? +``` + + +### bug反馈: +``` js +### 使用版本: + + +### 报错信息: + + +### 希望结果: + + +### 复现步骤: + + +< 备注:如果复现步骤比较复杂,请将 demo 上传到 gitee 并留下地址 > +``` + + +### 功能提问: +``` js +### 对以下问题有疑问: + + +< 备注:请尽量详细描述问题所在 > +``` + + +### 建议增加新功能: +``` js +### 建议增加的新功能: + + +### 应用场景阐述: + + +< 备注:请尽量详细描述功能应用场景 > +``` + + +### 踩坑记录: +``` js +### 遇到的问题: + + +### 解决方案: + + +< 备注:请尽量描述详细一点,为后人提供清晰的排查思路,人人为我,我为人人 > +``` + + + + + + + + diff --git a/sa-token-doc/fun/jur-cache.md b/sa-token-doc/fun/jur-cache.md new file mode 100644 index 000000000..6f12ad232 --- /dev/null +++ b/sa-token-doc/fun/jur-cache.md @@ -0,0 +1,77 @@ +# 参考:将权限数据放在缓存里 +前面我们讲解了如何通过`StpInterface`接口注入权限数据,框架默认是不提供缓存能力的,如果你想减小数据库的访问压力,则需要将权限数据放到缓存中 + +--- + +参考如下: +``` java +/** + * 自定义权限验证接口扩展 + */ +@Component +public class StpInterfaceImpl implements StpInterface { + + // 返回一个账号所拥有的权限码集合 + @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; + } + +} +``` + +##### 疑问:为什么不直接缓存 `[账号id->权限列表]`的关系,而是 `[账号id -> 角色id -> 权限列表]`? + + + +答:`[账号id->权限列表]`的缓存方式虽然更加直接粗暴,却有一个严重的问题: + +- 通常我们系统的权限架构是RBAC模型:权限与用户没有直接的关系,而是:用户拥有指定的角色,角色再拥有指定的权限 +- 而这种'拥有关系'是动态的,是可以随时修改的,一旦我们修改了它们的对应关系,便要同步修改或清除对应的缓存数据 + +现在假设如下业务场景:我们系统中有十万个账号属于同一个角色,当我们变动这个角色的权限时,难道我们要同时清除这十万个账号的缓存信息吗? +这显然是一个不合理的操作,同一时间缓存大量清除容易引起Redis的缓存雪崩 + +而当我们采用 `[账号id -> 角色id -> 权限列表]` 的缓存模型时,则只需要清除或修改 `[角色id -> 权限列表]` 一条缓存即可 + +一言以蔽之:权限的缓存模型需要跟着权限模型走,角色缓存亦然 + + diff --git a/sa-token-doc/fun/log.md b/sa-token-doc/fun/log.md new file mode 100644 index 000000000..79bfdb535 --- /dev/null +++ b/sa-token-doc/fun/log.md @@ -0,0 +1,89 @@ +# 参考:全局 Log 输出 + +--- + +### 打开全局日志输出 + +以下配置可以打开全局日志输出: + + + + +``` yaml +sa-token: + # 是否输出操作日志 + is-log: true +``` + + +``` properties +# 是否输出操作日志 +sa-token.is-log=true +``` + + + +此配置项打开之后,框架将会在账号登录、注销、二级认证 等关键性步骤打印日志,以方便项目开发调试。 + +框架默认将日志信息打印到控制台,如果需要将日志输出到其它地方,你可以重写 SaLog 对象,例如以下代码将会把日志转接到 Slf4j 下: + +``` java +/** + * 将 Sa-Token log 信息转接到 Slf4j + */ +@Component +public class SaLogForSlf4j implements SaLog { + Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); + + @Override + public void trace(String str, Object... args) { + log.trace(str, args); + } + @Override + public void debug(String str, Object... args) { + log.debug(str, args); + } + @Override + public void info(String str, Object... args) { + log.info(str, args); + } + @Override + public void warn(String str, Object... args) { + log.warn(str, args); + } + @Override + public void error(String str, Object... args) { + log.error(str, args); + } + @Override + public void fatal(String str, Object... args) { + log.error(str, args); + } +} +``` + +重新启动项目,观察日志打印变化。 + +### 增加API访问日志 + +手动增加 API 请求日志信息,这将非常有助于你调试代码,例如: + +``` java +@Bean +public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + .addInclude("/**") + .addExclude("/favicon.ico") + .setAuth(obj -> { + // 输出 API 请求日志,方便调试代码 + SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); + // 其它校验代码... + }) + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + return SaResult.error(e.getMessage()); + }) + ; +} +``` \ No newline at end of file diff --git a/sa-token-doc/fun/not-login-scene.md b/sa-token-doc/fun/not-login-scene.md new file mode 100644 index 000000000..0ea39655c --- /dev/null +++ b/sa-token-doc/fun/not-login-scene.md @@ -0,0 +1,68 @@ +# NotLoginException 场景值 + +本篇介绍如何根据`NotLoginException`异常的场景值,来定制化处理未登录的逻辑
+应用场景举例:未登录、被顶下线、被踢下线等场景需要不同方式来处理 + + +## 何为场景值 +在前面的章节中,我们了解到,在会话未登录的情况下尝试获取`loginId`会使框架抛出`NotLoginException`异常,而同为未登录异常却有五种抛出场景的区分 + +| 场景值 | 对应常量 | 含义说明 | +|--- |--- |--- | +| -1 | NotLoginException.NOT_TOKEN | 未能从请求中读取到有效 token | +| -2 | NotLoginException.INVALID_TOKEN | 已读取到 token,但是 token 无效 | +| -3 | NotLoginException.TOKEN_TIMEOUT | 已读取到 token,但是 token 已经过期 ([详](/fun/token-timeout)) | +| -4 | NotLoginException.BE_REPLACED | 已读取到 token,但是 token 已被顶下线 | +| -5 | NotLoginException.KICK_OUT | 已读取到 token,但是 token 已被踢下线 | +| -6 | NotLoginException.TOKEN_FREEZE | 已读取到 token,但是 token 已被冻结 | +| -7 | NotLoginException.NO_PREFIX | 未按照指定前缀提交 token | + + + +那么,如何获取场景值呢?废话少说直接上代码: + + +``` java +// 全局异常拦截(拦截项目中的NotLoginException异常) +@ExceptionHandler(NotLoginException.class) +public SaResult handlerNotLoginException(NotLoginException nle) + throws Exception { + + // 打印堆栈,以供调试 + nle.printStackTrace(); + + // 判断场景值,定制化异常信息 + String message = ""; + if(nle.getType().equals(NotLoginException.NOT_TOKEN)) { + message = "未能读取到有效 token"; + } + else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) { + message = "token 无效"; + } + else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) { + message = "token 已过期"; + } + else if(nle.getType().equals(NotLoginException.BE_REPLACED)) { + message = "token 已被顶下线"; + } + else if(nle.getType().equals(NotLoginException.KICK_OUT)) { + message = "token 已被踢下线"; + } + else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) { + message = "token 已被冻结"; + } + else if(nle.getType().equals(NotLoginException.NO_PREFIX)) { + message = "未按照指定前缀提交 token"; + } + else { + message = "当前会话未登录"; + } + + // 返回给前端 + return SaResult.error(message); +} +``` + +
+注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理 + diff --git a/sa-token-doc/fun/plugin-dev.md b/sa-token-doc/fun/plugin-dev.md new file mode 100644 index 000000000..d853b9740 --- /dev/null +++ b/sa-token-doc/fun/plugin-dev.md @@ -0,0 +1,249 @@ +# Sa-Token 插件开发指南 + + + +插件,从字面意思理解就是可拔插的组件,作用是在不改变 Sa-Token 现有架构的情况下,替换或扩展一部分底层代码逻辑。 + +--- + + +## 1、插件开发 + +为 Sa-Token 开发插件非常简单,以下是几种可行的方式: + +- 1、自定义全局策略。 +- 2、更改全局组件实现。 +- 3、实现自定义SaTokenContext。 +- 4、其它自由扩展。 + +下面依次介绍这几种方式。 + +### 方式1:自定义全局策略 + +Sa-Token 将框架的一些关键逻辑抽象出一个统一的概念 —— 策略,并统一定义在 `SaStrategy` 中,源码参考:[SaStrategy](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java) 。 + +SaStrategy 的每一个函数都可以单独重写,以 “自定义Token生成策略” 这一需求为例: + +``` java +// 重写 Token 生成策略 +SaStrategy.instance.createToken = (loginId, loginType) -> { + return SaFoxUtil.getRandomString(60); // 随机60位长度字符串 +}; +``` + +就像变量的重新赋值一样,你只需重新指定一个新的策略函数,即可自定义 Token 生成的逻辑。 + + +### 方式2:更改全局组件实现 + +你可以找到不符合你需求的组件,重新定义一个子类,以 临时令牌认证 模块为例,你需要自定义 `SaTempTemplate` 的子类: + +``` java +/** + * 临时认证模块 自定义子类实现 + */ +@Component +public class MySaTempTemplate extends SaTempTemplate { + + @Override + public String createToken(Object value, long timeout, boolean isRecordIndex) { + System.out.println("------- 自定义一些逻辑 createToken "); + return super.createToken(value, timeout, isRecordIndex); + } + + @Override + public Object parseToken(String token) { + System.out.println("------- 自定义一些逻辑 parseToken "); + return super.parseToken(token); + } + +} +``` + +### 方式3:实现自定义SaTokenContext +SaTokenContext 是对接不同框架的上下文接口,篇幅限制,可参考:[自定义 SaTokenContext 指南](/fun/sa-token-context) + + +### 方式4:其它自由扩展 +这种方式就无需注入什么全局组件替换内部实现了,你可以在 Sa-Token 的基础之上封装任何代码,进行功能扩展。 + + + +## 2、插件注册 + +在你完成插件开发之后,你还需要考虑一个问题,如何让插件代码注入到项目中。 + +首先这里需要分两种情况: +- 情况1:只打算自己的项目使用这个插件。 +- 情况2:准备提交 pr 到 Sa-Token 仓库,让更多人使用。 + + +### 情况1:只打算自己的项目使用这个插件 + +这种情况比较简单,如果是 SpringBoot 项目,你可以在自定义插件类上添加注解 `@Component`: + +``` java +@Component +public class MySaTempTemplate extends SaTempTemplate { + // ... +} +``` + +这样在项目启动时, sa-token-spring-boot-starter 集成包将会扫描到这个自定义组件,注入到框架中。 + +如果是重写全局策略的代码,也可以通过 `@PostConstruct` 注解做到项目启动时自动执行: + +``` java +@PostConstruct +public void rewriteSaStrategy() { + // 重写 token 生成策略 + SaStrategy.instance.createToken = (loginId, loginType) -> { + return SaFoxUtil.getRandomString(60); + }; +} +``` + +如果是非 SpringBoot 项目,项目环境无法做到自动注入,保底的方案是在 main 方法中,手动注册组件: + +``` java +public static void main(String[] args) { + // 示例:手动替换 Sa-Token 内部组件 + // Sa-Token 大部分全局组件都定义在 SaManager 之上,参考:https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java + SaManager.setSaTempTemplate(new MySaTempTemplate()); + + // 示例:手动重写 Sa-Token 全局策略 + SaStrategy.instance.createToken = (loginId, loginType) -> { + return SaFoxUtil.getRandomString(60); + }; +} +``` + + +### 情况2:准备提交 pr 到 Sa-Token 仓库,让更多人使用。 + +这种情况稍微复杂一些,因为你基本上很难:通过在插件内部写一些代码,帮助“插件使用者”注册插件到项目中。 + +一种解决方案是:难办,那就别办了。 + +对,就是你只负责开发相对应的自定义组件,而将自定义组件的注册过程完全交给使用者,这并不是妥协的选择,反而会给插件使用者更大的自由度, +sa-token-jwt、sa-token-thymeleaf 等官方插件都是这样做的。 + +如果你觉得还是完成插件的自动注入比较好,也是有办法的,那就是利用 SPI 机制来注册组件。 + +(关于 java SPI 机制,网上教程众多,此处暂不详细介绍,不熟悉的同学可以直接向 deepseek 等 AI 工具提问,给你讲的明明白白的) + +你需要考虑一点:这个插件是专门给 SpringBoot 项目使用的,还是面向 Solon、JFinal 等任意项目使用: + +#### 如果是:SpringBoot 专用插件 + +如果这个插件只打算给 SpringBoot 项目使用,可以利用 SpringBoot 的 SPI 机制注册插件 + +SpringBoot2 格式:创建 `resources\META-INF\spring.factories` 文件: + +``` txt +org.springframework.boot.autoconfigure.EnableAutoConfiguration=插件完全限定名 +``` + +SpringBoot3 格式:创建 `resources\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports` 文件: + +``` txt +插件完全限定名 +``` + +这样在别人引入此插件时,便会根据 SPI 文件指定的地址去加载插件类,做到插件引入即注册的效果。 + + +#### 如果是:通用型插件 + +通用型插件则不能使用 SpringBoot 的 SPI 机制去注册组件,因为其它项目是无法识别 SpringBoot SPI 文件的, +好在 Sa-Token 提供了自己的 SPI 机制,所有环境均可使用: + +1、新建 `SaTokenPluginForXxx` 类,此类需要 `implements SaTokenPlugin` 接口,并且推荐定义在 `cn.dev33.satoken.plugin` 下: + +``` java +/** + * SaToken 插件安装:插件作用描述 + */ +public class SaTokenPluginForXxx implements SaTokenPlugin { + @Override + public void install() { + // 书写需要在项目启动时执行的代码,例如: + // SaManager.setXxx(new SaXxxForXxx()); + } +} +``` + +2、新建 `resources\META-INF\satoken\cn.dev33.satoken.plugin.SaTokenPlugin` 文件,填写上插件类的完全限定名地址 +``` txt +cn.dev33.satoken.plugin.SaTokenPluginForXxx +``` + +这样便可以在项目启动时,被 Sa-Token 插件管理器加载到此插件,执行自定义 `SaTokenPluginForXxx` 实现类的 `install` 方法,完成插件安装。 + + +## 3、练练手 + +学废了吗?给你出个题练练手: + +开发一个 `sa-token-hutool-json` 插件,要求引入该插件后,自动替换掉 Sa-Token 的 json 序列化方案为 hutool-json 模块。 + +如果没有思路,可以参考一下 `sa-token-fastjson` 的插件源码实现哦。 + + + + + + + + + + + + + + diff --git a/sa-token-doc/fun/refer-info.md b/sa-token-doc/fun/refer-info.md new file mode 100644 index 000000000..54e108a0d --- /dev/null +++ b/sa-token-doc/fun/refer-info.md @@ -0,0 +1,12 @@ +# 参考资料 +记录 Sa-Token 框架开发中参考过的一些资料。(由 2025-3-1 开始整理) + + +- Sa-Token对url过滤不全存在的风险点 - https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA +- SpringBoot 热拔插 AOP 组件: + - https://www.jb51.net/program/297714rev.htm + - https://www.bilibili.com/video/BV1WZ421W7Qx + - https://blog.csdn.net/Tomwildboar/article/details/139199801 +- 单元测试 - https://www.cnblogs.com/flypig666/p/11505277.html + + diff --git a/sa-token-doc/fun/sa-token-context--backup.md b/sa-token-doc/fun/sa-token-context--backup.md new file mode 100644 index 000000000..952238cd8 --- /dev/null +++ b/sa-token-doc/fun/sa-token-context--backup.md @@ -0,0 +1,180 @@ +# 自定义 SaTokenContext 指南 + +目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包, +如果我们使用的 Web 框架不在上述列表之中,则需要自定义 SaTokenContext 接口的实现完成整合工作。 + +--- + +### 1、SaTokenContext是什么,为什么要实现 SaTokenContext 接口? + +在鉴权中,必不可少的步骤就是从 `HttpServletRequest` 中读取 Token,然而并不是所有框架都具有 HttpServletRequest 对象,例如在 WebFlux 中,只有 `ServerHttpRequest`, +在一些其它Web框架中,可能连 `Request` 的概念都没有。 + +那么,Sa-Token 如何只用一套代码就对接到所有 Web 框架呢? + +解决这个问题的关键就在于 `SaTokenContext` 接口,此接口的作用是屏蔽掉不同 Web 框架之间的差异,提供统一的调用API: + +sa-token-context + + +SaTokenContext只是一个接口,没有工作能力,这也就意味着 SaTokenContext 接口的实现是必须的。 +那么疑问来了,我们之前在 SpringBoot 中引用 Sa-Token 时为什么可以直接使用呢? + +其实原理很简单,`sa-token-spring-boot-starter`集成包中已经内置了`SaTokenContext`的实现:[SaTokenContextForSpring](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java), +并且根据 Spring 的自动注入特性,在项目启动时注入到 Sa-Token 中,做到“开箱即用”。 + +那么如果我们使用不是 Spring 框架,是不是就必须得手动实现 `SaTokenContext` 接口?答案是肯定的,脱离Spring 环境后,我们就不能再使用`sa-token-spring-boot-starter`集成包了, +此时我们只能引入 `sa-token-core` 核心包,然后手动实现 `SaTokenContext` 接口。 + +不过不用怕,这个工作很简单,只要跟着下面的文档一步步来,你就可以将 Sa-Token 对接到任意Web框架中。 + + +### 2、实现 Model 接口 +我们先来观察一下 `SaTokenContext` 接口的签名: +``` java +/** + * Sa-Token 上下文处理器 + */ +public interface SaTokenContext { + + /** + * 获取当前请求的 [Request] 对象 + */ + public SaRequest getRequest(); + + /** + * 获取当前请求的 [Response] 对象 + */ + public SaResponse getResponse(); + + /** + * 获取当前请求的 [存储器] 对象 + */ + public SaStorage getStorage(); + + /** + * 校验指定路由匹配符是否可以匹配成功指定路径 + */ + public boolean matchPath(String pattern, String path); + +} +``` + +你可能对 `SaRequest` 比较疑惑,这个对象是干什么用的?正如每个 Web 框架都有 Request 概念的抽象,Sa-Token 也封装了 `Request`、`Response`、`Storage`三者的抽象: + +- `Request`:请求对象,携带着一次请求的所有参数数据。参考:[SaRequest.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java)。 +- `Response`:响应对象,携带着对客户端一次响应的所有数据。参考:[SaResponse.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java)。 +- `Storage`:请求上下文对象,提供 [一次请求范围内] 的上下文数据读写。参考:[SaStorage.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java)。 + + +因此,在实现 `SaTokenContext` 之前,你必须先实现这三个 Model 接口。 + +先别着急动手,如果你的 Web 框架是基于 Servlet 规范开发的,那么 Sa-Token 已经为你封装好了三个 Model 接口的实现,你要做的就是引入 `sa-token-servlet`包即可: + + + +``` xml + + + cn.dev33 + sa-token-servlet + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 权限认证(ServletAPI 集成包) +implementation 'cn.dev33:sa-token-servlet:${sa.top.version}' +``` + + + +如果你的 Web 框架不是基于 Servlet 规范,那么你就需要手动实现这三个 Model 接口,我们可以参考 `sa-token-servlet` 是怎样实现的: +[SaRequestForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java)、 +[SaResponseForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java)、 +[SaStorageForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java)。 + + +### 3、实现 SaTokenContext 接口 + +接下来我们奔入主题,提供 `SaTokenContext` 接口的实现,同样我们可以参考 Spring 集成包是怎样实现的: + +``` java +/** + * Sa-Token 上下文处理器 [ SpringMVC版本实现 ] + */ +public class SaTokenContextForSpring implements SaTokenContext { + + /** + * 获取当前请求的Request对象 + */ + @Override + public SaRequest getRequest() { + return new SaRequestForServlet(SpringMVCUtil.getRequest()); + } + + /** + * 获取当前请求的Response对象 + */ + @Override + public SaResponse getResponse() { + return new SaResponseForServlet(SpringMVCUtil.getResponse()); + } + + /** + * 获取当前请求的 [存储器] 对象 + */ + @Override + public SaStorage getStorage() { + return new SaStorageForServlet(SpringMVCUtil.getRequest()); + } + + /** + * 校验指定路由匹配符是否可以匹配成功指定路径 + */ + @Override + public boolean matchPath(String pattern, String path) { + return SaPathMatcherHolder.getPathMatcher().match(pattern, path); + } + +} +``` + +详细参考: +[SaTokenContextForSpring.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java) + + +### 4、将自定义实现注入到 Sa-Token 框架中 + +有了 `SaTokenContext` 接口的实现,我们还需要将这个实现类注入到 Sa-Token 之中,伪代码参考如下: +``` java +/** + * 程序启动类 + */ +public class Application { + + public static void main(String[] args) { + // 框架启动 + XxxApplication.run(xxx); + + // 将自定义的 SaTokenContext 实现类注入到框架中 + SaTokenContext saTokenContext = new SaTokenContextForXxx(); + SaManager.setSaTokenContext(saTokenContext); + } + +} +``` + +如果你使用的框架带有自动注入特性,那就更简单了,参考 Spring 集成包的 Bean 注入流程: +[注册Bean](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java)、 +[注入Bean](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java) + + +### 5、启动项目 + +启动项目,尝试打印一下 `SaManager.getSaTokenContext()` 对象,如果输出的是你的自定义实现类,那就证明你已经自定义 `SaTokenContext` 成功了, +快来体验一下 Sa-Token 的各种功能吧。 + + + + diff --git a/sa-token-doc/fun/sa-token-context.md b/sa-token-doc/fun/sa-token-context.md new file mode 100644 index 000000000..26a2faa7d --- /dev/null +++ b/sa-token-doc/fun/sa-token-context.md @@ -0,0 +1,111 @@ +# 自定义 SaTokenContext 指南 + +目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包, +如果我们使用的 Web 框架不在上述列表之中,则需要自定义 SaTokenContext 相关接口完成整合工作。 + +我们需要关注的主要就是四个接口: + +- SaTokenContext:上下文管理器。 +- SaRequest:请求对象,携带着一次请求的所有参数数据。 +- SaResponse:响应对象,携带着对客户端一次响应的所有数据。 +- SaStorage:请求上下文对象,提供 [一次请求范围内] 的上下文数据读写。 + +--- + + +### 上下文包装类 + +在鉴权中,必不可少的步骤就是从 `HttpServletRequest` 中读取 Token,然而当我们调用 `StpUtil.isLogin()` 获取当前会话是否登录时, +我们并没有传递 `HttpServletRequest` 参数,框架是怎么读取出来 Token 的呢? + +以 SpringBoot 项目为例,Sa-Token 框架会自动注册一个全局过滤器,在每次接收到请求时,将 `HttpServletRequest` 对象保存在 `ThreadLocal` 之中。 + +在后续的方法中,如果你调用了 `StpUtil.isLogin()` 等方法,框架便会从 `ThreadLocal` 中获取 `HttpServletRequest` 对象,从而进一步读取 token 等信息。 + +让我们来看一下具体的代码细节,全局上下文初始化过滤器: + +``` java +/** + * SaTokenContext 上下文初始化过滤器 (基于 Servlet) + */ +@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) +public class SaTokenContextFilterForServlet implements Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + SaTokenContextServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response); + chain.doFilter(request, response); + } finally { + SaTokenContextServletUtil.clearContext(); + } + } +} +``` + +进一步追踪 `SaTokenContextServletUtil.setContext` 方法: + +``` java +public static void setContext(HttpServletRequest request, HttpServletResponse response) { + SaRequest req = new SaRequestForServlet(request); + SaResponse res = new SaResponseForServlet(response); + SaStorage stg = new SaStorageForServlet(request); + SaManager.getSaTokenContext().setContext(req, res, stg); +} +``` + +此处有一个细节,为什么保存的不是原生 `HttpServletRequest` 与 `HttpServletResponse`,而是 `SaRequest`、`SaResponse`、`SaStorage` 三个包装对象? + +因为并不是所有的 web 框架都具有 `HttpServletRequest` 对象,例如在 WebFlux 中,只有 `ServerHttpRequest`, +在一些其它Web框架中,可能连 `Request` 的概念都没有。 + +Sa-Token 为了一套代码对接所有的 Web 框架,就在原生请求对象的基础上又封装了一层 `SaTokenContext` 相关接口,用于屏蔽掉不同 Web 框架之间的差异,提供统一的调用API: + +sa-token-context + +因此,要对接不同的 Web 框架,就要针对不同的 Web 框架封装不同版本的 `SaRequest`、`SaResponse`、`SaStorage` 包装类对象。 + +如果你的 Web 框架是基于 Servlet 规范开发的,那么你可以直接引入 `sa-token-servlet`,这个包封装了针对 Servlet 规范的上下文包装类对象: + + + +``` xml + + + cn.dev33 + sa-token-servlet + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 权限认证(ServletAPI 集成包) +implementation 'cn.dev33:sa-token-servlet:${sa.top.version}' +``` + + + +如果你的 web 框架不是基于 Servlet 规范开发的,也问题不大,手动实现一下即可,参考一下 Servlet 包是怎么做的: +[SaRequestForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java)、 +[SaResponseForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java)、 +[SaStorageForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java)。 + +封装好包装类对象之后,接下来要做的就是在这个 Web 框架中注册一个全局过滤器,将包装类对象保存到“全局上下文管理器”之中,以备调用: + +``` java +SaRequest req = new SaRequestForXxx(request); +SaResponse res = new SaResponseForXxx(response); +SaStorage stg = new SaStorageForXxx(request); +SaManager.getSaTokenContext().setContext(req, res, stg); +``` + +这样我们即可在具体的 Controller 请求中,成功调用 `StpUtil.isLogin()` 的 API。 + +总结:整体的步骤并不复杂,就是先定义 `SaRequest`、`SaResponse`、`SaStorage` 的包装类,然后在全局过滤器保存在上下文管理器中。 +可以参考具体实现 `sa-token-spring-boot-starter`(SpringBoot2 项目 starter 包): +[SaTokenContextFilterForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForServlet.java) + + + +### 旧版本方案 +在旧版本中(< v1.42.0)我们推荐的方案是自定义整个 `SaTokenCentext` 接口,目前此方案在新版本已不推荐,此处仅做留档备份:[自定义 SaTokenContext 指南](/fun/sa-token-context--backup.md) + diff --git a/sa-token-doc/doc/fun/sa-token-test.md b/sa-token-doc/fun/sa-token-test.md similarity index 100% rename from sa-token-doc/doc/fun/sa-token-test.md rename to sa-token-doc/fun/sa-token-test.md diff --git a/sa-token-doc/doc/fun/session-model.md b/sa-token-doc/fun/session-model.md similarity index 76% rename from sa-token-doc/doc/fun/session-model.md rename to sa-token-doc/fun/session-model.md index ef26e323d..3608bbbae 100644 --- a/sa-token-doc/doc/fun/session-model.md +++ b/sa-token-doc/fun/session-model.md @@ -2,7 +2,7 @@ --- -### 1、User-Session +### 1、Account-Session 提起Session,你脑海中最先浮现的可能就是 JSP 中的 HttpSession,它的工作原理可以大致总结为: @@ -24,17 +24,17 @@ Sa-Token Session可以理解为 HttpSession 的升级版: 3. Sa-Token支持Cookie、Header、body三个途径提交Token,而不是仅限于Cookie 4. 由于不强依赖Cookie,所以只要将Token存储到不同的地方,便可以做到一个客户端同时登录多个账号 -这种为账号id分配的Session,我们给它起一个合适的名字:`User-Session`,你可以通过如下方式操作它: +这种为账号id分配的Session,我们给它起一个合适的名字:`Account-Session`,你可以通过如下方式操作它: ``` java -// 获取当前会话的 User-Session +// 获取当前会话的 Account-Session SaSession session = StpUtil.getSession(); -// 从 User-Session 中读取、写入数据 +// 从 Account-Session 中读取、写入数据 session.get("name"); session.set("name", "张三"); ``` -使用`User-Session`在不同端同步数据是非常方便的,因为只要 PC 和 APP 登录的账号id一致,它们对应的都是同一个Session, +使用`Account-Session`在不同端同步数据是非常方便的,因为只要 PC 和 APP 登录的账号id一致,它们对应的都是同一个Session, 举个应用场景:在PC端点赞的帖子列表,在APP端的点赞记录里也要同步显示出来 @@ -42,15 +42,16 @@ session.set("name", "张三"); 随着业务推进,我们还可能会遇到一些需要数据隔离的场景: +> [!NOTE| label:业务场景] > 指定客户端超过两小时无操作就自动下线,如果两小时内有操作,就再续期两小时,直到新的两小时无操作 -那么这种请求访问记录应该存储在哪里呢?放在 User-Session 里吗? +那么这种请求访问记录应该存储在哪里呢?放在 Account-Session 里吗? -可别忘了,PC端和APP端可是共享的同一个 User-Session ,如果把数据放在这里, +可别忘了,PC端和APP端可是共享的同一个 Account-Session ,如果把数据放在这里, 那就意味着,即使用户在PC端一直无操作,只要手机上用户还在不间断的操作,那PC端也不会过期! 解决这个问题的关键在于,虽然两个设备登录的是同一账号,但是两个它们得到的token是不一样的, -Sa-Token针对会话登录,不仅为账号id分配了`User-Session`,同时还为每个token分配了不同的`Token-Session` +Sa-Token针对会话登录,不仅为账号id分配了`Account-Session`,同时还为每个token分配了不同的`Token-Session` 不同的设备端,哪怕登录了同一账号,只要它们得到的token不一致,它们对应的 `Token-Session` 就不一致,这就为我们不同端的独立数据读写提供了支持: @@ -80,22 +81,28 @@ session.set("name", "张三"); 只要两个自定义Session的Id一致,它们就是同一个Session +Custom-Session的会话有效期默认使用`SaManager.getConfig().getTimeout()`, 如果需要修改会话有效期, 可以在创建之后, 使用对象方法修改 + +``` java +session.updateTimeout(1000); // 参数说明和全局有效期保持一致 +``` + ### 4、Session模型结构图 三种Session创建时机: -- `User-Session`: 指的是框架为每个 账号id 分配的 Session +- `Account-Session`: 指的是框架为每个 账号id 分配的 Session - `Token-Session`: 指的是框架为每个 token 分配的 Session - `Custom-Session`: 指的是以一个 特定的值 作为SessionId,来分配的 Session **假设三个客户端登录同一账号,且配置了不共享token,那么此时的Session模型是:** -![session-model](https://oss.dev33.cn/sa-token/doc/session-model3.png 's-w') +session-model 简而言之: -- `User-Session` 以UserId为主,只要token指向的UserId一致,那么对应的Session对象就一致 +- `Account-Session` 以账号 id 为主,只要 token 指向的账号 id 一致,那么对应的Session对象就一致 - `Token-Session` 以token为主,只要token不同,那么对应的Session对象就不同 - `Custom-Session` 以特定的key为主,不同key对应不同的Session对象,同样的key指向同一个Session对象 diff --git a/sa-token-doc/fun/sso-vs-oauth2.md b/sa-token-doc/fun/sso-vs-oauth2.md new file mode 100644 index 000000000..00b9efe69 --- /dev/null +++ b/sa-token-doc/fun/sso-vs-oauth2.md @@ -0,0 +1,22 @@ +# 技术选型:[ 单点登录 ] VS [ OAuth2.0 ] + +--- + +QQ群里经常有小伙伴提问:项目需要搭建统一认证中心,是用 SSO 方便还是 OAuth2.0 方便呢?针对这个问题,我们列出两者的主要区别以供大家参考: + + +| 功能点 | SSO单点登录 | OAuth2.0 | +| :-------- | :-------- | :-------- | +| 统一认证 | 支持度高 | 支持度高 | +| 统一注销 | 支持度高 | 支持度低 | +| 多个系统会话一致性 | 强一致 | 弱一致 | +| 第三方应用授权管理 | 不支持 | 支持度高 | +| 自有系统授权管理 | 支持度高 | 支持度低 | +| Client级的权限校验 | 不支持 | 支持度高 | +| 集成简易度 | 比较简单 | 难度中等 | +| 适合项目 | 企业内部项目整合 | 企业搭建统一认证授权平台,对外开放服务 | + + +注:以上仅为在 Sa-Token 中两种技术的差异度比较,不同框架的实现可能略有差异,但整体思想是一致的。 + + diff --git a/sa-token-doc/fun/team.md b/sa-token-doc/fun/team.md new file mode 100644 index 000000000..95ab0ae66 --- /dev/null +++ b/sa-token-doc/fun/team.md @@ -0,0 +1,89 @@ +# 团队成员 + + +### 开发 + +负责:代码开发、社区维护、issue 处理、pr 审核等。 + + + + + + + + + + + + + + + + + + + + + + +
头像昵称个人主页
小风筝(作者)https://gitee.com/click33
AppleOfGrayhttps://gitee.com/appleOfGray
ly-chnhttps://gitee.com/ly-chn
+ + + +### 提案讨论组成员 + +负责:提案新增、讨论、投票。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
头像昵称个人主页
刘潇https://gitee.com/click33
AppleOfGrayhttps://gitee.com/appleOfGray
ly-chnhttps://gitee.com/ly-chn
茉莉https://gitee.com/kidoldman
药水https://gitee.com/java_pioneer
呆某人https://gitee.com/zhubj0510
春困夏倦秋乏https://gitee.com/uncarbon97
淡墨https://gitee.com/jinan-jimeng-network_0
+ + + + + + diff --git a/sa-token-doc/doc/fun/tech-stack.md b/sa-token-doc/fun/tech-stack.md similarity index 90% rename from sa-token-doc/doc/fun/tech-stack.md rename to sa-token-doc/fun/tech-stack.md index 34632355d..85b0ad21e 100644 --- a/sa-token-doc/doc/fun/tech-stack.md +++ b/sa-token-doc/fun/tech-stack.md @@ -5,7 +5,7 @@ - Maven多模块项目 - Servlet API、临时Cookie与永久Cookie、Request参数获取 - SpringBoot2.0、Redis、Jackson、Hutool、jwt -- SpringBoot自定义starter、Spring包扫码 + 依赖注入、AOP注解切面、yml配置映射、拦截器 +- SpringBoot自定义starter、Spring包扫描 + 依赖注入、AOP注解切面、yml配置映射、拦截器 - Java8 接口与default实现、静态方法、枚举、定时器、异常类、泛型、反射、IO流、自定义注解、Lambda表达式、函数式编程 - package-info注释、Serializable序列化接口、synchronized锁 - java加密算法:MD5、SHA1、SHA256、AES、RSA diff --git a/sa-token-doc/fun/three-scope.md b/sa-token-doc/fun/three-scope.md new file mode 100644 index 000000000..32320d3da --- /dev/null +++ b/sa-token-doc/fun/three-scope.md @@ -0,0 +1,42 @@ +# 三大作用域 + +--- + +Sa-Token 数据存储有三大作用域,分别是: +- `SaStorage` - 请求作用域:存储的数据只在一次请求内有效。 +- `SaSession` - 会话作用域:存储的数据在一次会话范围内有效。 +- `SaApplication` - 全局作用域:存储的数据在全局范围内有效。 + + +### SaStorage - 请求作用域 +在 SaStorage 中存储的数据只在一次请求范围内有效,请求结束后数据自动清除。使用 SaStorage 时无需处于登录状态。 + +``` java +SaStorage storage = SaHolder.getStorage(); +storage.get("key"); // 取值 +storage.set("key", "value"); // 写值 +storage.delete("key"); // 删值 +``` + + +### SaSession - 会话作用域 +在 SaSession 存储的数据在一次会话范围内有效,会话结束后数据自动清除。必须登录后才能使用 SaSession 对象。 + +``` java +SaSession session = StpUtil.getSession(); +session.get("key"); // 取值 +session.set("key", "value"); // 写值 +session.delete("key"); // 删值 +``` + + +### SaApplication - 全局作用域 +在 SaApplication 存储的数据在全局范围内有效,应用关闭后数据自动清除(如果集成了 Redis 那则是 Redis 关闭后数据自动清除)。使用 SaApplication 时无需处于登录状态。 + +``` java +SaApplication application = SaHolder.getApplication(); +application.get("key"); // 取值 +application.set("key", "value"); // 写值 +application.delete("key"); // 删值 +``` + diff --git a/sa-token-doc/fun/timeline.md b/sa-token-doc/fun/timeline.md new file mode 100644 index 000000000..32db89aa6 --- /dev/null +++ b/sa-token-doc/fun/timeline.md @@ -0,0 +1,39 @@ +# Sa-Token 开源大事记 + +--- + +- **2020-02-04:** 在 GitHub 提交第一个版本,正式开源。 +- **2020-09-14:** GitHub star 数量破 100。 +- **2020-10-26:** Gitee star 数量破 100。 +- **2021-03-01:** 被 [HelloGitHub] 第 59 期收录推荐。 +- **2021-03-26:** GitHub star 数量破 1k。 +- **2021-03-30:** 受 TLog 作者邀请,Sa-Token 加入 dromara 社区。 +- **2021-03-30:** 被 Gitee 列为推荐项目。 +- **2021-03-31:** Gitee star 数量破1K。 + + + +- **2021-06-17:** Sa-Token star 数量 (3529) 超过 Shiro (3506)。 + +- **2021-07-26:** GitHub star 数量破5K。 + + +- **2021-09-24:** Sa-Token star 数量 (6280) 超过 SpringSecurity (6247)。 + +- **2021-11-08:** 荣获开源中国 “码云 GVP 认证”。 + +- **2021-11-28:** Gitee star 数量破5K。 +- **2021-12-27:** 荣获 OSC-2021 最佳软件 Top 30。 +- **2022-05-20:** 成为 [可信开源社区共同体] 预备成员。 +- **2022-08-01:** 加入 [中国开源社区 landscape]。 +- **2022-08-18:** GitHub 第 10000 个 star 里程碑! +- **2023-01-09:** 荣获 OSC 2022 年度最热开源项目社区。 +- **2023-11-21:** 被评为“开放原子基金会2023快速成长开源项目”。 +- **2024-04-25:** 42.9k star 登顶 Gitee 开源项目推荐榜 Top 1。 +- **2024-08-19:** 成为 GitCode G-Star 开源摘星计划毕业项目。 +- **2024-11-22:** 所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。 +- **2025-11-13:** 荣获 Gitee 5000 Star 专属定制奖杯。 +- **2026-02-05:** 荣获《Gitee 2025年度开源项目 Web应用开发 Top 2》。 + + + diff --git a/sa-token-doc/fun/token-info.md b/sa-token-doc/fun/token-info.md new file mode 100644 index 000000000..584bf8a93 --- /dev/null +++ b/sa-token-doc/fun/token-info.md @@ -0,0 +1,22 @@ +# SaTokenInfo 参数详解 + +token信息Model: 用来描述一个token的常用参数 + +``` js +{ + "code": 200, + "msg": "ok", + "data": { + "tokenName": "satoken", // token名称 + "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值 + "isLogin": true, // 此token是否已经登录 + "loginId": "10001", // 此token对应的LoginId,未登录时为null + "loginType": "login", // 账号类型标识 + "tokenTimeout": 2591977, // token剩余有效期 (单位: 秒) + "sessionTimeout": 2591977, // Account-Session剩余有效时间 (单位: 秒) + "tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存) + "tokenActiveTimeout": -1, // token 距离被冻结还剩的时间 (单位: 秒) + "loginDevice": "DEF" // 登录设备类型 + }, +} +``` \ No newline at end of file diff --git a/sa-token-doc/fun/token-timeout.md b/sa-token-doc/fun/token-timeout.md new file mode 100644 index 000000000..c4d11900d --- /dev/null +++ b/sa-token-doc/fun/token-timeout.md @@ -0,0 +1,131 @@ +# Token有效期详解 + + + +Sa-Token 提供两种 Token 自动过期策略,分别是 `timeout` 与 `active-timeout`,配置方法如下: + + + +``` yaml +sa-token: + # token 有效期(单位:秒),默认30天,-1代表永不过期 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 +``` + +``` properties +# token 有效期(单位:秒),默认30天,-1代表永不过期 +sa-token.timeout=2592000 +# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 +sa-token.active-timeout=-1 +``` + + + +两者的区别,可以通过下面的例子体现: + +> [!TIP| label:场景示例] +> 1. 假设你到银行要存钱,首先就要办理一张卡 (要访问系统接口先登录)。 +> 2. 银行为你颁发一张储蓄卡(系统为你颁发一个Token),以后每次存取钱都要带上这张卡(后续每次访问系统都要提交 Token)。 +> 3. 银行为这张卡设定两个过期时间: +> - 第一个是 `timeout`,代表这张卡的长久有效期,就是指这张卡最长能用多久,假设 `timeout=3年`,那么3年后此卡将被银行删除,想要继续来银行办理业务必须重新办卡(Token 过期后想要访问系统必须重新登录)。 +> - 第二个就是 `active-timeout`,代表这张卡的最低活跃频率限制,就是指这张卡必须每隔多久来银行一次,假设 `active-timeout=1月` ,你如果超过1月不来办一次业务,银行就将你的卡冻结,列为长期不动户(Token 长期不访问系统,被冻结,但不会被删除)。 +> 4. 两个过期策略可以单独配置,也可以同时配置,只要有其中一个有效期超出了范围,这张卡就会变得不可用(两个有效期只要有一个过期了,Token就无法成功访问系统了)。 + +下面是对两个过期策略的详细解释: + +### timeout +1. `timeout`代表 Token 的长久有效期,单位/秒,例如将其配置为 2592000 (30天),代表在30天后,Token必定过期,无法继续使用。 +2. `timeout`~~无法续签,想要继续使用必须重新登录~~。v1.29.0+ 版本新增续期方法:`StpUtil.renewTimeout(100)`。 +3. `timeout`的值配置为-1后,代表永久有效,不会过期。 + + +### active-timeout +1. `active-timeout`代表最低活跃频率,单位/秒,例如将其配置为 1800 (30分钟),代表用户如果30分钟无操作,则此Token会立即过期(被冻结,但不会删除掉)。 +2. 如果在30分钟内用户有操作,则会再次续签30分钟,用户如果一直操作则会一直续签,直到连续30分钟无操作,Token才会过期。 +3. `active-timeout`的值配置为-1后,代表永久有效,不会过期,此时也无需频繁续签。 + + +### 关于active-timeout的续签 +如果`active-timeout`配置了大于零的值,Sa-Token 会在登录时开始计时,在每次直接或间接调用`getLoginId()`、`getTokenSession()`时进行一次冻结检查与续签操作。 +此时会有两种情况: +1. 一种是会话无操作时间太长,Token已经被冻结,此时框架会抛出`NotLoginException`异常(场景值=-3), +2. 另一种则是会话在`active-timeout`有效期内通过检查,此时Token可以成功续签 + + +### 我可以手动续签 active-timeout 吗? +**可以!** +如果框架的自动续签算法无法满足您的业务需求,你可以进行手动续签,Sa-Token 提供两个API供你操作: +1. `StpUtil.checkActiveTimeout()`: 检查当前Token 是否已经被冻结,如果是则抛出异常 +2. `StpUtil.updateLastActiveToNow()`: 续签当前Token:(将 [最后操作时间] 更新为当前时间戳) + +注意:在手动续签时,即使 Token 已经被冻结也可续签成功(解冻),如果此场景下需要提示续签失败,可采用先检查再续签的形式保证Token有效性 + +例如以下代码: +``` java +// 先检查是否已被冻结 +StpUtil.checkActiveTimeout(); +// 检查通过后继续续签 +StpUtil.updateLastActiveToNow(); +``` + +同时,你还可以关闭框架的自动续签(在配置文件中配置 `autoRenew=false` ),此时续签操作完全由开发者控制,框架不再自动进行任何续签操作 + +如果你需要给其它 Token 续签: + +``` java +// 为指定 Token 续签 +StpUtil.stpLogic.updateLastActiveToNow(tokenValue); +``` + + +### timeout 与 active-timeout 可以同时使用吗? +**可以同时使用!** +两者的认证逻辑彼此独立,互不干扰,可以同时使用。 + + +### StpUtil 类中哪些方法支持自动续签 active-timeout? +> 直接或间接调用过 `getLoginId()`、`getTokenSession()` 的方法 + +| 包括但不限于这些: | +|---| +| StpUtil.checkLogin() | +| StpUtil.getLoginId() | +| StpUtil.getLoginIdAsInt() | +| StpUtil.getLoginIdAsString() | +| StpUtil.getLoginIdAsLong() | +|---| +| StpUtil.getSession() | +| StpUtil.getTokenSession() | +|---| +| StpUtil.getRoleList() | +| StpUtil.hasRole() | +| StpUtil.hasRoleAnd() | +| StpUtil.hasRoleOr() | +| StpUtil.checkRole() | +| StpUtil.checkRoleAnd() | +| StpUtil.checkRoleOr() | +|---| +| StpUtil.getPermissionList() | +| StpUtil.hasPermission() | +| StpUtil.hasPermissionAnd() | +| StpUtil.hasPermissionOr() | +| StpUtil.checkPermission() | +| StpUtil.checkPermissionAnd() | +| StpUtil.checkPermissionOr() | +|---| +| StpUtil.openSafe() | +| StpUtil.isSafe() | +| StpUtil.checkSafe() | +| StpUtil.getSafeTime() | +| StpUtil.closeSafe() | + +> 以下注解都间接调用过 getLoginId() 方法 + +| 支持自动续签的注解 | +|---| +| @SaCheckLogin | +| @SaCheckRole | +| @SaCheckPermission | +| @SaCheckSafe | diff --git a/sa-token-doc/github-logo.svg b/sa-token-doc/github-logo.svg new file mode 100644 index 000000000..dfafc4830 --- /dev/null +++ b/sa-token-doc/github-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sa-token-doc/include/include-qa.md b/sa-token-doc/include/include-qa.md new file mode 100644 index 000000000..14344dbe0 --- /dev/null +++ b/sa-token-doc/include/include-qa.md @@ -0,0 +1,9 @@ + + +> [!WARNING| label:更改了 hosts 但无法访问?] +> - 可能 1:你没保存。 +> - 可能 2:你后端项目没启动。 +> - 可能 3:你访问时端口写错了。 +> - 可能 4:你开了 VPN,关掉试试。 + + \ No newline at end of file diff --git a/sa-token-doc/index.css b/sa-token-doc/index.css deleted file mode 100644 index 13fbad771..000000000 --- a/sa-token-doc/index.css +++ /dev/null @@ -1,159 +0,0 @@ -/* ================================== 内容 ====================================== */ -/* 总 */ -*{margin: 0px; padding: 0px;} -body{font-size: 16px; color: #34495E; font-family: "Source Sans Pro","Helvetica Neue","Arial,sans-serif";} -.s-width{width: 1000px; margin: auto;} -/* 栏目标题 */ -.s-title{color: #000; margin-top: 90px; margin-bottom: 50px;} -/* 解释性文字 */ -.re-text{color: #4e6e8e;} -/* 分割线 */ -.s-fenge{width: 80%; margin: auto; border-top: 1px #ddd solid;} - -/* -------- header头部 --------- */ -.s-header{height: 70px; border: 0px #000 solid; position: fixed; width: 100%;} -.s-header{background-color: #FFF; color: #000; z-index: 99999; box-shadow: 0 1px 3px rgba(26,26,26,0.1);} -/* logo部分 */ -.logo-box {margin-top: 10px; margin-left: 30px; cursor: pointer; color: #000; float: left;} -.logo-box img {width: 50px; height: 50px; vertical-align: middle;} -.logo-box .logo-text {display: inline-block;vertical-align: middle; font-size: 22px;font-weight: 400;} -/* 小章鱼 */ -.github-corner svg{color: #fff; fill: var(--theme-color, #42b983); height: 80px; width: 80px;} -/* 右边导航 */ -.nav-right{float: right; line-height: 70px; padding-right: 4.5em; white-space: nowrap;} -.nav-right a{padding: 0px 1em; color: #34495E; text-decoration: none; transition: all 0.2s;} -.nav-right a:hover{color: #42B983;} - -/* -------- 海报部分 --------- */ -.main-box{width: 100%; min-height: 100vh;; /* height: 80vh; */ text-align: center; background-image: url(https://oss.dev33.cn/sa-token/home-bg.jpg); } -.main-box{display: flex; align-items: center; text-align: center; } -.fenge{min-height: 90px;} -.content-box{color: #000; flex: 1;} -.content-box h1{font-size: 100px; font-weight: 400; position: relative; margin-top: 40px; /* margin-top: 15vh; */} -.content-box h1 small{font-size: 18px; position: absolute; bottom: 10px; margin-left: 5px; font-weight: 100;} -/* .title-logo{width: 221px; cursor: pointer; transition: all 0.2s;} -.title-logo:hover{transform: scale(1.2, 1.2);} */ - -.sub-title{font-size: 26px; font-weight: 400; margin-top: 30px; margin-bottom: 25px; color: #6a8bad; color: #444;} -/* .content-box p{line-height: 30px; padding: 0px 1em;} */ - -.main-box{animation: changes 60s 0.2s linear infinite normal; background-attachment: ;} /* normal | alternate */ -@keyframes changes { - from {background-position: 0vw 0%;} - to {background-position: -100vw 0%;} -} - -/* 几个按钮 */ -.btn-box{margin-top: 50px; margin-bottom: 40px;} -.btn-box a{border: 1px #42B983 solid; border-radius: 2em; box-sizing: border-box; color: #3eaf7c; display: inline-block;transition: all 0.1s;} -.btn-box a{font-size: 14px; background-color: rgba(0,255,0,0.04); letter-spacing: 1px; padding: 1em 2em; margin: 0 0.5em; margin-bottom: 14px; text-decoration: none; } -/* 最后一个加深底色 */ -.btn-box a:last-child {color: #fff; background-color: #42B983; border: 1px green solid;} -.btn-box a:hover{/* transform: scale(1.05, 1.05); */padding: 1em 2.3em; margin: 0 0.2em;} - -/* 按钮发光动画 */ -.btn-box a:last-child{animation: bganimation 3s infinite;} -@keyframes bganimation{ - 0%{box-shadow: 0 0 1px #42B983;} - 50%{box-shadow: 0 0 20px #42B983;} - 100%{box-shadow: 0 0 20px #FFF;} -} -/* 微信二维码 */ -.wx-qr-box{margin-top: 50px;} -.qr-item{display: inline-block;} -.qr-item p{font-size: 12px; padding: 0 0.5em;} -/* .qr-item a{color: #42B983;} */ -.wx-qr{width: 150px;} -.wx-qr-box p{margin-top: 10px; color: #666; margin-bottom: 20px;} -.wx-qr,.dro-qr{cursor: pointer;} - -/* -------- 支持特性 --------- */ -.feature-z{padding: 0em 1em; padding-top: 0px; padding-bottom: 60px; text-align: center; color: #000;} -.feature-z .s-title{font-size: 30px; font-weight: 400; margin-top: 70px; margin-bottom: 40px;} -.feature-z{color: rgb(128, 128, 128); text-align: center; box-sizing: border-box; line-height: 24px; font-size: 16px;} - -.feature-box{margin-top: 40px; margin-bottom: 60px; display: flex; flex-wrap: wrap; justify-content: flex-start;} -.feature{border: 0px #000 solid; flex: 1; min-width: 33%; max-width: 33%; text-align: left; padding: 1.5em 1.2em; box-sizing: border-box;} -.feature h2{font-size: 24px; color: #000; font-weight: 400;} -.feature p{margin-top: 14px; font-size: 16px; color: #4e6e8e;} - -/* -------- 使用公司 --------- */ -.com-box-f{padding: 1em 1em; padding-bottom: 30px; text-align: center;} -.com-box-f h2{font-size: 30px; color: #000; font-weight: 400;} -.com-box{display: flex; flex-wrap: wrap; width: 100%; margin-bottom: 50px; justify-content: flex-start;} -.com-box a{flex: 1; display: block; cursor: pointer; border: 0px #ddd solid;} -.com-box a{min-width: 17%; max-width: 18%; margin: 5px; line-height: 100px;} -.com-box a img{transition: all 0.2s; vertical-align: middle; min-width: 60%; max-width: 100%; max-height: 100%;} -.com-box a img:hover{transform: scale(1.05, 1.05);} -.com-box-you a img:hover{transform: none;} - -/* -------- 友情链接 --------- */ -.com-box-you a{min-width: 13%; max-width: 14%; line-height: 60px; height: 60px; margin: 10px;} -.com-box-you a img{min-width: 60%; max-width: 100%; vertical-align: middle; max-height: 100%;} - -/* -------- 底部 - 连接 --------- */ -#footer{background-color: #181818;} -#footer h3{font-weight: 400; font-size: 16px; color: #ccc; margin-top: 20px; margin-bottom: 20px;} -#footer{border-top: 1px #666 solid;} -.footer-r-b{display: flex; padding: 40px 0;} -.ss-box{display: inline-block; flex: 1; color: #595959; margin: 0 50px; font-size: 14px;} -.ss-box a{color: #595959; text-decoration: none;} -.ss-box a:hover{color: #EEE; text-decoration: underline;} -.ss-box ul{margin: 0; padding: 0;} -.ss-box li{list-style: none; line-height: 28px;} - -/* -------- 底部 - 版权 --------- */ -.foot-box{background-color: #000; color: #ddd; padding: 2em 0px; line-height: 28px; overflow: hidden; position: relative; z-index: 100;} -.foot-box{border-top: 0px #666 solid;} -.foot-box p{text-indent: 1em;} -.foot-box b{font-size: 1.1em;} -.foot-box a{color: #ddd; font-size: 0.9em;} -.foot-box a:hover{text-decoration: underline;} - - - -/* -------- 自适应 --------- */ -/* 媒体查询 */ -@media screen and (max-width: 800px) { - - .s-title{padding: 0 16px;} - .s-width{width: 100%;} - - .logo-box,.copyright {display: none;} - .main-box{ height: auto;} - .content-box{ padding: 2em 1em;} - .content-box h1{font-size: 50px;} - - .feature-z{padding: 0em;} - .feature{min-width: 100%;} - - .com-box-f{padding: 0em;} - .com-box a{min-width: 90%;} - - .footer-r-b{display: block;} - .ss-box{display: block; width: 90%; margin: 0px; padding-left: 1.5em;} - - /* .s-header{position: static;} */ - footer{position: static; line-height: 40px;} -} - -@media screen and (min-width: 1700px) { - .content-box h1{margin-top: 120px;} -} - - -/* 闪光背景 */ -/* .main-box{ - background-size: 500%; - background-image: linear-gradient(125deg,#BFFEBE,#F6F8B5,#FCD0B3,#BFB6F8,#E8D8B3); - animation: bganimation 15s infinite; -} -@keyframes bganimation{ - 0%{background-position: 0% 50%;} - 50%{background-position: 100% 50%;} - 100%{background-position: 0% 50%;} -} */ - - - - diff --git a/sa-token-doc/index.html b/sa-token-doc/index.html index 7a2cbab8a..3f5370843 100644 --- a/sa-token-doc/index.html +++ b/sa-token-doc/index.html @@ -1,315 +1,1174 @@ - + Sa-Token - - - - - - + + + + + + + + + + + + + + + + -

- +
+ -
- -
- - Sa-Token -
-
+
+
- +
-
- -

Sa-Tokenv1.24.0

-
这可能是史上功能最全的java权限认证框架!
+ +

Sa-Tokenv1.45.0

+
+ 开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅! +
 
+
-

- - - - - - - +

+ + + + + + + +

-
-
- -

QQ交流群: 1002350610 点击加入

-
-
- -

微信交流群:扫码加入 (请备注: sa)

-
+
+ + + + + + + + + + + + B站、抖音、视频号 ... +
+

关注我们 → 分享“权限认证架构设计”干货视频

+
+ + + +
+
+
+ + +
-

支持 ⚡ 特性

+

Sa-Token 支持特性

-

登录认证

-

多地登录、单地登录、同端互斥登录…… 轻松登录鉴权,并提供五种细分场景值

-
-
-

权限认证

-

权限认证、角色认证、会话二级认证……适配RBAC权限模型,权限分配精细到按钮级

-
-
-

Session会话

-

内置三种会话模型:全端共享Session、单端独享Session、自定义Session

-
-
-

踢人下线

-

一行代码实现踢人下线功能,清退违规用户,对方再次访问系统提示“已被踢下线”

-
-
-

账号封禁

-

封禁指定账号,使其无法登陆,还可指定解封时间,亦或永久封禁!

+

⚡️ 登录认证

+

多端登录、单端登录、同端互斥登录、七天免登录…… 多种登录策略只需改个配置即可完成

-

Redis集成

-

可扩展持久层,集成Redis、Memcached等专业缓存中间件,重启数据不丢失

+

🔑️️ 权限认证

+

权限认证、角色认证、会话二级认证、注解鉴权、路由鉴权……多种姿势灵活鉴权

-

前后台分离

-

三种token读取策略,适配无Cookie模式,完美兼容APP、小程序等前后台分离场景

+

⛏️ 踢人下线

+

强制注销、踢人下线、账号封禁、身份切换、自动续签 …… 提供完善的会话管理方案

-

分布式会话

-

保证多节点数据一致性,提供jwt集成和共享数据中心两种分布式会话方案

+

🔎 Redis集成

+

提供 Redis 集成方案、项目重启数据不丢失、多系统数据互通,可自定义数据持久化策略

-

微服务网关鉴权

-

适配Gateway、ShenYu、Zuul等常见网关组件的请求拦截认证

+

🚀️️ 前后端分离

+

内置多种 Token 读取策略,适配APP、小程序、SPA单页应用等前后端分离场景

-

单点登录

-

内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以轻松搞定

+

️🍃 单点登录

+

同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离……提供各种架构下的SSO接入方案

-

OAuth2.0

-

基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式

+

🍂 OAuth2.0

+

轻松搭建 OAuth2.0 认证中心,支持四种授权模式,支持 openid 授权机制,支持二次扩展开发

-

Alone独立Redis

-

为Sa-Token单独配置一个Redis实例,将权限缓存与业务缓存分离

+

💦️ 微服务支持

+

分布式 Session 会话、网关统一鉴权、RPC调用鉴权……提供开箱即用的微服务认证方案

-

临时Token验证

-

集成jwt临时验证模块,用于短时间的权限验证

+

🗳️ 开箱即用

+

提供SpringMVC、WebFlux、Solon、jwt 等常见框架集成包,真正的开箱即用……

-
-

临时身份切换

-

实时操作任意用户状态数据,还可临时身份切换,关键时刻让你少写N多代码

-
-
-

同端互斥登录

-

像QQ一样手机电脑可以同时在线,但是在两个手机上互斥登录

-
-
-

多账号体系认证

-

例如一个商城项目的User表和Admin表分开鉴权,互不干扰,轻松搞定一个系统多套权限体系的场景!

+
+
+
+ + + +
+
+
+
+
+

+ 七年磨一剑 🗡️ + 一站式解决方案 +

+
+ + + +
+
+ + Sa-Token 可以帮你轻松解决大多数权限认证问题! + 点击查看功能结构图 +
+
+
+ + + +
+
+
+

曾获荣誉

+
+
+
+
+
+

GVP - Gitee 最有价值开源项目

+
+
+
+

GitCode G-Star 优质开源项目

+
+
+
+

OSCHINA 2021 人气指数 TOP 30 开源项目

+
+
+
+

OSCHINA 2022 年度最火热中国开源项目社区

+
+
+
+

开放原子基金会2023快速成长开源项目

+
+
+
+

Gitee 5000 star 专属奖杯

+
+
+
+

Gitee 2025年度开源项目 Web应用开发 Top 2

+
+
+
+

Dromara 组织顶尖项目(之一)

+
+
+
+

可信开源社区共同体预备成员

+
+
+
+

所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖

+
+
+
+

Gitee 项目推荐榜 top 1

+
+
+
+

GitHub stars 超 18k+

+
+
+
+
+
-
-

二级认证

-

在已登录的会话上再次认证,保证安全性

+
+
+
+ + +
+
+

Java 鉴权框架 Stars 对比

+
+ +
+
+
+ + + +
+
+

赞助者名单(感谢!感谢!感谢!)

+
+
+
+ 日期排序 + | + 赞助额排序 +
+ + + + + + + + + + + + +
赞助人赞助金额留言时间
+ +
+ + 第 1/1 页 + +
+
+

+ (如果您也有赞助 Sa-Token 的想法,可以参考: + 赞助名单 + ) +

-
-

定制Token风格

-

内置六种token风格随意切换,还可自定义token生成策略

+
+
+
+ + + + +
+
+
+

优秀开源集成案例

+
+ +
+ + + +

Snowy

+ 小诺开源技术 +

国内首个国密前后分离快速开发平台,基于Vue3、Antdv、SaToken

-
-

注解式鉴权

-

优雅的将鉴权与业务代码分离!

+ +
+ + + +

mall4j

+ Mall4j商城系统 +

基于Spring Boot 3 JDK17的一个商城手脚架。

-
-

路由拦截式鉴权

-

轻松搞定不同模块不同权限校验,还可适配Restful风格路由

+ +
+ + + +

RuoYi-Vue-Plus

+ 疯狂的狮子Li +

重写 RuoYi-Vue 所有功能,集成 Sa-Token、Mybatis-Plus、Hutool 定期同步

-
-

自动续签

-

提供两种token过期策略,灵活搭配使用,还可自动续签

+ +
+ + + +

Smart-Admin

+ 1024创新实验室 +

坚持以「高质量代码」为核心,「简洁、高效、安全」的中后台解决方案!

-
-

会话治理

-

提供方便灵活的会话查询接口,绕过API,直达数据底层,解决各种边际场景的疑难杂症

+ +
+ + + +

SpringBoot_v2

+ 开源oschina +

努力打造 springboot 框架的极致细腻的脚手架,原生纯净。

-
-

记住我模式

-

轻松适配 [记住我] 模式,重启浏览器免验证,还可指定免验证具体时长

+ +
+ + + +

灯灯

+ 最后 +

专注于多租户解决方案的微服务中后台快速开发平台。

-
-

密码加密

-

提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密

+ +
+ + + +

RuoYi-Cloud-Plus

+ 疯狂的狮子Li +

重写 RuoYi-Cloud 所有功能 整合 SpringCloudAlibaba、Dubbo3.0、Sa-Token

-
-

全局侦听器

-

可在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作

+ +
+ + + +

橙单

+ orange-form +

橙单中台化低代码生成器。多应用、多租户、多渠道、工作流、在线表单等。

-
-

开箱即用

-

提供SpringMVC、WebFlux、Solon等常见web框架的starter集成包,真正的开箱即用

+ +
+ + + +

拾壹博客

+ bule +

一款 Vue + SpringBoot 前后端分离的博客系统

+ + + + + + +
+
+ + 如果您的开源项目也使用了 Sa-Token,您可以 + 在此 + 提交 + +
+ +
+

Sa-Token 官方公众号,及时接收框架更新通知、技术文章

+
-
有了Sa-Token,你所有的权限认证问题,都不再是问题!
- - + + +

-

正在使用 Sa-Token 的公司 / 机构

+

正在使用 Sa-Token 的企业 / 机构

-

- (如果您的项目也使用了Sa-Token,您可以 - 在此 +

+ (如果您的企业也使用了 Sa-Token,您可以 + 在此 提交)

- - - + + +
+ + + - + + +
-->
-

Dromara公众号

+

Sa-Token 公众号

- +
- +
- -
- - - -
-
- - - + + + - - + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sa-token-doc/logo.png b/sa-token-doc/logo.png new file mode 100644 index 000000000..88fbcde9d Binary files /dev/null and b/sa-token-doc/logo.png differ diff --git a/sa-token-doc/micro/dcs-session.md b/sa-token-doc/micro/dcs-session.md new file mode 100644 index 000000000..9ae4a3c6b --- /dev/null +++ b/sa-token-doc/micro/dcs-session.md @@ -0,0 +1,68 @@ +# 微服务 - 分布式Session会话 + +--- + +### 需求场景 + +微服务架构下的第一个难题便是数据同步,单机版的`Session`在分布式环境下一般不能正常工作,为此我们需要对框架做一些特定的处理。 + +首先我们要明白,分布式环境下为什么`Session`会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点, +这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。 + +### 解决方案 + +要怎么解决这个问题呢?目前的主流方案有四种: +1. **Session同步**:只要一个节点的数据发生了改变,就强制同步到其它所有节点 +2. **Session粘滞**:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务 +3. **建立会话中心**:将Session存储在专业的缓存中间件上,使每个节点都变成了无状态服务,例如:`Redis` +4. **颁发无状态token**:放弃Session机制,将用户数据直接写入到令牌本身上,使会话数据做到令牌自解释,例如:`jwt` + + +### 方案选择 + +该如何选择一个合适的方案? +- 方案一:性能消耗太大,不太考虑 +- 方案二:需要从网关处动手,与框架无关 +- 方案三:Sa-Token 整合`Redis`非常简单,详见章节:[集成 Redis](/up/integ-redis) +- 方案四:详见官方仓库中 Sa-Token 整合`jwt`的示例(示例压缩包:[Sa-Token 集成示例大全下载](/more/download-demos) ) + +由于`jwt`模式不在服务端存储数据,对于比较复杂的业务可能会功能受限,因此更加推荐使用方案三 + + + + + +集成依赖示例: + + + +``` xml + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + +``` + +``` gradle +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' +``` + + +详细参考:[集成 Redis](/up/integ-redis) + + + + + + + + + diff --git a/sa-token-doc/micro/gateway-auth.md b/sa-token-doc/micro/gateway-auth.md new file mode 100644 index 000000000..8f8d3c14b --- /dev/null +++ b/sa-token-doc/micro/gateway-auth.md @@ -0,0 +1,132 @@ +# 微服务 - 网关统一鉴权 + +微服务架构下的鉴权一般分为两种: +1. 每个服务各自鉴权 +2. 网关统一鉴权 + +方案一和传统单体鉴权差别不大,不再过多赘述,本篇介绍方案二的整合步骤: + +--- + + + +### 1、引入依赖 + +首先,根据 [依赖引入说明](/micro/import-intro) 引入正确的依赖,以`[SpringCloud Gateway]`为例: + + + +``` xml + + + cn.dev33 + sa-token-reactor-spring-boot-starter + ${sa.top.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + +``` gradle +// Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' + +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + + +注:Redis包是必须的,因为我们需要和各个服务通过Redis来同步数据 + +### 2、实现鉴权接口 +``` java +/** + * 自定义权限验证接口扩展 + */ +@Component +public class StpInterfaceImpl implements StpInterface { + + @Override + public List getPermissionList(Object loginId, String loginType) { + // 返回此 loginId 拥有的权限列表 + return ...; + } + + @Override + public List getRoleList(Object loginId, String loginType) { + // 返回此 loginId 拥有的角色列表 + return ...; + } + +} + +``` +关于数据的获取,建议以下方案三选一: +1. 在网关处集成ORM框架,直接从数据库查询数据 +2. 先从Redis中获取数据,获取不到时走ORM框架查询数据库 +3. 先从Redis中获取缓存数据,获取不到时走RPC调用子服务 (专门的权限数据提供服务) 获取 + + +### 3、注册全局过滤器 +然后在网关处注册全局过滤器进行鉴权操作 + +``` java +/** + * [Sa-Token 权限认证] 配置类 + * @author click33 + */ +@Configuration +public class SaTokenConfigure { + // 注册 Sa-Token全局过滤器 + @Bean + public SaReactorFilter getSaReactorFilter() { + return new SaReactorFilter() + // 拦截地址 + .addInclude("/**") /* 拦截全部path */ + // 开放地址 + .addExclude("/favicon.ico") + // 鉴权方法:每次访问进入 + .setAuth(obj -> { + // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 + SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin()); + + // 权限认证 -- 不同模块, 校验不同权限 + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + + // 更多匹配 ... */ + }) + // 异常处理方法:每次setAuth函数出现异常时进入 + .setError(e -> { + return SaResult.error(e.getMessage()); + }) + ; + } +} +``` + +详细操作参考:[路由拦截鉴权](/use/route-check) + + + + + + diff --git a/sa-token-doc/micro/import-intro.md b/sa-token-doc/micro/import-intro.md new file mode 100644 index 000000000..80101d2c0 --- /dev/null +++ b/sa-token-doc/micro/import-intro.md @@ -0,0 +1,100 @@ + +# 微服务中使用Sa-Token 依赖引入说明 + +--- + +虽然在 [开始] 章节已经说明了依赖引入规则,但是交流群里不少小伙伴提出bug解决到最后发现都是因为依赖引入错误导致的,此处再次重点强调一下: + +> [!TIP| style:callout] +> **在微服务架构中使用Sa-Token时,网关和内部服务要分开引入Sa-Token依赖(不要直接在顶级父pom中引入Sa-Token)** + +总体来讲,我们需要关注的依赖就是两个:`sa-token-spring-boot-starter` 和 `sa-token-reactor-spring-boot-starter`: + + + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 + + +``` gradle +// Sa-Token 权限认证,在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 + + + + +``` xml + + + cn.dev33 + sa-token-reactor-spring-boot-starter + ${sa.top.version} + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + +``` gradle +// Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + + +至于怎么分辨我们需要引入哪个呢?这个要看你使用的基础框架: + +对于内部基础服务来讲,我们一般都是使用SpringBoot默认的web模块:SpringMVC, +因为这个SpringMVC是基于Servlet模型的,在这里我们需要引入的是`sa-token-spring-boot-starter` + +对于网关服务,大体来讲分为两种: +- 一种是基于Servlet模型的,如:Zuul,我们需要引入的是:`sa-token-spring-boot-starter`,详细戳:[在SpringBoot环境集成](/start/example);理论上`Zuul`并不支持`Spring Boot3` +- 一种是基于Reactor模型的,如:SpringCloud Gateway、ShenYu 等等,我们需要引入的是:`sa-token-reactor-spring-boot-starter`,**并且注册全局过滤器!**,详细戳:[在WebFlux环境集成](/start/webflux-example) + +注:切不可直接在一个项目里同时引入这两个依赖,否则会造成项目无法启动 + +另外,我们需要引入Redis集成包,因为我们的网关和子服务主要通过Redis来同步数据 + + + +``` xml + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + +``` + +``` gradle +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' +``` + + +详细参考:[集成 Redis](/up/integ-redis) + + + + + + + diff --git a/sa-token-doc/micro/same-token.md b/sa-token-doc/micro/same-token.md new file mode 100644 index 000000000..ff0c089b7 --- /dev/null +++ b/sa-token-doc/micro/same-token.md @@ -0,0 +1,277 @@ +# 微服务 - 内部服务外网隔离 + +--- + + +### 一、需求场景 + +我们的子服务一般不能通过外网直接访问,必须通过网关转发才是一个合法的请求,这种子服务与外网的隔离一般分为两种: + +1. 物理隔离:子服务部署在指定的内网环境中,只有网关对外网开放 +2. 逻辑隔离:子服务与网关同时暴露在外网,但是子服务会有一个权限拦截层保证只接受网关发送来的请求,绕过网关直接访问子服务会被提示:无效请求 + +这种鉴权需求牵扯到两个环节: **`网关转发鉴权`** 、 **`服务间内部调用鉴权`** + +Sa-Token提供两种解决方案: +1. 使用 OAuth2.0 模式的凭证式,将 Client-Token 用作各个服务的身份凭证进行权限校验 +2. 使用 Same-Token 模块提供的身份校验能力,完成服务间的权限认证 + +本篇主要讲解方案二 `Same-Token` 模块的整合步骤,其鉴权流程与 OAuth2.0 类似,不过使用方式上更加简洁(希望使用方案一的同学可参考Sa-OAuth2模块,此处不再赘述) + +Same-Token_同源系统认证.svg + + +### 二、网关转发鉴权 + +##### 1、引入依赖 + +在网关处引入的依赖为(此处以 SpringCloud Gateway 为例): + + +``` xml + + + cn.dev33 + sa-token-reactor-spring-boot-starter + ${sa.top.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + +``` gradle +// Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' + +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + +在下游子服务引入的依赖为: + + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 + + +``` gradle +// Sa-Token 权限认证,在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' + +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 + + +##### 2、网关处添加Same-Token + +为网关添加全局过滤器: +``` java +/** + * 全局过滤器,为请求添加 Same-Token + */ +@Component +public class ForwardAuthFilter implements GlobalFilter { + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest newRequest = exchange + .getRequest() + .mutate() + // 为请求追加 Same-Token 参数 + .header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()) + .build(); + ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); + return chain.filter(newExchange); + } +} +``` +此过滤器会为 Request 请求头追加 `Same-Token` 参数,这个参数会被转发到子服务 + + +##### 3、在子服务里校验参数 + +在子服务添加过滤器校验参数 +``` java +/** + * Sa-Token 权限认证 配置类 + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + // 注册 Sa-Token 全局过滤器 + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + .addInclude("/**") + .addExclude("/favicon.ico") + .setAuth(obj -> { + // 校验 Same-Token 身份凭证 —— 以下两句代码可简化为:SaSameUtil.checkCurrentRequestToken(); + String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN); + SaSameUtil.checkToken(token); + }) + .setError(e -> { + return SaResult.error(e.getMessage()); + }) + ; + } +} +``` + +启动网关与子服务,访问测试: + +> [!WARNING| label:越过网关访问] +> 如果通过网关转发,可以正常访问。如果直接访问子服务会提示:`无效Same-Token:xxx` + + +### 三、服务间内部调用鉴权 + +有时候我们需要在一个服务调用另一个服务的接口,这也是需要添加`Same-Token`作为身份凭证的 + +在服务里添加 Same-Token 流程与网关类似,我们以RPC框架 `Feign` 为例: + +##### 1、首先在调用方添加 FeignInterceptor +``` java +/** + * feign拦截器, 在feign请求发出之前,加入一些操作 + */ +@Component +public class FeignInterceptor implements RequestInterceptor { + // 为 Feign 的 RPC 调用 添加请求头Same-Token + @Override + public void apply(RequestTemplate requestTemplate) { + requestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()); + + // 如果希望被调用方有会话状态,此处就还需要将 satoken 添加到请求头中 + // requestTemplate.header(StpUtil.getTokenName(), StpUtil.getTokenValue()); + } +} +``` + +##### 2、在调用接口里使用此 Interceptor +``` java +/** + * 服务调用 + */ +@FeignClient( + name = "sp-home", // 服务名称 + configuration = FeignInterceptor.class, // 请求拦截器 (⚠️ 关键代码) + fallbackFactory = SpCfgInterfaceFallback.class // 服务降级处理 + ) +public interface SpCfgInterface { + + // 获取server端指定配置信息 + @RequestMapping("/SpConfig/getConfig") + public String getConfig(@RequestParam("key")String key); + +} +``` + +被调用方的代码无需更改(按照网关转发鉴权处的代码注册全局过滤器),保持启动测试即可 + + +### 四、Same-Token 模块详解 + +Same-Token —— 专门解决同源系统互相调用时的身份认证校验,它的作用不仅局限于微服务调用场景 + +基本使用流程为:服务调用方获取 Same-Token,提交到请求中,被调用方取出 Same-Token 进行校验:如果一致则校验通过,否则拒绝服务。 + +Same-Token_同源系统认证.svg + + + + +首先我们预览一下此模块的相关API: +``` java +// 获取当前Same-Token +SaSameUtil.getToken(); + +// 判断一个Same-Token是否有效 +SaSameUtil.isValid(token); + +// 校验一个Same-Token是否有效 (如果无效则抛出异常) +SaSameUtil.checkToken(token); + +// 校验当前Request提供的Same-Token是否有效 (如果无效则抛出异常) +SaSameUtil.checkCurrentRequestToken(); + +// 刷新一次Same-Token (注意集群环境中不要多个服务重复调用) +SaSameUtil.refreshToken(); + +// 在 Request 上储存 Same-Token 时建议使用的key +SaSameUtil.SAME_TOKEN; +``` + +##### 1、疑问:这个Token保存在什么地方?有没有泄露的风险?Token为永久有效还是临时有效? +Same-Token 默认随 Sa-Token 数据一起保存在Redis中,理论上不会存在泄露的风险,每个Token默认有效期只有一天 + +##### 2、如何主动刷新Same-Token,例如:五分钟、两小时刷新一次? +Same-Token 刷新间隔越短,其安全性越高,每个Token的默认有效期为一天,在一天后再次获取会自动产生一个新的Token + +> [!WARNING| label:注意点] +> 需要注意的一点是:Same-Token默认的自刷新机制,并不能做到高并发可用,多个服务一起触发Token刷新可能会造成毫秒级的短暂服务失效,其只能适用于 项目开发阶段 或 低并发业务场景 + +因此在微服务架构下,我们需要有专门的机制主动刷新Same-Token,保证其高可用 + +例如,我们可以专门起一个服务,使用定时任务来刷新Same-Token +``` java +/** + * Same-Token,定时刷新 + */ +@Configuration +public class SaSameTokenRefreshTask { + // 从 0 分钟开始 每隔 5 分钟执行一次 Same-Token + @Scheduled(cron = "0 0/5 * * * ? ") + public void refreshToken(){ + SaSameUtil.refreshToken(); + } +} +``` + +以上的cron表达式刷新间隔可以配置为`五分钟`、`十分钟` 或 `两小时`,只要低于Same-Token的有效期(默认为一天)即可。 + +##### 3、如果网关携带token转发的请求在落到子服务的节点上时,恰好刷新了token,导致鉴权未通过怎么办? +Same-Token 模块在每次刷新 Token 时,旧 Token 会被作为次级 Token 存储起来, +只要网关携带的 Token 符合新旧 Token 其一即可通过认证,直至下一次刷新,新 Token 再次作为次级 Token 将此替换掉。 + + + + + + diff --git a/sa-token-doc/more/blog.md b/sa-token-doc/more/blog.md new file mode 100644 index 000000000..a58f1502c --- /dev/null +++ b/sa-token-doc/more/blog.md @@ -0,0 +1,401 @@ +# 框架博客 + +> 官方精品原创文章见 [Sa-Token 博客](/blog/index.html)。 +> 此页面收集 Sa-Token 相关技术文章(不限平台,按照发表日期倒序), +> 如果你也想投稿,请考虑加入:[Sa-Token 内容合作群 ](/more/content-cooperation) + +--- + +- [[ 公众号 ] 为什么越来越多人用 Sa-Token?](https://mp.weixin.qq.com/s/m-cfYtLb34heeM9W7fSUVw) (2026-8-7) + +- [[ 51CTO ] sa-token项目的日志SaLog接口他的实现以及salog配置生效](https://blog.51cto.com/lenglingx/14828743) (2026-8-6) + +- [[ CSDN ] 基于Sa-Token实现OAuth 2.0静默授权:原理、配置与生产实践](https://blog.csdn.net/weixin_42614269/article/details/163546104) (2026-8-6) + +- [[ 公众号 ] 为什么越来越多人用Sa-Token?](https://mp.weixin.qq.com/s/NAsaVb5iOuTCmK4FVIm8IQ) (2026-8-4) + +- [[ CSDN ] Sa-Token 核心原理与实战:从登录会话到微服务权限鉴权](https://blog.csdn.net/weixin_57796792/article/details/163417448) (2026-8-3) + +- [[ 公众号 ] Spring | 基于 Sa-Token 构建安全、优雅的登录认证体系](https://mp.weixin.qq.com/s/GzROn0J6yjuk208XLu2nvg) (2026-7-31) + +- [[ 公众号 ] 安全体系深度对比:Sa-Token JWT + RBAC + 数据权限 vs Spring Security + Shiro](https://mp.weixin.qq.com/s/hYG5lC0g4ndwae2cLiI5LQ) (2026-7-22) + +- [[ 公众号 ] 为什么后来我的项目,几乎都开始使用 Sa-Token?](https://mp.weixin.qq.com/s/-7swQiXqnkIpF1ydN1bAbQ) (2026-7-2) + +- [[ 公众号 ] Sa-Token JWT认证方案:Token续期、多点登录控制、权限缓存一站式实践](https://mp.weixin.qq.com/s/BlBQ2A3waEckRZNorTh__Q) (2026-7-1) + +- [[ CSDN ] SpringBoot 整合 Sa-Token 实现权限认证——轻量级替代 Shiro](https://blog.csdn.net/DoubleZ137/article/details/162438684) (2026-6-30) + +- [[ CSDN ] Sa-Token到底怎么鉴权的???(个人项目复盘)](https://blog.csdn.net/weixin_52073525/article/details/162464158) (2026-6-30) + +- [[ 公众号 ] 架构实战第5篇:四道防线守护每个接口-鹿鲸项目的Sa-Token权限架构解析](https://mp.weixin.qq.com/s/XRKkluee_LQK2D4OwC21Gg) (2026-6-26) + +- [[ 公众号 ] Sa-Token:Java 权限认证框架,让鉴权简单优雅](https://mp.weixin.qq.com/s/WvXPVyhx27zO0iJwRfJO_w) (2026-6-19) + +- [[ CSDN ] 一行代码登录,微秒级鉴权:Sa-Token + Redis Session 实践](https://blog.csdn.net/LI_124301012/article/details/162080845) (2026-6-18) + +- [[ 公众号 ] 若依AI学习第10天:Sa-Token 5分钟入门实战](https://mp.weixin.qq.com/s/Fes4WbdJ8wUxQ7dpwVEnng) (2026-6-17) + +- [[ 公众号 ] 9.若依AI学习第9天:Sa-Token 权限认证](https://mp.weixin.qq.com/s/p3ot7Vy7OeHe075STJqX7g) (2026-6-15) + +- [[ 公众号 ] SpringBoot 携手 Sa-Token:极简配置,开启权限认证的丝滑体验](https://mp.weixin.qq.com/s/QpumBtc2L4GWyZNkSlPLIQ) (2026-6-8) + +- [[ 百家号 ] Sa-Token把登录、SSO、OAuth2、网关鉴权都塞进几行代码,省下的不是配置,是后面三年的维护命](https://baijiahao.baidu.com/s?id=1867417301403436037) (2026-6-8) + +- [[ 公众号 ] 告别Shiro与Spring Security?Sa-Token:一个足以颠覆Java权限生态的极简框架](https://mp.weixin.qq.com/s/o_HOYldBh9zfL6_tAvantQ) (2026-6-7) + +- [[ CSDN ] Sa-Token:一行代码搞定登录,Java鉴权框架还能这么简单](https://blog.csdn.net/chenchuang0128/article/details/161693721) (2026-6-4) + +- [[ 公众号 ] Sa-Token - 一站式 Java 权限认证框架](https://mp.weixin.qq.com/s/u8gbFJ4rSzaQiC1oeo5bqA) (2026-6-3) + +- [[ CSDN ] Spring Boot脚手架集成Sa-Token实现生产级RBAC权限管理](https://blog.csdn.net/qq_39818325/article/details/161524723) (2026-6-1) + +- [[ 公众号 ] Sa-Token:48,800+ Star 的背后让鉴权变得简单优雅](https://mp.weixin.qq.com/s/1w9Ac-EGfn8dU7pb45YCdQ) (2026-5-27) + +- [[ 公众号 ] 还在受 Spring Security 折磨?1 分钟认识国产鉴权神器 Sa-Token](https://mp.weixin.qq.com/s/Cn7tT_O8KMHmQ_RVwSZ6zg) (2026-5-22) + +- [[ CSDN ] ChaperTwo-整合 SaToken 实现 JWT 登录功能](https://blog.csdn.net/2503_91700196/article/details/161235399) (2026-5-19) + +- [[ CSDN ] Spring Boot + Sa-Token 实时聊天系统:用户注册流程源码深度剖析](https://blog.csdn.net/2401_89707442/article/details/161193882) (2026-5-18) + +- [[ 公众号 ] 太强了!Sa-Token 的 NodeJS 版本!](https://mp.weixin.qq.com/s/Kc0ThtQg0MlPg22ef1woUw) (2026-5-18) + +- [[ 公众号 ] Sa-Token:18.8K Star权限认证框架,登录更简单](https://mp.weixin.qq.com/s/1hnWYSmRr7kumJez7jF2FA) (2026-5-15) + +- [[ 公众号 ] 9 大框架统一接入,sa-token-rust 为什么值得关注](https://mp.weixin.qq.com/s/kLx5ff2DNIACXnSclv00ZA) (2026-5-7) + +- [[ CSDN ] Sa-Token基础篇](https://blog.csdn.net/2401_87882047/article/details/160829277) (2026-5-6) + +- [[ 公众号 ] Sa-Token:一行代码搞定登录,Java鉴权框架还能这么简单](https://mp.weixin.qq.com/s/gB89MRuBOaElBdUXtP8OjQ) (2026-5-4) + +- [[ 公众号 ] Sa-Token 核心流程](https://mp.weixin.qq.com/s/Y_8neMzyUg5danEU3Y1PdQ) (2026-5-1) + +- [[ 51cto ] SpringBoot结合Sa-token实现权限认证(3)](https://blog.51cto.com/u_16099187/14575721) (2026-4-29) + +- [[ 掘金 ] Spring @Async 异步无法获取当前登录用户?Sa-Token 1.34.0 终极踩坑解决方案](https://juejin.cn/post/7633614065114316851) (2026-4-28) + +- [[ 掘金 ] 把 Sa-Token 搬到 NestJS 生态:xlt-token 1.0 的几个设计取舍](https://juejin.cn/post/7632620169131343914) (2026-4-26) + +- [[ 公众号 ] Sa-Token 的 switchTo,不是 "管理员冒充用户"](https://mp.weixin.qq.com/s/Uqh1sDhjnJIBEkNwoxqA-g) (2026-4-24) + +- [[ 公众号 ] 权限框架怎么选?SpringSecurity、Shiro、Sa-Token 全面对比](https://mp.weixin.qq.com/s/ilOjwq92KK3FIOHUwjKOLA) (2026-4-24) + +- [[ CSDN ] 别再只写@SaCheckPermission了!手把手教你自定义Sa-Token权限校验逻辑(附源码)](https://blog.csdn.net/weixin_42568801/article/details/160469012) (2026-4-24) + +- [[ 公众号 ] Sa-Token权限框架](https://mp.weixin.qq.com/s/X-Ew5ig_nQUoGw5uPSPLgw) (2026-4-23) + +- [[ 掘金 ] Sa-Token 使用方法及实现原理](https://juejin.cn/post/7630724767640928290) (2026-4-20) + +- [[ CSDN ] 【RuoYi-Vue-Plus】Sa-Token 拦截器升级实战:从源码拆解 SaInterceptor 的设计哲学与性能优化](https://blog.csdn.net/weixin_42568801/article/details/160306227) (2026-4-19) + +- [[ 公众号 ] SpringBoot + JWT + Sa-Token:认证鉴权双框架对比,安全登录与权限控制最佳实践](https://mp.weixin.qq.com/s/NoO5wd-B1Jl4exlLYX8y2g) (2026-4-18) + +- [[ 公众号 ] 别再盲目上 SSO 了!Sa-Token 多端跳转的「作弊级」解法](https://mp.weixin.qq.com/s/x828OMUIjQMf7RqiwGIgcw) (2026-4-17) + +- [[ CSDN ] Sa-Token vs Spring Security权限认证对比:Ruoyi-vue-plus项目该选哪个?](https://blog.csdn.net/weixin_42563415/article/details/160171603) (2026-4-15) + +- [[ 掘金 ] 在nest.js中我想把Java的Sa-Token搬来](https://juejin.cn/post/7628910305003946011) (2026-4-15) + +- [[ 公众号 ] 真相:别再复制粘贴了!Sa-Token 全链路权限架构,一次落地所有项目](https://mp.weixin.qq.com/s/3Msd1UNA0GgY81fioDDahQ) (2026-4-14) + +- [[ 公众号 ] 基于 Sa-Token 搭建大模型开放平台:生产级 API Key 认证与限流实战](https://mp.weixin.qq.com/s/B9QttdYnkN3Au8cbhuo5Vw) (2026-4-14) + +- [[ CSDN ] JWT与Sa-Token认证方案对比](https://blog.csdn.net/t194978/article/details/160158394) (2026-4-14) + +- [[ 公众号 ] 告别SpringSecurity!Sa-Token+Gateway+Nacos极简鉴权实战](https://mp.weixin.qq.com/s/-mKBH70Z0zDYR3JT9YN7Qw) (2026-4-13) + +- [[ 公众号 ] Sa-Token + WebSocket 鉴权:别让"长连接"成了安全漏洞!](https://mp.weixin.qq.com/s/m34rq63GHgZMxvzkJYRy2w) (2026-4-10) + +- [[ 公众号 ] 使用Sa-Token解决WebSocket握手身份认证](https://mp.weixin.qq.com/s/ieCtD3-SW748k5Ri-iSmkg) (2026-4-10) + +- [[ 公众号 ] 苦 Spring Security 久矣?这款霸榜 Gitee 的权限框架,把优雅做到了极致](https://mp.weixin.qq.com/s/yJDy7o6ZnNLEvn_9hHah4Q) (2026-4-10) + +- [[ 公众号 ] Sa-Token 真的好用:告别 Spring Security 的"配置地狱",真香!](https://mp.weixin.qq.com/s/-uuj2CxGLRtwg55RxwKwDA) (2026-4-9) + +- [[ 公众号 ] 别只会 MD5!API 签名这几种才是大厂标配(Sa-Token 实战版)](https://mp.weixin.qq.com/s/hxMQCJrraGLaZk8JpU_XGA) (2026-4-9) + +- [[ 公众号 ] 基于 Sa-Token 搭建大模型开放平台:生产级 API Key 认证与限流实战](https://mp.weixin.qq.com/s/AXiOnEI_mbhOCtv3ZezcQQ) (2026-4-7) + +- [[ 公众号 ] 一行代码搞定鉴权!1.8万Star的Sa-Token,Java权限认证框架](https://mp.weixin.qq.com/s/nFrVbDcRMVOAuxNMmqyWkw) (2026-4-6) + +- [[ 公众号 ] 一文彻底搞懂 Sa-Token safe-auth:它不是"再登录一次",而是给危险接口加一道门](https://mp.weixin.qq.com/s/zdhZh9LPwcF0wXF7bFzNHA) (2026-4-3) + +- [[ 公众号 ] Sa-Token:一行代码搞定登录权限、优雅“踢人”,有github源码,开箱即可踢,优雅!](https://mp.weixin.qq.com/s/M6tXTCsHZr-ONwfn3v9JeA) (2026-4-2) + +- [[ 公众号 ] 登录不是接口调用,而是一套账号生命周期策略](https://mp.weixin.qq.com/s/pcGcTmU6zu3GDsnpNXZ88A) (2026-3-27) + +- [[ 公众号 ] Sa-Token:轻量级 Java 权限认证框架推荐](https://mp.weixin.qq.com/s/qTLYmx2otKcd46j_0iBP1A) (2026-3-27) + +- [[ 公众号 ] Sa-Token 记住我机制,讲透 Cookie 和 Token](https://mp.weixin.qq.com/s/dMekJRRRhOgGPLpEvPmh1g) (2026-3-26) + +- [[ 公众号 ] Sa-Token + JWT:强强联合,打造前后端分离认证的最佳实践](https://mp.weixin.qq.com/s/NMmMcHRxPEthV6FRBlDxDg) (2026-3-26) + +- [[ 公众号 ] 多端登录不是功能,是账号策略](https://mp.weixin.qq.com/s/HNWo8qoE-RRiylnMaBMqeg) (2026-3-25) + +- [[ 公众号 ] 16、Spring Boot 3.2.5 整合 Sa-Token 1.44.0 完整教程](https://mp.weixin.qq.com/s/A_LqOzTHJcZGNqTBxT8hKA) (2026-3-25) + +- [[ 公众号 ] 多租户 SaaS 别再乱答了!数据隔离就这三层,有github代码](https://mp.weixin.qq.com/s/AcoEOB_RzgY227RwaulHsA) (2026-3-25) + +- [[ 公众号 ] 七年磨一剑,一款开源、免费、一站式 java 权限认证框架,让鉴权变得简单](https://mp.weixin.qq.com/s/YQW4r5BmfHjUY-TBm9FDng) (2026-3-24) + +- [[ 公众号 ] 告别繁琐配置:Sa-Token 如何让 Java 鉴权回归"一行代码"的优雅](https://mp.weixin.qq.com/s/sWw7JU91Bk8gHf0Z3CHfdQ) (2026-3-22) + +- [[ 公众号 ] Sa-Token 整合用户认证授权(完整实战指南)](https://mp.weixin.qq.com/s/8yiu-ij0Vk8dAWw3qxAGpg) (2026-3-22) + +- [[ 公众号 ] 微服务鉴权不用愁!Sa-Token 网关统一鉴权 + 分布式Session 实战+源码](https://mp.weixin.qq.com/s/9WW67naQQTRtTCb_cHg8LA) (2026-3-21) + +- [[ 公众号 ] 告别Spring Security!Sa-Token一行代码搞定登录+权限(有料有源码)](https://mp.weixin.qq.com/s/ciTXQTSCeihR6axa8MxiJg) (2026-3-20) + +- [[ 掘金 ] 基于 Spring Cloud Gateway + Sa-Token 的架构为例,Token 异常的执行链路](https://juejin.cn/post/7619201247540166675) (2026-3-20) + +- [[ 公众号 ] 面试官:你能手写一个 SSO 单点登录流程吗?](https://mp.weixin.qq.com/s/abhYTjpshCQo4Tbyv71GYw) (2026-3-19) + +- [[ 公众号 ] 面试官:"你们项目用的什么权限框架,为什么这么选?",用了Sa-Token,上手快开箱即用。。。](https://mp.weixin.qq.com/s/kojBf_Qo5a6giBlU9Y18vg) (2026-3-18) + +- [[ PHP中文网 ] 怎样在Java中搭建Sa-Token权限认证环境_Java安全新选型](https://www.php.cn/faq/2211433.html) (2026-3-16) + +- [[ 公众号 ] Sa-Token 多端会话管控解析:构建统一身份认证秩序,解决多端登录难题](https://mp.weixin.qq.com/s/mpoOxgz4jDPcFBoGHVhoiw) (2026-3-11) + +- [[ 公众号 ] 诈尸级更新?停更9个月,Sa-Token 1.45.0 携 Spring Boot 4.x 适配回来了!](https://mp.weixin.qq.com/s/Wj83wEbbYoUQU0M7IgfVnA) (2026-3-10) + +- [[ 公众号 ] Kaleido-AI教程(九)基于Sa-Token实现多账户认证体系](https://mp.weixin.qq.com/s/ihW1sM8DvQJS-1ZKhFr9KQ) (2026-3-8) + +- [[ 公众号 ] Sa-Token 的 token-prefix 和 token-style,到底谁管谁?](https://mp.weixin.qq.com/s/1_PaPxvEui-16Is6Urw3CA) (2026-3-7) + +- [[ 公众号 ] JAVA:Spring Boot3 集成 Sa-Token 轻量级权限认证](https://mp.weixin.qq.com/s/cjk9ad9tj397Bd0hAyCsyA) (2026-3-6) + +- [[ 公众号 ] Sa-Token(二)之从入门到实战——一篇文章助你真正了解掌握Sa-Token](https://mp.weixin.qq.com/s/9-CLoSJZBrfTF2tul-M8Ww) (2026-3-6) + +- [[ 公众号 ] 不用 Cookie,鉴权照样稳](https://mp.weixin.qq.com/s/k8DC-GiYYPbofGDD_4jlPQ) (2026-3-6) + +- [[ CSDN ] Sa-Token登录策略全解析:从单地登录到同端互斥,这些配置项你都知道吗?](https://blog.csdn.net/weixin_29284885/article/details/158675177) (2026-3-5) + +- [[ 公众号 ] 18.6k vs 9.5k Star!若依认证该选谁?](https://mp.weixin.qq.com/s/BVFWWPiYloa1nuZ4MZ8XZg) (2026-3-4) + +- [[ 公众号 ] SaToken 支持使用 JSON body验签](https://mp.weixin.qq.com/s/0Rr9PuDBUJwaEolhtJxBwA) (2026-3-3) + +- [[ 公众号 ] 明明接了 Redis,重启后会话还是丢了?](https://mp.weixin.qq.com/s/-O1qwR0I30wngurGo-qOuw) (2026-3-3) + +- [[ CSDN ] JAVA:Spring Boot3 集成 Sa-Token 轻量级权限认证](https://shdxhl.blog.csdn.net/article/details/157695326) (2026-2-27) + +- [[ 公众号 ] 苦 Spring Security 久矣?这款霸榜 Gitee 的权限框架,把优雅做到了极致](https://mp.weixin.qq.com/s/CzBPkeV6jWZ7mpA_6JH09g) (2026-2-27) + +- [[ 公众号 ] 架构师推荐开源项目:轻量级Java权限认证框架!](https://mp.weixin.qq.com/s/OZtTqYIZNU2l_yyKFvbd9A) (2026-2-24) + +- [[ 公众号 ] Sa-Token(一)之简介及入门:告别鉴权内耗,让每一位Java开发者都能轻松上手](https://mp.weixin.qq.com/s/HLG1PHnbfOTpC3e-tGutew) (2026-2-24) + +- [[ CSDN ] Sa-Token SSO 前后端分离实战:SpringBoot + Vue2 单点登录全流程解析](https://blog.csdn.net/weixin_29291863/article/details/158324193) (2026-2-24) + +- [[ CSDN ] RefreshToken反查踩坑记:Sa-Token 1.42.0临时令牌管理新姿势](https://blog.csdn.net/weixin_28327051/article/details/158301601) (2026-2-23) + +- [[ 公众号 ] SpringBoot3 + Sa-Token 单点登录|30分钟上手,代码可复制,新手零踩坑](https://mp.weixin.qq.com/s/2jN9HotfYttLFMzHE54XiA) (2026-2-21) + +- [[ 公众号 ] Sa-Token Session会话:三种模型彻底搞懂,不再傻傻分不清](https://mp.weixin.qq.com/s/gpqogF0QyahuqJIqhs6DUg) (2026-2-21) + +- [[ 公众号 ] SpringBoot3 + Sa-Token 双Token登录认证实战(避坑版)](https://mp.weixin.qq.com/s/LDSCSZYuUIkQA91MYXhnGQ) (2026-2-20) + +- [[ CSDN ] SaToken实战:5分钟搞定微信小程序登录功能(附完整代码)](https://blog.csdn.net/weixin_29218509/article/details/158226233) (2026-2-20) + +- [[ 公众号 ] 告别SpringSecurity!Sa-Token+Gateway+Nacos极简鉴权实战](https://mp.weixin.qq.com/s/lFcH7XyLRtaNH6q4-TzHbQ) (2026-2-19) + +- [[ 公众号 ] SpringSecurity、Shiro和Sa-Token,哪个更好?](https://mp.weixin.qq.com/s/BEvk1ohFntL7iorDEvqIpg) (2026-2-18) + +- [[ CSDN ] Sa-Token 1.42.0实战:5分钟搞定API Key权限隔离与TOTP双因子认证](https://blog.csdn.net/weixin_29271053/article/details/158175327) (2026-2-18) + +- [[ CSDN ] SaToken权限注解全解析:@SaCheckPermission和@SaCheckRole的20种实战用法](https://blog.csdn.net/weixin_28454475/article/details/158163447) (2026-2-18) + +- [[ 公众号 ] 5 分钟上手 Sa-Token:Spring Boot 权限认证从未如此简单](https://mp.weixin.qq.com/s/2iRMPQdfBEgqSgeld3crXw) (2026-2-17) + +- [[ 公众号 ] SpringSecurity、Shiro和Sa-Token,哪个更好?](https://mp.weixin.qq.com/s/QdR1tyIXN8GvWbhN48XTjA) (2026-2-13) + +- [[ 公众号 ] sa-token前后端分离集成redis与jwt基础案例](https://mp.weixin.qq.com/s/c1UYdxuRjWudGnpQa_A72w) (2026-2-11) + +- [[ 公众号 ] Sa-Token(一)之简介及入门:告别鉴权内耗,让每一位Java开发者都能轻松上手](https://mp.weixin.qq.com/s/JLQSMAgqK1U0vtrdtRsKlA) (2026-2-11) + +- [[ 公众号 ] Sa-Token 的极简设计哲学](https://mp.weixin.qq.com/s/Yr48InNXxaVkNfVRbllO7w) (2026-2-10) + +- [[ 公众号 ] 还在用 @PreAuthorize?聊聊我切换到 Sa-Token 路由拦截后的真实体感](https://mp.weixin.qq.com/s/VMtSZDC1AFquCKRakTxytw) (2026-2-10) + +- [[ 公众号 ] Sa-Token 实战进阶:从“能用”到“好用”的企业级鉴权方案](https://mp.weixin.qq.com/s/hOY37lIxw01aPvjmdTf6VQ) (2026-2-9) + +- [[ 公众号 ] Sa-Token:把“登录/鉴权/踢人/SSO/OAuth2”做成一套顺手的 Java 权限方案(附 Spring Boot 快速上手)](https://mp.weixin.qq.com/s/zT8iRNuFfOEqDZfhVFy15w) (2026-2-9) + +- [[ 公众号 ] 开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅!](https://mp.weixin.qq.com/s/FLDwIXHQoa6V2nKPs1r4cw) (2026-2-9) + +- [[ 公众号 ] Sa-Token 注解鉴权](https://mp.weixin.qq.com/s/72oLlgj-x8oetUpIJhR02A) (2026-2-9) + +- [[ 公众号 ] 用户投诉账号异常登录,CTO 让我 5 分钟内解决](https://mp.weixin.qq.com/s/pfMSZLxmDKIVYoq6UGUj1Q) (2026-2-4) + +- [[ 公众号 ] Sa-Token实战:SpringBoot与微服务权限认证极简方案](https://mp.weixin.qq.com/s/yNma6FhHvPLNHUqYFkdPCg) (2026-1-25) + +- [[ 公众号 ] Sa-Token过期机制](https://mp.weixin.qq.com/s/gFQ8YJT1yg5pTm8Z3uDZHw) (2026-1-22) + +- [[ 公众号 ] 别再写死权限了!SpringBoot + Sa-Token 实现 RBAC 的最佳姿势](https://mp.weixin.qq.com/s/ZwzAInOoqiQ2h0ogWaOoQg) (2026-1-14) + +- [[ 公众号 ] 集成sa-token跨域正确姿势](https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g) (2026-1-9) + +- [[ 公众号 ] 后端开发必看:最简单的 Java 登录认证框架 Sa-Token 上手指南](https://mp.weixin.qq.com/s/Kk9HEVAG43-FPikiMZKWJw) (2026-1-8) + +- [[ 公众号 ] Sa-Token:一站式权限认证解决方案的实战指南](https://mp.weixin.qq.com/s/FVkn-3CqWT8dNM6a5kD2oA) (2025-12-30) + +- [[ 公众号 ] SpringSecurity、Shiro和Sa-Token,哪个更好?](https://mp.weixin.qq.com/s/gtQ7_n9cPJd2-i_Qm-jk1A) (2025-12-28) + +- [[ 公众号 ] SpringBoot + JWT + Sa-Token:认证鉴权双框架对比,安全登录与权限控制最佳实践](https://mp.weixin.qq.com/s/SDPKdmxtwb4MbOHfg-bF8Q) (2025-12-27) + +- [[ 公众号 ] 一行代码搞定认证](https://mp.weixin.qq.com/s/UaAw1WdVtumA44SxjFW3yg) (2025-12-24) + +- [[ 掘金 ] Netty + Sa-Token 实现 WebSocket 握手认证](https://juejin.cn/post/7585490245006950406) (2025-12-21) + +- [[ 公众号 ] 《第27节》SpringBoot+SaToken实现鉴权功能](https://mp.weixin.qq.com/s/mY_jrQL1dG2yis51rX2i9w) (2025-12-16) + +- [[ 公众号 ] 告别 Spring Security!Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/s36bdkhi5ACGN2j7hLiD7w) (2025-12-15) + +- [[ 公众号 ] 《第26节》SpringBoot3+SaToken实现用户注册登录功能](https://mp.weixin.qq.com/s/u7nVa0PJcFWx-9xKq1RNhA) (2025-12-13) + +- [[ 公众号 ] 《第25节》SpringBoot3之集成sa-token权限认证框架](https://mp.weixin.qq.com/s/sxgzLqiKCf4_fqWAxN8Ozg) (2025-12-12) + +- [[ 掘金 ] sa-token前后端分离集成redis与jwt基础案例](https://juejin.cn/post/7576843726011645978) (2025-11-26) + +- [[ 公众号 ] Sa-Token 1.44.0:Java权限认证的“轻量级王者”,让鉴权优雅如诗](https://mp.weixin.qq.com/s/UprusTkp9LZOH9TJDTKJRw) (2025-11-20) + +- [[ 公众号 ] sa-token-rust 项目:高性能的 Rust 认证授权框架](https://mp.weixin.qq.com/s/jlQAX1K1M64DtUgrHrDX1A) (2025-11-17) + +- [[ 知乎 ] Sa-Token 与 Spring Security 对比分析](https://zhuanlan.zhihu.com/p/1975162115833754745) (2025-11-12) + +- [[ 公众号 ] 不会吧,居然还有人没有用过?全网爆火的权限校验框架 Sa-Token 超详细教程它来了](https://mp.weixin.qq.com/s/kNYq0MmlYB_0tRWI-HvU1g) (2025-11-6) + +- [[ 公众号 ] 若依框架集成 Sa-Token 实现权限认证与会话管理](https://mp.weixin.qq.com/s/JAgL0hxcPeP0E4OW4oy8Yg) (2025-10-21) + +- [[ 公众号 ] 太强了!Sa-Token 的 Go 版本!](https://mp.weixin.qq.com/s/idfrMeAMY2CeGAZGY9csmw) (2025-10-20) + +- [[ 公众号 ] 从 0 到 1!Sa-Token 与 SpringBoot 整合教程,让鉴权优雅到飞起](https://mp.weixin.qq.com/s/PudodYBsIQODdfeweo39fQ) (2025-10-16) + +- [[ 公众号 ] 太强了!Sa-Token 的 rust 版本!](https://mp.weixin.qq.com/s/CveVq368Dz5Xw-a2nT3YDw) (2025-10-12) + +- [[ 公众号 ] 功能最全的Java权限认证框架](https://mp.weixin.qq.com/s/fO5Mm1UIN8oDOwvbq-sQTw) (2025-10-10) + +- [[ 公众号 ] 一篇搞定!SpringBoot 搭建超安全 Sa-Token 登录鉴权系统](https://mp.weixin.qq.com/s/5fbrNS6jMpuViPPO8z_kMw) (2025-10-3) + +- [[ 公众号 ] 《Spring Cloud Gateway 从入门到实战》第4篇:安全与认证 —— 基于 Sa-Token 的网关统一鉴权方案](https://mp.weixin.qq.com/s/qeWQufIDNtGPyJ0b7yF4vQ) (2025-9-29) + +- [[ 公众号 ] SpringBoot整合Sa-Token实现认证与鉴权](https://mp.weixin.qq.com/s/l4OjqdeNpXjMyFCSr3PuWw) (2025-9-25) + +- [[ 公众号 ] Spring Gateway、Sa-Token、Nacos 认证/鉴权方案](https://mp.weixin.qq.com/s/JpXtI75eANwkRAppQZJG9Q) (2025-9-17) + +- [[ 公众号 ] 告别 Spring Security!Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/hlBH1H6vX-KIlQGmeY8bIg) (2025-9-15) + +- [[ 公众号 ] Spring Gateway、Sa-Token、Nacos 认证/鉴权方案,yyds!](https://mp.weixin.qq.com/s/OYsxjEmLfkxH0NqZidb4fA) (2025-9-10) + +- [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.4 Sa-Token高级特性实现](https://mp.weixin.qq.com/s/0Fex83DyngC-mxIu346X1Q) (2025-8-30) + +- [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.3 权限控制与注解使用](https://mp.weixin.qq.com/s/f4iVDeIAZ-nixqR6BNtUfg) (2025-8-30) + +- [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.2 登录认证机制详解](https://mp.weixin.qq.com/s/gu5kT93WjEauut7xoXmuag) (2025-8-29) + +- [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.1 Sa-Token框架基础](https://mp.weixin.qq.com/s/w8c5fvaap7ipMu2OdXNZng) (2025-8-29) + +- [[ 公众号 ] 告别 Spring Security!Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/5nmEDAsFgEWk-Ymn_pE74w) (2025-8-22) + +- [[ 公众号 ] 搭建基于sa-token 的网关权限管理系统](https://mp.weixin.qq.com/s/LM6g3QaklSHpaVJXUz5Gvg) (2025-7-19) + +- [[ 公众号 ] 别再被 Spring Security 和 Shiro 劝退了!这款国产 Java 权限框架真香!](https://mp.weixin.qq.com/s/2C0WSlM8zpjqQDtgv59Kaw) (2025-7-1) + +- [[ 公众号 ] 一文精通Java集成Sa-Token实现SSO单点登录](https://mp.weixin.qq.com/s/-fex5XFm4wmTmuzZCtUiRw) (2025-5-22) + +- [[ 公众号 ] 47.8k star,一款接私活神器,10分钟搞定企业级鉴权!](https://mp.weixin.qq.com/s/KRz3-h6etPaKOwHN4Xz7qg) (2025-5-15) + +- [[ 公众号 ] Sa-Token:17.5k Star!轻量级Java权限认证框架,登录鉴权超简单](https://mp.weixin.qq.com/s/akPTgU8sQkwWmXm4GNSyyQ) (2025-5-11) + +- [[ 公众号 ] SaToken-微服务认证与授权](https://mp.weixin.qq.com/s/9WYg6iLDST7YDSSsCt1NxQ) (2025-4-3) + +- [[ 公众号 ] SpringBoot 整合 Sa-Token 快速实现 API 接口签名安全校验](https://mp.weixin.qq.com/s/2NS3axN1CbRYULHrv5Du2w) (2025-3-20) + +- [[ 公众号 ] SaToken 简化开发的身份认证与权限管理框架](https://mp.weixin.qq.com/s/Yxsswl4Zn8244j4XeV8_VA) (2025-1-24) + +- [[ 公众号 ] 使用 Sa-Token 平替 Spring Security,告别繁琐的认证与鉴权!](https://mp.weixin.qq.com/s/whKQPm09ApzkVBjlSwO2Tg) (2025-1-15) + +- [[ 公众号 ] sa-token之@SaIgnore注解失效的真正原因及正确姿势](https://mp.weixin.qq.com/s/c6eckHp2M4oz2x3Hea6pGg) (2025-1-14) + +- [[ 公众号 ] SpringBoot3.x+Vue3+Sa-Token实现登录认证](https://mp.weixin.qq.com/s/0GkDoOYW8KKxTfzV83J6UA) (2024-12-27) + +- [[ 公众号 ] 万字雄文:一次说清基于Sa-Token和MaxKey的统一认证中心实现](https://mp.weixin.qq.com/s/Gl2K47F9I6-Il-AieWLplw) (2024-11-15) + +- [[ 公众号 ] 集成sa-token前后端分离部署配置corsFliter解决跨域失效的真正原因](https://mp.weixin.qq.com/s/bSS4vmKlKM7ov_CUkjxkBg) (2024-7-8) + +- [[ 公众号 ] sa-token前后端分离解决跨域的正确姿势](https://mp.weixin.qq.com/s/96WbWL28T5_-xzyCfJ7Stg) (2024-7-6) + +- [[ 公众号 ] 集成sa-token实现登录和RBAC权限控制](https://mp.weixin.qq.com/s/SREjXoyL9s1JfddQnU38yA) (2024-4-16) + +- [[ CSDN ] springboot整合Sa-Token实现登录认证和权限校验(万字长文)](https://blog.csdn.net/2301_78646673/article/details/136008153) (2024-3-31) + +- [[ CSDN ] 【Sa-Token】9、Sa-Token实现在线用户管理功能](https://blog.csdn.net/qq_40065776/article/details/132180932) (2023-8-9) + +- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 31 - Sa-Token(五)登录验证拦截器之 Token 有效期及其续签(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/126071871) (2022-7-30) + +- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 29 - Sa-Token(四)V1.30.0 登录流程分析(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/125659797) (2022-7-7) + +- [[ 掘金 ] sa-token过期后WebSocket提示过期](https://juejin.cn/post/7103446095987998733) (2022-5-30) + +- [[ 掘金 ] Sa-Token 单点登录 SSO模式二 URL重定向传播会话示例](https://juejin.cn/post/7102733249088077854) (2022-5-28) + +- [[ 掘金 ] SaToken技术分享](https://juejin.cn/post/7097967875670933535) (2022-5-15) + +- [[ CSDN ] 使用sa-token 进行权限控制](https://blog.csdn.net/u012389318/article/details/124098705) (2022-4-13) + +- [[ 掘金 ] SpringMVC配置sa-Token](https://juejin.cn/post/7081471627766005790) (2022-4-1) + +- [[ CSDN ] 【SpringBoot】59、SpringBoot使用Sa-Token-Quick-Login插件快速登录认证](https://lizhou.blog.csdn.net/article/details/123571910) (2022-3-30) + +- [[ CSDN ] 【Sa-Token】1、Sa-Token实现登录功能](https://lizhou.blog.csdn.net/article/details/119301185) (2022-3-30) + +- [[ 掘金 ] 【SpringCloud-Alibaba系列教程】13.gateway网关结合Sa-token进行登录鉴权](https://juejin.cn/post/7070805258296885285) (2022-3-3) + +- [[ CSDN ] Sa-Token的Token有效期和临时有效期的区别](https://blog.csdn.net/ControlDemo/article/details/123177825) (2022-2-28) + +- [[ 掘金 ] Spring Cloud Gateway 集成Sa-Token](https://juejin.cn/post/7069748160087719967) (2022-2-28) + +- [[ 掘金 ] Java轻量级权限认证框架 Sa-Token 初体验](https://juejin.cn/post/7068105371839102983) (2022-2-24) + +- [[ CSDN ] Sa-Token获取当前所有可用Token](https://blog.csdn.net/ControlDemo/article/details/122940634) (2022-2-15) + +- [[ 掘金 ] 使用 Sa-Token 解决 WebSocket 握手身份认证](https://juejin.cn/post/7064232762664255525) (2022-2-14) + +- [[ CSDN ] sa-token配置路由拦截放行Swagger路径](https://blog.csdn.net/ControlDemo/article/details/122885782) (2022-2-11) + +- [[ CSDN ] sa-token 多端登录思路和遇到的坑](https://blog.csdn.net/ControlDemo/article/details/122428512) (2022-1-28) + +- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 13 - Sa-Token(三)退出登录流程(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/122691698) (2022-1-25) + +- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 12 - Sa-Token(二)通过注解校验用户权限(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/122526722) (2022-1-16) + +- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 11 - 集成 Sa-Token 实现登录认证流程(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/122480703) (2022-1-13) + +- [[ 掘金 ] Springboot插件集成(三)-权限认证插件sa-token](https://juejin.cn/post/7051872914458542093) (2022-1-11) + +- [[ CSDN ] Sa-token简单介绍和基本使用](https://blog.csdn.net/weixin_43967582/article/details/122075950) (2021-12-21) + +- [[ CSDN ] sa-token使用(源码解析 + 万字)](https://blog.csdn.net/weixin_39570751/article/details/121291274) (2021-11-12) + +- [[ 公众号 ] 还在用Spring Security?推荐你一款使用简单、功能强大的权限认证框架](https://mp.weixin.qq.com/s/L2KOgwJcXCxrSAV8bPJsJQ) (2021-10-8) + +- [[ 公众号 ] Spring Security太复杂?试试这个轻量、强大、优雅的权限认证框架!](https://mp.weixin.qq.com/s/BWziNxRZH29F2v4Tmb5meA) (2021-9-22) + +- [[ 博客园 ] Sa-Token之注解鉴权:优雅的将鉴权与业务代码分离!](https://www.cnblogs.com/shengzhang/p/15260818.html) (2021-9-13) + +- [[ 掘金 ] 开箱即用!看看人家的微服务权限解决方案,那叫一个优雅!](https://juejin.cn/post/7003141949259513887) (2021-9-2) + +- [[ 掘金 ] 再见Spring Security!推荐一款功能强大的Java权限认证框架,用起来够优雅!](https://juejin.cn/post/7000174417846222878) (2021-8-25) + +- [[ 掘金 ] 史上功能最全的 Java 权限认证框架!](https://juejin.cn/post/6986174013647093773) (2021-7-18) + +- [[ 知乎 ] 一个项目搞定Java权限认证框架,二十多个特性开箱即用](https://zhuanlan.zhihu.com/p/390030149) (2021-7-15) + +- [[ 掘金 ] 从零搭建开发脚手架 集成认证授权 sa-token(尝鲜)](https://juejin.cn/post/6950163768533843999) (2021-4-12) + +- [[ 掘金 ] 权限认证就它了Sa-Token](https://juejin.cn/post/6938747514837434376) (2021-3-12) + +- [[ 掘金 ] sa-token之前后端分离模式下如何完成权限认证](https://juejin.cn/post/6937219472507797535) (2021-3-8) + +- [[ 掘金 ] 一个登录功能也能玩出这么多花样?sa-token带你轻松搞定多地登录、单地登录、同端互斥登录](https://juejin.cn/post/6917884159491276808) (2021-1-15) + +- [[ 掘金 ] sa-token v1.9.0 版本已发布,带来激动人心新特性:同端互斥登录](https://juejin.cn/post/6914612737020526599) (2021-1-6) diff --git a/sa-token-doc/more/common-action.md b/sa-token-doc/more/common-action.md new file mode 100644 index 000000000..46099a74d --- /dev/null +++ b/sa-token-doc/more/common-action.md @@ -0,0 +1,151 @@ +# 全局类、方法 +本篇介绍 Sa-Token 中一些常用的全局对象、类 + +--- + +### SaManager +SaManager 负责管理 Sa-Token 所有全局组件。 +``` java +SaManager.getConfig(); // 获取全局配置对象 +SaManager.setConfig(config); // 设置全局配置对象 +SaManager.getSaTokenDao(); // 获取数据持久化对象 +SaManager.setSaTokenDao(saTokenDao); // 设置数据持久化对象 +SaManager.getStpInterface(); // 获取权限认证对象 +SaManager.setStpInterface(stpInterface); // 设置权限认证对象 +SaManager.getSaTokenContext(); // 获取 SaTokenContext 上下文处理对象 +SaManager.setSaTokenContext(saTokenContext); // 设置 SaTokenContext 上下文处理对象 +SaManager.getSaTempTemplate(); // 获取临时令牌认证模块对象 +SaManager.setSaTempTemplate(saTempTemplate); // 设置临时令牌认证模块对象 +SaManager.getSaJsonTemplate(); // 获取 JSON 转换器 Bean +SaManager.setSaJsonTemplate(saJsonTemplate); // 设置 JSON 转换器 Bean +SaManager.getSaHttpTemplate(); // 获取 Http 请求处理 Bean +SaManager.setSaHttpTemplate(saHttpTemplate); // 设置 Http 请求处理 Bean +SaManager.getSaSerializerTemplate(); // 获取序列化器 Bean +SaManager.setSaSerializerTemplate(saSerializerTemplate); // 设置序列化器 Bean +SaManager.getSaSameTemplate(); // 获取 Same-Token 模块 Bean +SaManager.setSaSameTemplate(saSameTemplate); // 设置 Same-Token 模块 Bean +SaManager.getSaTotpTemplate(); // 获取 TOTP 模块 Bean +SaManager.setSaTotpTemplate(totpTemplate); // 设置 TOTP 模块 Bean +SaManager.getLog(); // 获取日志输出对象 +SaManager.setLog(log); // 设置日志输出对象 +SaManager.getStpLogic("type"); // 获取指定账号类型的 StpLogic 对象,获取不到时自动创建并返回 +SaManager.getStpLogic("type", false); // 获取指定账号类型的 StpLogic 对象,获取不到时抛出异常 +SaManager.putStpLogic(stpLogic); // 向全局集合中 put 一个 StpLogic +``` + +侦听器注册请使用 `SaTokenEventCenter.registerListener(listener)`,参数签名模块请使用 `SaSignManager.getSaSignTemplate()`(见 [参数签名](/plugin/api-sign) 章节)。 + + +### SaHolder +Sa-Token上下文持有类,通过此类快速获取当前环境的相关对象 +``` java +SaHolder.getContext(); // 获取当前请求的 SaTokenContext +SaHolder.getRequest(); // 获取当前请求的 [Request] 对象 +SaHolder.getResponse(); // 获取当前请求的 [Response] 对象 +SaHolder.getStorage(); // 获取当前请求的 [Storage] 对象 +SaHolder.getApplication(); // 获取全局 SaApplication 对象 +``` + + +### SaRouter +路由匹配工具类,详细戳:[路由拦截式鉴权](/use/route-check) + + +### SaFoxUtil +Sa-Token内部工具类,包含一些工具方法 +``` java +SaFoxUtil.printSaToken(); // 打印 Sa-Token 版本字符画 +SaFoxUtil.getRandomString(8); // 生成指定长度的随机字符串 +SaFoxUtil.isEmpty(str); // 指定字符串是否为null或者空字符串 +SaFoxUtil.isNotEmpty(str); // 指定字符串是否不是null或者空字符串 +SaFoxUtil.equals(a, b); // 比较两个对象是否相等 +SaFoxUtil.getMarking28(); // 以当前时间戳和随机int数字拼接一个随机字符串 +SaFoxUtil.formatDate(date); // 将日期格式化为yyyy-MM-dd HH:mm:ss字符串 +SaFoxUtil.searchList(dataList, prefix, keyword, start, size, sortType); // 从集合里查询数据 +SaFoxUtil.searchList(dataList, start, size, sortType); // 从集合里查询数据 +SaFoxUtil.vagueMatch(patt, str); // 字符串模糊匹配 +SaFoxUtil.getValueByType(obj, cs); // 将指定值转化为指定类型 +SaFoxUtil.joinParam(url, parameStr); // 在url上拼接上kv参数并返回 +SaFoxUtil.joinParam(url, key, value); // 在url上拼接上kv参数并返回 +SaFoxUtil.joinSharpParam(url, parameStr); // 在url上拼接锚参数 +SaFoxUtil.joinSharpParam(url, key, value); // 在url上拼接锚参数 +SaFoxUtil.arrayJoin(arr); // 将数组的所有元素使用逗号拼接在一起 +SaFoxUtil.isUrl(str); // 使用正则表达式判断一个字符串是否为URL +SaFoxUtil.encodeUrl(str); // URL编码 +SaFoxUtil.decoderUrl(str); // URL解码 +SaFoxUtil.convertStringToList(str); // 将指定字符串按照逗号分隔符转化为字符串集合 +SaFoxUtil.convertListToString(list); // 将指定集合按照逗号连接成一个字符串 +SaFoxUtil.convertStringToArray(str); // String 转 Array,按照逗号切割 +SaFoxUtil.convertArrayToString(arr); // Array 转 String,按照逗号切割 +SaFoxUtil.emptyList(); // 返回一个空集合 +SaFoxUtil.toList(... strs); // String 数组转集合 +``` + + +### SaTokenConfigFactory +配置对象工厂类,通过此类你可以方便的根据 properties 配置文件创建一个配置对象 + +1、首先在项目根目录,创建一个配置文件:`sa-token.properties` + +``` properties +# token 名称 (同时也是 cookie 名称) +tokenName=satoken +# token 有效期(单位:秒) 默认30天,-1 代表永久有效 +timeout=2592000 +# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 +activeTimeout=-1 +# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) +isConcurrent=true +# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) +isShare=false +# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) +tokenStyle=uuid +# 是否输出操作日志 +isLog=false +``` + +2、然后使用以下代码获取配置对象 +``` java +// 设置配置文件地址 +SaTokenConfigFactory.configPath = "sa-token.properties"; + +// 获取配置信息到 config 对象 +SaTokenConfig config = SaTokenConfigFactory.createConfig(); + +// 注入到 SaManager 中 +SaManager.setConfig(config); +``` + + +### SpringMVCUtil +SpringMVC操作的工具类,位于包:`sa-token-spring-boot-starter` +``` java +SpringMVCUtil.getRequest(); // 获取本次请求的 request 对象 +SpringMVCUtil.getResponse(); // 获取本次请求的 response 对象 +SpringMVCUtil.isWeb(); // 判断当前是否处于 Web 上下文中 +``` + + +### SaReactorHolder & SaReactorSyncHolder +Sa-Token集成Reactor时的 ServerWebExchange 工具类,位于包:`sa-token-reactor-spring-boot-starter` +``` java +// 异步方式获取 ServerWebExchange 对象 +SaReactorHolder.getMonoExchange().map(e -> { + System.out.println(e); + return e; +}); + +// 在 Mono 链中调用 Sa-Token 同步 API(返回 Mono,Controller 中需 return) +return SaReactorHolder.sync(() -> { + StpUtil.checkLogin(); + return SaResult.ok(); +}); + +// 同步方式(WebFilter 等场景) +SaReactorSyncHolder.setContext(exchange); // 写入当前请求的 ServerWebExchange +SaReactorSyncHolder.getExchange(); // 获取当前请求的 ServerWebExchange +SaReactorSyncHolder.clearContext(); // 清除当前上下文 +SaReactorSyncHolder.setContext(exchange, () -> {}); // 在指定 exchange 上下文中执行代码块并返回结果 +``` + + diff --git a/sa-token-doc/more/common-questions.md b/sa-token-doc/more/common-questions.md new file mode 100644 index 000000000..64d763aee --- /dev/null +++ b/sa-token-doc/more/common-questions.md @@ -0,0 +1,999 @@ +# 常见问题排查 +本篇整理大家在群聊里经常提问的一些问题,如有补充,欢迎提交pr + +[[toc]] + +--- + + + +## 一、常见报错 + + +### Q:报错:SaTokenContext 上下文尚未初始化 + +可能1::你在 异步上下文 / 响应式上下文 里调用了 Sa-Token 的同步 API,解决方案参考:[异步 & Mock 上下文](/fun/async--mock) + +可能2:访问了一个不存在的路由,而且 SaInterceptor 拦截器里有鉴权代码。 + +SpringBoot 默认会把 404 请求转发到 `/error`,如果恰好 SaInterceptor 里有鉴权代码,就会造成: + +写入上下文 → 进入拦截器(有上下文,可调用鉴权代码) → 发现是404 → 清除上下文 → +将请求转发至 /error -> 再次进入拦截器(无上下文,不可调用鉴权代码) → 报错:SaTokenContext 上下文尚未初始化。 + +解决方案:将 "/error" 地址排除在拦截器之外: + +``` java +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + // 鉴权代码 ... + })) + .addPathPatterns("/**") + .excludePathPatterns("/error"); + } +} +``` + +可能3:SpringMVC 项目里使用了 `SseEmitter` / `Flux` 等异步、流式返回,参考下面这条 QA。 + + +### Q:SpringMVC 下使用 SSE / Flux 流式返回时报错:SaTokenContext 上下文尚未初始化? + +常见于未引入 WebFlux、仅个别接口返回 `SseEmitter`、`Flux` 的场景,参考:[升级后 SSE 请求报 SaTokenContext 上下文尚未初始化](https://gitee.com/dromara/sa-token/issues/IC4XFE) + + +### Q:报错:NotLoginException:xxx + +这个错是说明调用接口的人没有通过登录校验,请注意:**通常情况下,异常提示语已经描述清楚了没有通过校验的具体原因:** + +**如果是:未能读取到有效Token** +- 可能1:前端没有提交 Token(最好从前端f12控制台看看请求参数里有 token 吗)。 +- 可能2:前端提交了 Token,但是参数名不对。默认参数名是 `satoken`,可通过配置文件 `sa-token.token-name: satoken` 来更改。 +- 可能3:前端提交了 Token,但是你配置了框架不读取,比如说你配置了 `is-read-header=false`(关闭header读取),此时你再从 header 里提交token,框架就无法读取到。 +- 可能4:前端提交了 Token,但是 Token前缀 不对,可参考:[自定义 Token 前缀](/up/token-prefix) +- 可能5:你的项目属于前后端分离架构,此时浏览器默认不自动提交 Cookie,参考:[前后端分离](/up/not-cookie) +- 可能6:你使用了 Nginx 反向代理,而且配置了 自定义Token名称,而且自定义的名称还带有下划线(比如 shop_token),而且还是你的项目还是从 Header头提交Token的,此时 Nginx 默认会吞掉你的下划线参数,可参考:[nginx做转发时,带下划线的header参数丢失](https://blog.csdn.net/zfw_666666/article/details/124420828) +- 可能7:可能是跨域了,导致前端提交不上 token,看看前端浏览器有没有跨域的报错。 + +**如果是:Token无效:6ad93254-b286-4ec9-9997-4430b0341ca0** +- 可能1:前端提交的 token 是乱填的,或者从别的项目拷过来的,或者多个项目一起开发时彼此的 Token 串项目了。 +- 可能2:前端提交的 token 已过期(timeout超时了)。 +- 可能3:在不集成 Redis 的情况下:颁发 token 后,项目重启了,导致 token 无效。 +- 可能4:在集成 Redis 的情况下:颁发 token 后,Redis重启了,导致 token 无效。 +- 可能5:你提交的 token 和框架读取到的 token 不一致: + - 可能5.1:比如说你配置了`is-read-header=false`(关闭header读取),然后你从header提交`token-A`,而框架从Cookie里读取`token-B`,导致鉴权不通过(框架读取顺序为`body->header->cookie`) + - 可能5.2:比如说你配置了`token-name=x-token`(自定义token名称),此时你从header提交:`satoken:token-A`(参数名没对上),然后框架从header里读取不到你提交的token,转而继续从Cookie读取到了`token-B`。 +- 可能6:在集成 jwt 插件的情况下: + - 如果使用的是 Simple 模式:情况和不集成jwt一样。 + - 如果使用的是 Mixin 和 Stateless 模式:查看这个 token 颁发后是否更改了 `jwtSecretKey` 配置项。 +- 可能7:同一账号登录数量超过12个,导致最先登录的被强制注销掉,这个值可以通过 `maxLoginCount` 来配置,默认值12,-1代表不做限制。 +- 可能8:在配置了 `is-concurrent=true, is-share=true`的情况下,你和别人共同登录了同一账号,此时对方注销了登录,由于你们使用的是同一个token,导致你这边的会话也失效了。 +- 可能9:可能是多账号鉴权的关系,在多账号模式下,如果是 `StpUserUtil.login()` 颁发的token,你从 `StpUtil.checkLogin()` 进行校验,永远都是无效token,因为账号体系没对上。 + +**如果是:Token已过期:6ad93254-b286-4ec9-9997-4430b0341ca0** +- 可能1:前端提交的 token 已被冻结(active-timeout超时了,比如配置了 active-timeout=120,但是超过了120秒没有访问接口)。 +- 可能2:集成jwt,而且使用的是 Mixin 或 Stateless 模式,而且token过期了(timeout超时了)。 + +**如果是:Token已被顶下线:6ad93254-b286-4ec9-9997-4430b0341ca0** +- 可能1:在项目配置了 `is-concurrent=false` 的前提下,这个账号又被别人登录了,导致旧登录被挤了下去。 +- 可能2:这个账号被 `StpUtil.replaced(loginId, device)` 方法强制顶下线了。 + +**如果是:Token已被踢下线:6ad93254-b286-4ec9-9997-4430b0341ca0** +- 可能1:这个账号被 `StpUtil.kickout(loginId)` 方法强制踢下线了。 + + +### Q:加了注解进行鉴权认证,不生效? +1. 注解鉴权功能默认关闭,两种方式任选其一进行打开:注册注解拦截器、集成AOP模块,参考:[注解式鉴权](/use/at-check) +2. 在Spring环境中, 如果同时配置了`WebMvcConfigurer`和`WebMvcConfigurationSupport`时, 也会导致拦截器失效. + - **常见场景**: 很多项目中会在`WebMvcConfigurationSupport`中配置`addResourceHandlers`方法开放Swagger等相关静态资源映射, 同时基于Sa-Token添加了`WebMvcConfigurer`配置`addInterceptors`方法注册注解拦截器, 这样会导致注解拦截器失效. + - **解决方案**: `WebMvcConfigurer`和`WebMvcConfigurationSupport`只选一个配置, 建议统一通过实现`WebMvcConfigurer`接口进行配置. +3. 如果以上步骤处理后仍然没有效果,加群说明一下复现步骤 + + + +### Q:我加了拦截器鉴权,但是好像没有什么效果,请求没有被拦截住? +- 可能1:这个拦截器可能没有注册成功。 +- 可能2:你访问的请求没有进入这个拦截器。 + +尝试按照下面的代码测试一下看看: + +``` java +// 注册拦截器 +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + System.out.println("--------- flag 1"); + registry.addInterceptor(new SaInterceptor(handle -> { + System.out.println("--------- flag 2,请求进入了拦截器,访问的 path 是:" + SaHolder.getRequest().getRequestPath()); + StpUtil.checkLogin(); // 登录校验,只有会话登录后才能通过这句代码 + })) + .addPathPatterns("/user/**") + .excludePathPatterns("/user/doLogin"); + } +} +``` + +在启动时 `flag 1` 被打印出来,才证明拦截器注册成功了,在访问请求时 `flag 2` 被打印出来,才证明请求进入了拦截器。 + +如果拦截器没有注册成功,则: + +- 可能1:`SaTokenConfigure` 配置类不在启动类的同包或者子包下,导致没有被 SpringBoot 扫描到。 +- 可能2:你的项目启动类上加了 `@ComponentScan("com.xxx")` 注解,导致包扫描范围不正确,请将此注解删除或移动到其它配置类上。 +- 可能3:项目属于 Maven 多模块项目,`SaTokenConfigure` 和启动类没有在一个模块,且启动类模块没有引入配置类的模块,导致加载不到。 + +如果拦截器已经注册成功,但请求没有进入拦截器: +- 可能1:你访问的 path,没有被 `.addPathPatterns("/user/**")` 拦截住,或者被 `.excludePathPatterns("/xxx/xx")` 排除掉了。 +- 可能2:你访问的是另一个项目,请把当前项目停掉,看看你的请求还能不能访问成功。 + +如果请求进入拦截器也成功了,那可能是: +- 可能1:前端访问时提交了会话 Token,且这个 Token 是有效的,通过了拦截器的代码校验。 +- 可能2:你访问的 path,和你预期不符,仔细观察一下打印出来的 path 信息,和你的预期相符吗。 + +注:以上的排查步骤,对过滤器不生效的情形一样适用。 + + +### Q:我使用拦截器鉴权时,明明排除了某个路径却仍然被拦截了? +- 可能1:你的项目可能是跨域了,先把跨域问题解决掉,参考:[解决跨域问题](/fun/cors-filter) +- 可能2:你访问的接口可能是404了,SpringBoot环境下如果访问接口404后,会被转发到`/error`,然后被再次拦截。请确保你访问的 path 有对应的 Controller 承接! +- 可能3:可能拦截器这里并没有拦截,但是又被其他地方拦截了。请先把这个拦截器给注释掉,看看还会不会拦截,如果依然拦截,那说明不是这个拦截器的锅,请仔细查看一下控制台抛出的堆栈信息,定位一下到底是哪行代码拦截住这个请求的。 +- 可能4:后端拦截的 path 未必是你前端访问的这个path(特别是经过网关转发后的path可能会有变化),建议先打印一下 path 信息,看看和你预想的是否一致,再做分析。 +``` java +@Override +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + try { + System.out.println("-------- 前端访问path:" + SaHolder.getRequest().getRequestPath()); + StpUtil.checkLogin(); + System.out.println("-------- 此 path 校验成功:" + SaHolder.getRequest().getRequestPath()); + } catch (Exception e) { + System.out.println("-------- 此 path 校验失败:" + SaHolder.getRequest().getRequestPath()); + throw e; + } + })).addPathPatterns("/**"); +} +``` +- 可能5:可能你只提交了一个请求,但是浏览器自动帮你提交了其它请求,举个例子:首次访问网站时,浏览器一般会自动提交 `/favicon.ico`,所以**你需要找出是哪个path被拦截了**,怎么找呢?用【可能4】的代码来测试找。 +- 可能6:你的项目配置了 `context-path` 上下文地址,比如 `server.servlet.context-path=/shop`,注意这个地址是不需要加在拦截器上的: +``` java +// 这是错误示例,不需要把 context-path 上下文参数写在下面的 excludePathPatterns 地址上。 +registry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin())) + .addPathPatterns("/**").excludePathPatterns("/shop/user/login"); +// 这是正确示例,无论你的 context-path 上下文配置了什么样的值,下面的 excludePathPatterns 地址都不需要写上它 +registry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin())) + .addPathPatterns("/**").excludePathPatterns("/user/login"); +``` +- 可能7:你写了多个匹配规则,请求越过了第一个规则,但又被其它规则拦下来了,例如以下代码: +``` java +// 以下代码,当你未登录访问 `/user/doLogin` 时,会被第1条规则越过,然后被第2条拦下,校验登录,然后抛出异常:`NotLoginException:xxx` +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/**").notMatch("/user/doLogin").check(r -> StpUtil.checkLogin()); // 第1个规则 + SaRouter.match("/**").notMatch("/article/getList").check(r -> StpUtil.checkLogin()); // 第2个规则 + SaRouter.match("/**").notMatch("/goods/getList").check(r -> StpUtil.checkLogin()); // 第3个规则 +})).addPathPatterns("/**"); +``` +- 可能8:你自定义的封装方法,并没有按照你的预想情况执行: +``` java +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + // 调用自定义的 excludePaths() 方法获取数据排除鉴权 + SaRouter.match("/**").notMatch(excludePaths()).check(r -> StpUtil.checkLogin()); + })).addPathPatterns("/**"); +} +// 自定义查询排查鉴权的地址方法 +public static List excludePaths() { + List list = ... // 从数据源查询...; + return list; +} +``` +如上方法, `excludePaths()` 可能并不会像你预想的一样正确执行返回相应的值,请在 `.notMatch()` 处 `一律先硬编码写固定死值来测试`,这时就有两种情况: + - 情况1:写固定死值时,代码能正常执行了,那说明你自定义的 `excludePaths()` 方法有问题,执行结果不正确。 + - 情况2:写固定也不行,那说明不是 `excludePaths()` 的问题,那再从其它地方开始排查。 + + + +### Q:我在配置文件中加了一些关于 Sa-Token 的配置,但是没有生效。 +首先,有没有生效的最佳判断方式是,在main方法中加一个打印,看看打印出来的和你配置文件的一致吗: + +``` java +@SpringBootApplication +public class SaTokenApplication { + public static void main(String[] args) { + SpringApplication.run(SaTokenApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } +} +``` + +如果不一致,请排查: +- 可能1:项目中还存在代码配置,而代码配置会覆盖 `application.yml` 中配置,详细参考:[框架配置](/use/config)。 +- 可能2:你的配置文件名字错误,SpringBoot 项目正常情况下配置文件名称应该是:`application.yml` 或 `application.properties`。 +- 可能3:可能是你的配置前缀不对,或者配置缩进不对: +``` yaml +# 错误示例,多加了 spring 前缀 +spring: + sa-token: + token-name: xxx-token +# 错误示例,缩进不对 +sa-token: +token-name: xxx-token +# 正确的应该是以 sa-token 开头 +sa-token: + token-name: xxx-token +``` + + +### Q:我自定义了组件,但是好像没有生效? +1、可能组件没有注入成功,排查方法为在 main 里打印这个组件,是否为自定义的class限定名: +``` java +@SpringBootApplication +public class SaTokenApplication { + public static void main(String[] args) { + SpringApplication.run(SaTokenApplication.class, args); + System.out.println(SaManager.getStpInterface()); // 打印全局的 StpInterface 实现类 + } +} +``` +如果打印出的是你的自定义实现类,则证明注入成功,如果不是,则证明没有注入成功,请排查: +- 自定义的组件实现类上是否加上了 `@Component` 注解,只有加上这个注解,组件才会被 Spring 自动实例化并注入。 +- 自定义的组件实现类是否在启动类的同目录或者子目录上,如果不在则无法被 springboot 启动时扫描,扫描不到也就无法注入。 +- 启动类上是否加了 `@ComponentScan` 注解,导致包扫描范围不正确,请将此注解删除或移动到其它配置类上。 + +2、这个组件注入成功了,但是还没到执行时机,比如 `StpInterface` 组件,只有在鉴权时才会触发,如果你的代码仅仅是登录校验,就不会执行到这个组件。 + + +### Q:集成 Redis 后,明明 Redis 中有值,却还是提示无效Token? + +根据以往的处理经验,发生这种情况 90% 的概率是因为你找错了Redis,即:代码连接的Redis和你用管理工具看到的Redis并不是同一个。 + +你可能会问:我看配置文件明明是同一个啊? + +我的回答是:别光看配置文件,不一定准确,在启动时直接执行 `SaManager.getSaTokenDao().set("name", "value", 100000);`, +随便写入一个值,看看能不能根据你的预期写进这个Redis,如果能的话才能证明`代码连接的Reids` 和`你用管理工具看到的Redis` 是同一个,再进行下一步排查。 + + +### Q:报错:无效Same-Token:xxxxxxxxxxx +与之类似的的报错还有: +- SSO模式二时,报错:无效ticket:xxxxxxxxxx +- OAuth2模块跨多个项目搭建Server时:报错无效 Access-Token:xxxxxx +- 微服务做分布式 Session 认证时,报错:无效 Token:xxxxxxxxx +- 等等等等.... + +这些功能有个统一的特点,就是需要多个项目连接同一个 Redis 才能搭建成功,如果连接的不是同一个 Redis,就会导致 Token / ticket 无法互相认证。 + +你可能会问:我看配置文件明明就是连接的同一个 Redis 啊? + +别急,和上一个问题一样,**不要凭借肉眼检查下定论**,在你的两个服务之间,分别使用以下代码测试一下: + +``` java +@SpringBootApplication +public class SaTokenApplication { + public static void main(String[] args) { + SpringApplication.run(SaTokenApplication.class, args); + // 写值测试:注意一定要用下列方法测试,不要用自己封装的 RedisUtil 之类的测试 + SaManager.getSaTokenDao().set("name", "value", 100000); + } +} +``` + +如果都能根据你的预期写进同一个 Redis,那才能证明两个服务确实连接的是同一个 Redis。 + +实际上,在交流群中提问这些问题的同学,90%的经过以上测试以后,都会发现两者连接的不是同一个 Reids,原因大多是:Redis配置没有生效、使用了 Alone-Redis 之类的…… + +如果你是剩下的 10%,那么继续排查:两边的 sa-token 配置是否完全一致,比如 token-name 配置不一致,也会导致数据无法相互认证。最好是把所有 sa-token 相关的配置都复制过去,试验一下看看。 + + + +### Q:我把 token 有效期设置为 30 天,但是总感觉不到 30 天的时候 token 就无效了,怎么回事? +- 可能1:你没有为 sa-token 集成 Redis,框架默认将会话数据保存在内存中,项目重启后数据会消失。 +- 可能2:你为 sa-token 集成了 Redis,但是 Redis 重启了,导致会话消失。 +- 可能3:你配置了 `is-concurrent=false`,不允许同一账号多端登录,有别人登录了这个账号把你顶下去了。 +- 可能4:你配置了 `is-concurrent=true`,但是`is-share=false`,同一账号每次登录产生不同的 token,默认最高可以同时登录12个客户端,超过将自动注销最原先的会话。 +- 可能5:你的这个账号,别人也登录了,别人调用了注销方法,把你这边的也注销了。`StpUtil.logout()` 为单 token 注销,`StpUtil.logout(10001)` 为账号所有 token 注销。 +- 可能6:你虽然 `sa-token.timeout` 配置了 30 天,但是 `sa-token.active-timeout` 配置了较短的值,超过这个时间无操作,token 就过期了。 +- 可能7:你换了浏览器,或者换了电脑,或者清空了浏览器最近缓存记录,自然而然需要重新登录。 +- 可能8:你中途改了项目配置,比如改了 `sa-token.token-name` 配置项的值,会导致会话保存的 key 发生改变,效果等同于手动清空了 Redis 数据,需要重新登录。 + + + +### Q:有时候我不加 Token 也可以通过鉴权,请问是怎么回事? +- 可能1:你访问的这个接口,根本就没有鉴权的代码,所以可以安全的访问通过。 +- 可能2:可能是 Cookie 帮你自动提交了 Token,在浏览器或 Postman 中会自动维护Cookie模式,如不需要可以在配置文件:`is-read-cookie: false`,然后重启项目再测试一下。 + + +### Q:一个 User 对象存进 Session 后,再取出来时报错:无法从 User 类型转换成 User 类型? +- 可能1:你的 User 类中途换了包名,导致存进去时和取出来时对不上,无法成功创建实例。 +- 可能2:你打开了代码热刷新模式,先存进去的对象,热刷新后再取出,会报错,关闭热刷新即可解决。 + + +### Q:在 SaServletFilter 中调用 SpringMVCUtil.getRequest() 报错:非Web上下文无法获取Request? + +- 可能1:项目中有配置类继承了: `extends WebMvcConfigurationSupport`。 +- 可能2:项目中有配置类添加了注解: `@EnableWebMvc`。 + +解决方案:不要加 `@EnableWebMvc`,不要 `extends WebMvcConfigurationSupport`,要 `implements WebMvcConfigurer` + +如果一定要 `extends WebMvcConfigurationSupport` ,可以通过手动注册 Spring 上下文初始化过滤器试试: + +``` java +@Configuration +public class SaTokenConfigure extends WebMvcConfigurationSupport { + + // Spring 上下文初始化过滤器 可能由于各种原因没有被注册到,这里手动帮忙注册一下 + @Bean + @ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class }) + @ConditionalOnMissingFilterBean(RequestContextFilter.class) + public static RequestContextFilter requestContextFilter() { + System.out.println("--------------------------- 注册了"); // 加个打印语句或者断点确保这里注册到了 + return new OrderedRequestContextFilter(); + } + +} +``` + + +如果不是以上原因,可以加群提供复现demo。 + + + + +### Q:我配置了 active-timeout 值,但是当我每次续签时 Redis 中的 ttl 并没有更新,是不是 bug 了? +不更新是正常现象,`active-timeout`不是根据 ttl 计算的,是根据value值计算的,value 记录的是该 Token 最后访问系统的时间戳, +每次验签时用:当前时间 - 时间戳 > active-timeout,来判断这个 Token 是否已经超时。 + + +### Q:整合 Redis 时先选择了默认jdk序列化,后又改成 jackson 序列化,程序开始报错,SerializationException? +两者的序列化算法不一致导致的反序列化失败,如果要更改序列化方式,则需要先将 Redis 中历史数据清除,再做更新。 + + +### Q:升级 sa-token-redisson 后登录失效 / 读 Redis 报错? +`SaTokenDaoForRedisson` 已改为默认使用 `StringCodec`,与升级前跟随 Redisson 全局 codec(一般为 `Kryo5Codec`)不兼容。请清空 Redis 中的 Sa-Token 旧缓存,或构造时传入 `new Kryo5Codec()`。详见:[集成 Redis - 集成 Redisson](/up/integ-redis?id=_5集成-redisson)。 + + +### Q:调用 `StpUtil.getExtra("name")` 报错:`this api is disabled`。 +`StpUtil.getExtra(key)` 是给 sa-token-jwt 插件提供的,不集成这个插件就不能调用这个API,如果是普通模式需要存储自定义参数,请在 SaSession 上存储 + +``` java +// 在登录时缓存参数 +StpUtil.getSession().set("name", "zhangsan"); + +// 然后我们就可以在任意处获取这个参数 +String name = StpUtil.getSession().getString("name"); +``` + + +### Q:我加了 Sa-Token 的全局过滤器,浏览器报错跨域了怎么办? +参考:[https://juejin.cn/post/7247376558367981627](https://juejin.cn/post/7247376558367981627) + + +### Q:前后端分离项目中,前端使用 vue,如果不打开 porxy 代理的话,调用 Sa-Token 登录不会将 token 自动注入到 Cookie 中,是因为跨域么? +是。 + +参考:[前后端分离](/up/not-cookie) + + +### Q:集成redis后对象模型序列化异常 +假设执行如下代码: +``` java +@Data +public class User implements Serializable { + private Long userId; + private String username; + private String password; +} +``` + +``` java +User user = new User(); +user.setUserId(10000L); +user.setUsername("oneName"); +user.setPassword("onePass"); +StpUtil.getSession().set("userObjKey", user); // 这里报错 +``` +报错信息如下: +``` +SerializationException: Could not read JSON: +Cannot deserialize value of type `java.lang.Long` from Array value (token `JsonToken.START_ARRAY`) +``` + +Springboot 集成 Sa-Token Redis 后, 一旦 Springboot 切换版本就有可能出现此问题 + +原因是 Redis 里面有之前的 Sa-Token 会话数据, 清空 Redis 即可。 + + + +### Q:我实现了 StpInterface 接口,但是在登录时没有进入我的实现类代码? +不进入是正常现象, StpInterface 是鉴权接口,在执行鉴权代码时才会进入 StpInterface 实现类,登录认证时不会进入。 + + +### Q:启动时报错,找不到 xx 类 xx 方法: +``` java +Caused by: java.lang.ClassNotFoundException: cn.dev33.satoken.same.SaSameTemplate +``` + +一般找不到类,或者找不到方法,都是版本冲突了,使用 Sa-Token 时一定要注意**版本对齐**,意思是所有和 Sa-Token 相关的依赖都需要版本一致。 + +比如说你如果一个依赖是 1.32.0,一个是 1.31.0,就会造成无法启动: + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + 1.32.0 + + + cn.dev33 + sa-token-core + 1.31.0 + +``` + +请仔细排查你的 pom.xml 文件,是否有 Sa-Token 依赖没对齐,**请不要肉眼检查,用全局搜索 "sa-token" 关键词来找**,如果是多模块或者微服务项目,就整个项目搜索。 + + +### Q:在多账号模式的注解鉴权时,报错:未能获取对应StpLogic,type=xxx + +报这个错说明对应 type 的 StpLogic 尚未初始化到全局 StpLogicMap 中,一般会有两种原因造成这种情况: +1. 注解里的 loginType 拼写错误,请改正 (建议使用常量)。 +2. 自定义 StpUtil 尚未初始化(静态类中的属性至少一次调用后才会初始化),解决方法两种: + - (1) 从main方法里调用一次 + - (2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 + + +### Q:使用拦截器鉴权,访问一个不存在的 path 时,springboot 会自动在控制台打印一下异常。 +可尝试添加以下配置解决: +``` properties +spring.web.resources.add-mappings=false +spring.mvc.throw-exception-if-no-handler-found=true +``` + + + + +### Q:开启了全局懒加载后,能启动项目,但是访问接口报“未能获取有效的上下文处理器” +开启了全局懒加载后,能启动项目,但是访问接口报异常 `InvalidContextException`: 未能获取有效的上下文处理器, 配置如下: +``` yaml +spring: + main: + lazy-initialization: true +``` +原因是 Sa-Token 自动配置入口类 SaBeanInject 被延迟加载了,只需要手动指定懒加载排除掉 SaBeanInject 就可以了,实现代码如下: +``` java +@Configuration +class MyConfiguration { + @Bean + LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() { + return LazyInitializationExcludeFilter.forBeanTypes(SaBeanInject.class); + } +} +``` +[经验来源](https://gitee.com/dromara/sa-token/issues/I7EXIU) + + +### Q:SpringBoot 3.x 路由拦截鉴权报错:No more pattern data allowed after {*...} or ** pattern element + + +报错原因:SpringBoot3.x 版本默认将路由匹配机制由 `ant_path_matcher` 改为了 `path_pattern_parser` 模式, +而此模式有一个规则,就是写路由匹配符的时候,不允许 `**` 之后再出现内容。例如:`/admin/**/info` 就是不允许的。 + +如果你的项目报了这个错,说明你写的路由匹配符出现了上述问题,有三种解决方案: +1. 等待 SpringMVC 官方增强 `path_pattern_parser` 模式能力,使之可以支持 `**` 之后再出现内容。 +2. 在写路由匹配规则时,避免使 `**` 之后再出现内容。 +3. 将项目的路由匹配机制改为 `ant_path_matcher`。 + +步骤1:先改项目的: +``` yml +spring: + mvc: + pathmatch: + matching-strategy: ant_path_matcher +``` + +步骤2:再改 Sa-Token 的: +``` java +/** + * 重写路由匹配算法,切换为 ant_path_matcher 模式,使之可以支持 `**` 之后再出现内容 + */ +@PostConstruct +public void customRouteMatcher() { + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return SaPatternsRequestConditionHolder.match(pattern, path); + }; +} +``` + +**注意点:** + +SpringBoot2.x 的 `WebFlux`或 `SC Gateway` 项目,按照上述步骤改造,可能会报错 + +``` html +java.lang.NoClassDefFoundError: org/springframework/web/servlet/mvc/condition/PatternsRequestCondition +``` + +只需要将“步骤2”中的代码 `return SaPatternsRequestConditionHolder.match(pattern, path);` +更换为 `return SaPathMatcherHolder.getPathMatcher().match(pattern, path);` 即可,例如: + +``` java +/** + * 重写路由匹配算法,切换为 ant_path_matcher 模式,使之可以支持 `**` 之后再出现内容 + */ +@PostConstruct +public void customRouteMatcher() { + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return SaPathMatcherHolder.getPathMatcher().match(pattern, path); + }; +} +``` + + +### Q:Webflux 环境集成,或者 SpringCloud Gateway 环境集成后,过滤器里路由拦截鉴权报错:`java.lang.NoSuchFieldError: defaultInstance` + +``` java +java.lang.NoSuchFieldError: defaultInstance + at cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil.match(SaPathPatternParserUtil.java:40) + at cn.dev33.satoken.reactor.spring.SaTokenContextForSpringReactor.matchPath(SaTokenContextForSpringReactor.java:34) + at cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:58) + at cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:72) + ... +``` + +原因:SpringBoot 版本用的太低了,导致一些类不存在。 + +- 方案一:升级项目的 SpringBoot 版本至 `2.3.x` 以上 +- 方案二:像上面的问题解决方案一样,重写一下相关类: + +``` java +/** + * 重写路由匹配算法,将 PathPatternParser.defaultInstance 改为 SaPathMatcherHolder.getPathMatcher() + */ +@PostConstruct +public void customRouteMatcher() { + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return SaPathMatcherHolder.getPathMatcher().match(pattern, path); + }; +} +``` + + +### Q:过低的 SpringBoot 版本引入 Sa-Token 后报错 + +在低于 2.2.0 时 (不包含2.2.0本身) 的 SpringBoot 项目中引入 Sa-Token 后,项目启动时会报错: + +``` txt +org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cn.dev33.satoken.spring.SaBeanInject': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [cn.dev33.satoken.spring.SaBeanInject]: Constructor threw exception; nested exception is java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator +``` + +这是由于缺少 jackson 相关依赖导致的,可以手动添加以下依赖来解决: + +``` xml + + + com.fasterxml.jackson.core + jackson-core + 2.17.3 + + + com.fasterxml.jackson.core + jackson-annotations + 2.17.3 + + + com.fasterxml.jackson.core + jackson-databind + 2.17.3 + +``` + + + +### Q:在 idea 导入源码,运行报错:java: 程序包cn.dev33.satoken.oauth2不存在。 + +在项目根目录进入 cmd,执行 `mvn package`,然后重新运行试试。 + +如果不行,先执行 `maven clean` ,然后删除 .idea 文件夹里除 `icon.png` 外的所有文件,然后执行 `mvn package`,然后重新运行试试。 + +如果还不行,删除整个项目,重新从 git 地址拉取一遍,再运行。 + + + +### Q:报错:非 web 上下文无法获取 HttpServletRequest。 + +报错原因解析: + +Sa-Token 的部分 API 只能在 Web 上下文中才能调用,例如:`StpUtil.getLoginId()` 获取当前用户Id,这个方法第一步需要先从前端提交的参数里获取 token 值, +当你在 main 方法里调用这个 API 时,由于 main 方法本质上不是一个 Controller 请求,所以框架无法完成 *“从前端提交的参数里获取 token 值”* 这一步骤,框架就只能抛出异常。 + +按照此标准,Sa-Token 的 API 可粗浅的分为两大类: +- 必须在 Web 上下文中才能调用的 API,例如:`StpUtil.getLoginId()`、`StpUtil.getTokenValue()` 等等。 +- 无需 Web 上下文也能调用的 API,例如:`StpUtil.getLoginType()`、`SaManager.getConfig()` 等等。 + +此处无法逐一列出到底哪些 API 属于 *“必须依赖 Web 上下文的 API”*,因为太多了,你只需要记住关键的一点: +**当一个 API 执行的代码需要先从前端请求中获取一些数据时,这个 API 就属于 *“必须依赖 Web 上下文的 API”*。** + +如果你的代码报这个错,说明你在不是 Web 上下文中的地方,调用了 *“必须依赖 Web 上下文的 API”*,请排查: + +1. 是否在 main 方法中调用了 *“必须依赖 Web 上下文的 API”*。 +2. 是否在带有 `@Async` 注解的方法中调用了 *“必须依赖 Web 上下文的 API”*。 +3. 是否在一些丢失 web 上下文的子线程中调用了 *“必须依赖 Web 上下文的 API”*,例如 `MyBatis-Plus` 的 `insertFill` 自动填充。 +4. 是否在一些非 Http 协议的 RPC 框架中(例如 Dubbo)调用了 *“必须依赖 Web 上下文的 API”*。 +5. 是否在 SpringBoot 启动初始化的方法中调用了 *“必须依赖 Web 上下文的 API”*,例如 `@PostConstruct` 修饰的方法。 +6. 是否在定时任务中调用了 *“必须依赖 Web 上下文的 API”*。 + + +### Q:报错:未能获取有效的上下文处理器。 + +报错原因解析: + +在 sa-token-core 核心包中,Sa-Token 底层不能确认最终运行的 web 容器,所以抽象了 `SaTokenContext` 接口,对接不同容器时需要注入不同的实现, +通常这个注入工作都是框架自动完成的,你只需要按照文档开始部分集成相应的依赖即可。例如: + +- 如果你使用的 `SpringBoot 2.x`,请引入 `sa-token-spring-boot-starter`。 +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 +- 如果你在基于 WebFlux 架构的网关中使用 Sa-Token,请引入 `sa-token-reactor-spring-boot-starter`(3.x 用 `sa-token-reactor-spring-boot3-starter`,4.x 用 `sa-token-reactor-spring-boot4-starter`)。 +- 你要在 Solon 中使用 Sa-Token,就引入:`sa-token-solon-plugin`。 +- 等等等等…… + +如果你的代码报 *“未能获取有效的上下文处理器”* 这个错,大概率是因为你没有正确引入所需的包,导致框架没有注入正确的 `SaTokenContext` 上下文实现,请排查: + +1. 如果你的项目是微服务项目,请直接参考:[微服务-依赖引入说明](/micro/import-intro),如果是单体项目,请往下看: +2. 请判断你的项目是 SpringMVC 环境还是 WebFlux 环境: + - 如果是 SpringMVC 环境就引入 `sa-token-spring-boot-starter` 依赖,参考:[在SpringBoot环境集成](/start/example) + - 如果是 WebFlux 环境就引入 `sa-token-reactor-spring-boot-starter` 依赖,参考:[在WebFlux环境集成](/start/webflux-example) +3. 如果你还无法分辨你是哪个环境,就看你的 pom.xml 依赖: + - 如果引入了`spring-boot-starter-web`就是 SpringMVC 环境。 + - 如果引入了 `spring-boot-starter-webflux` 就是WebFlux环境。 + - 什么?你说你两个都引入了?那你的项目能启动成功吗? +4. 如果是 WebFlux 环境而且正确引入了依赖,依然报错,**请检查是否注册了 SaReactorFilter 全局过滤器,在 WebFlux 下这一步是必须的**,具体还是请参考上面的 [ 在WebFlux环境集成 ] 章节。 +5. 需要仔细注意,如果你使用的是 `SpringBoot 3.x` 或 `SpringBoot 4.x`,请分别引入 `sa-token-spring-boot3-starter` 或 `sa-token-spring-boot4-starter`,不要错误引入 `sa-token-spring-boot-starter`,不然会导致框架报错。 +6. 如果你的项目开启了全局懒加载(spring.main.lazy-initialization=true)后,能启动项目,但是访问接口报异常,请直接参考:[Q:开启了全局懒加载后,能启动项目,但是访问接口报未能获取有效的上下文处理器](/more/common-questions?id=q:开启了全局懒加载后,能启动项目,但是访问接口报未能获取有效的上下文处理器) +7. 如果以上步骤排除无误后依然报错,请直接提 issue 或者加入QQ群求助。 + + + + + + + +## 二、常见疑问 + +### Q:登录方法需要我自己实现吗? +是的,不同于`shiro`等框架,`Sa-Token`不会在登录流程中强插一脚,开发者比对完用户的账号和密码之后,只需要调用`StpUtil.login(id)`通知一下框架即可 + +``` java +// 会话登录接口 +@RequestMapping("doLogin") +public SaResult doLogin(String name, String pwd) { + // 第一步:比对前端提交的账号名称、密码 + if("zhang".equals(name) && "123456".equals(pwd)) { + + // 第二步:比对成功后,调用通知框架,xxx账号登录成功 + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); +} +``` + +### Q:框架抛出的权限不足异常,我想根据自定义提示信息,可以吗? +可以,在全局异常拦截器里捕获`NotPermissionException`,可以通过`getPermission()`获取没有通过认证的权限码,可以据此自定义返回信息 + +``` java +@RestControllerAdvice +public class GlobalExceptionHandler { + // 全局 NotPermissionException 异常捕获 + @ExceptionHandler(NotPermissionException.class) + public SaResult handlerException(NotPermissionException e) { + e.printStackTrace(); + return SaResult.error("缺少权限:" + e.getPermission()); + } +} +``` + +### Q:在 SaInterceptor 中,注解鉴权总是先于路由拦截鉴权执行,能调整一下顺序吗? +框架没有提供直接的 API,但你有以下两种方式可以做到这一点: +- 方式1:将 SaInterceptor 里的代码复制出来一份,按照你的需求改一下,然后使用你这个自定义的拦截器,不再使用官方的。 +- 方式2:注册两次 SaInterceptor 拦截器,例如: + +``` java +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 路由拦截鉴权 + registry.addInterceptor(new SaInterceptor(r -> { + // 路由拦截鉴权的代码 ... + }).isAnnotation(false)).addPathPatterns("/**"); + + // 打开注解鉴权 + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); + } +} +``` +如上,第一个完成路由拦截鉴权功能,第二个完成注解鉴权功能。 + + +### Q:我的项目权限模型不是RBAC模型,很复杂,可以集成吗? +无论什么模型,只要能把一个用户具有的所有权限塞到一个List里返回给框架,就能集成 + + +### Q:StpInterface 接口的 方法,在什么时候执行? +每次鉴权时执行,例如你调用了 `StpUtil.checkgetPermission("xxx")` 方法,框架就会调用底层的 `StpInterface#getPermissionList` 方法来获取权限数据。 + +如果你的 `getPermissionList` 里有读数据库的代码,那么你每鉴一次权,系统将访问一次数据库。如果要减小性能消耗,可以把权限数据放在缓存中,参考:[把权限放在缓存里](/fun/jur-cache)。 + + +### Q:当我配置不并发登录时,每次登陆都会产生一个新的 Token,旧 Token 依然被保存在 Redis 中,框架为什么不删除呢? +首先,不删除旧 Token 的原因是为了在旧 Token 再次访问系统时提示他:已被顶下线。 + +而且这个 Token 不会永远留在 `Redis` 里,在其 TTL 到期后就会自动清除,如果你想让它立即消失,可以: + +- 方法一:配置文件把 `is-concurrent` 和 `is-share` 都打开,这样每次登陆都会复用以前的旧 Token,就不会有废弃 Token 产生了。 +- 方法二:每次登录前把先调用注销方法 `StpUtil.logout(10001)` ,把这个账号的旧登录都给清除了。 +- 方法三:写一个定时任务查询 Redis 值进行删除。 + + +### Q:我使用过滤器鉴权 or 全局拦截器鉴权,结果 Swagger 不能访问了,我应该排除哪些地址? +尝试加上排除 `"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" ,"/doc.html/**","/error","/favicon.ico"` + +不同版本可能会有所不同,其实在前端摁一下 `F12` 看看哪个 url 报错排除哪个就行了(另附:注解鉴权是不需要排除的,因为 `Swagger` 本身也没有使用 Sa-Token 的注解) + + +### Q:SaRouter.match 有多个路径需要排除怎么办? +可以点进去源码看一下,`SaRouter.match`方法有多个重载,可以放一个集合, 例如: +``` java +SaRouter.match("/**").notMatch("/login", "/reg").check(r -> StpUtil.checkLogin()); +``` + + +### Q:为什么StpUtil.login() 不能直接写入一个User对象? +`StpUtil.login()`只是为了给当前会话做个唯一标记,通常写入`UserId`即可,如果要存储User对象,可以使用`StpUtil.getSession()`获取Session对象进行存储。 + + +### Q:前后端分离模式下和普通模式有何不同? +主要是失去了`Cookie`无法自动化保存和提交`token秘钥`,可以参考章节:[前后端分离](/up/not-cookie) + + +### Q:前后端分离时,前端提交的 header 参数是叫 token 还是 satoken 还是 tokenName? +默认是satoken,如果想换一个名字,更改一下配置文件的`tokenName`即可。 + + +### Q:一个账号拥有哪些权限,可以做成动态的吗? +权限本来就是动态的,框架预留的 `StpInterface` 接口,就是为了让你可以写任意代码来获取数据 + + +### Q:路由拦截鉴权,可以做成动态的吗? +参考:[把路由拦截鉴权动态化](/fun/dynamic-router-check) + + +### Q:我不想让框架自动操作Cookie,怎么办? +在配置文件将`isReadCookie`值配置为`false` + + +### Q:怎么关掉每次启动时的字符画打印? +在配置文件将`isPrint`值配置为`false` + + +### Q:StpUtil.getSession()必须登录后才能调用吗?如果我想在用户未登录之前存储一些数据应该怎么办? +`StpUtil.getSession()`获取的是`Account-Session`,必须登录后才能使用,如果需要在未登录状态下也使用Session功能,请使用`Token-Session`
+步骤:先在配置文件里将`tokenSessionCheckLogin`配置为`false`,然后通过`StpUtil.getTokenSession()`获取Session 。或者直接调用 `StpUtil.getAnonTokenSession()` 获取匿名 Token-Session。 + + +### Q:我只使用header来传输token,还需要打开Cookie模式吗? +不需要,如果只使用header来传输token,可以在配置文件关闭Cookie模式,例:`isReadCookie=false` + + +### Q:我想让用户修改密码后立即掉线重新登录,应该怎么做? +框架内置 [强制指定账号下线] 的APi,在执行修改密码逻辑之后调用此API即可: `StpUtil.logout()` + + +### Q:代码鉴权、注解鉴权、路由拦截鉴权,我该如何选择? +这个问题没有标准答案,这里只能给你提供一些建议,从鉴权粒度的角度来看: +1. 路由拦截鉴权:粒度最粗,只能粗略的拦截一个模块进行权限认证 +2. 注解鉴权:粒度较细,可以详细到方法级,比较灵活 +3. 代码鉴权:粒度最细,不光可以控制到方法级,甚至可以if语句决定是否鉴权 + +So:从鉴权粒度的角度来看,需要针对一个模块鉴权的时候,就用路由拦截鉴权,需要控制到方法级的时候,就用注解鉴权,需要根据条件判断是否鉴权的时候,就用代码鉴权 + + +### Q:Sa-Token的全局过滤器我应该怎么指定它的优先级呢? +为了保证相关组件能够及时初始化,框架默认给过滤器注册的优先级为-100,如果你想更改优先级,直接在注册过滤器的方法上加上 `@Order(xxx)` 即可覆盖框架的默认配置 + + +### Q:timeout 过期了,获取到的 NotLoginException 场景值是-2,按照文档说的应该是-3吧。是我理解的不对还是操作有误? +你的理解是对的,但是框架现在只能做到返回-2,因为 token 过期后,就从 Redis 中消失了,框架没法分辨这个 token 是曾经有过然后过期的,还是从来就没有在Redis中存在过, +所以只能统一抛出-2,这个行为也和具体使用的 SaTokenDao 有关联,例如集成 sa-token-jwt 插件后,框架就能分辨出来是 token 过期了,抛出-3。 + + +### Q:Sa-Token 是否提供类似 RefreshToken 的概念,与 AccessToken 相互配合刷新令牌鉴权。 +关于长短 token,Sa-Token 没有提供直接的 API 支持,但是你可以利用 “临时 token 认证模块” 轻易的达到这一点: + +1. 把 `sa-token.timeout` 的值配置小一点,然后把 `StpUtil.login(10001)` 生成的 token 作为短 token ,用来鉴权。 +2. 用 “临时 token 认证模块” 生成长 token, `String refreshToken = SaTempUtil.createToken(10001, 2592000);`。 +3. 把这两个 token 一起返回到前端。 +4. 你再开个接口,可以让前端通过长 token,刷新短 token,参考代码: + +``` java +@RequestMapping("/refreshToken") +public SaResult refreshToken(String refreshToken) { + // 1、验证 + Object userId = SaTempUtil.parseToken(refreshToken); + if(userId == null) { + return SaResult.error("无效 refreshToken"); + } + + // 2、为其生成新的短 token + String accessToken = StpUtil.createLoginSession(userId); + + // 3、返回 + return SaResult.data(accessToken); +} +``` + + +### Q:前后端一体项目下,在拦截未登录进入登录页面时,如何登录完成后原路返回? +可以在拦截跳转登录页面时,把原 url 作为 back 参数挂载到登录页后方,登录完成后读取 back 参数并跳转 +``` java +@RestControllerAdvice +public class GlobalException { + // 未登录异常拦截 + @ExceptionHandler(NotLoginException.class) + public Object handlerException(NotLoginException e) { + e.printStackTrace(); + return SaHolder.getResponse().redirect("/login?back=" + SaHolder.getRequest().getUrl()); + } +} +``` + + + +### Q:怎么改变请求返回的 http 状态码? +``` java +SaHolder.getResponse().setStatus(401) +``` + + + +### Q:Sa-Token 集成 Redis 如何集群? +以 `sa-token-redis-template` 为例:Sa-Token 底层使用的是 RedisTemplate 对象来操作数据的,也就是说,你只要给 RedisTemplate 配置上集群模式,Sa-Token 自动就是集群模式了。 + + +### Q:多个项目共用同一个 redis,怎么防止冲突? + +详见:[集成 Redis - 多个项目共用同一个 Redis,怎么防止冲突?](/up/integ-redis?id=_4多个项目共用同一个-redis怎么防止冲突) + + +### Q:是否启用 Redis,可以通过配置文件来决定吗? + +框架没有提供直接的配置开关。不过你可以自己注册 `SaTokenDao`,根据配置返回不同实现: + +``` yaml +# 自定义开关(非框架内置配置项) +sa-token: + my-use-redis: true +``` + +``` java +@Configuration +public class SaTokenDaoConfig { + + @Value("${sa-token.my-use-redis:false}") + private boolean useRedis; + + @Bean + @Primary + public SaTokenDao saTokenDao() { + // 开启 Redis:使用 RedisTemplate 持久化 + if (useRedis) { + return new SaTokenDaoForRedisTemplate(); + } + // 关闭 Redis:回落到内存实现 + return new SaTokenDaoDefaultImpl(); + } +} +``` + +也可以按 SpringBoot 多环境配置文件拆分(如 `application-dev.yml` / `application-prod.yml` 配不同的 `sa-token.my-use-redis`),达到「开发用内存、生产用 Redis」的效果。 + + +### Q:如何防止 CSRF 攻击? +CSRF 攻击的核心在于利用浏览器自动提交 Cookie 的特性,代替用户发送自己不想发送的请求。 + +**方案一:关闭 Cookie模式。** + +在配置文件里配置 `sa-token.is-read-cookie=false` 关闭 Cookie 读取模式,采用 localStorage 存储 token + header 头提交,即可避免 CSRF 攻击。 + +**方案二:增加 csrf-token 验证** + +如果项目必须采用 Cookie 模式验证,可以在请求中增加 csrf-token 验证的环节: + +1、在登录时,生成一个 `csrf_token` 返回到前端: +``` java +// 测试登录 +@RequestMapping("/login") +public SaResult login() { + StpUtil.login(10001); + String csrfToken = StpUtil.getSession().get("csrf_token", () -> SaFoxUtil.getRandomString(60)); + return SaResult.ok().set("csrf_token", csrfToken); +} +``` + +2、前端将 csrf_token 存储在 localStorage 中(注意一定要存储在 localStorage 而非 Cookie 中,存储在 Cookie 中还是可能会被浏览器自动提交) +``` java +localStorage.setItem('csrf_token', csrf_token); +``` +每次请求将 csrf_token 塞到 Header 中。 + +3、在需要防止 CSRF 攻击的接口验证 csrf_token: +``` java +@RequestMapping("/test") +public SaResult test() { + + // 先验证 csrfToken + String csrfToken = SaHolder.getRequest().getHeader("csrf_token"); + if (csrfToken == null || ! csrfToken.equals(StpUtil.getSession().get("csrf_token")) ) { + throw new SaTokenException("csrf_token 不匹配"); + } + + // 通过后再处理具体业务 + // ... + + return SaResult.ok(); +} +``` + +也可以将验证代码写到全局拦截器中,为所有接口提供校验。 + + + +### Q:如何自定义框架读取 token 的方式? +**方式一:通过 StpUtil.getStpLogic().setTokenValueToStorage("abcdefgxxxxxxxx") 自定义 token 值** + +如果你可以在框架读取 token 之前写一些代码,那么你可以通过如下代码自定义当前请求的 token 值: +``` java +@RequestMapping("/test") +public SaResult test() { + System.out.println(StpUtil.getTokenValue()); // 此时读取到的是前端提交的: cebcc930-c0f5-4009-8eb0-1b6aee63b4aa + StpUtil.getStpLogic().setTokenValueToStorage("abcdefgxxxxxxxx"); + System.out.println(StpUtil.getTokenValue()); // 此时读取到的是我们自定义的: abcdefgxxxxxxxx + return SaResult.ok(); +} +``` + +**方式二:重写 StpLogic 读取 token 的方法** + +``` java +@Component +public class MyStpLogic extends StpLogic { + public MyStpLogic() { + super("login"); + } + // 自定义 token 读取方式,例如此处改为读取请求头为 my-token 的值 + @Override + public String getTokenValue() { + String token = SaHolder.getRequest().getHeader("my-token"); + return token; + } +} +``` + + + + +### Q:文档是否能下载?是否有离线版? +文档已完整开源,请访问 Sa-Token 官方仓库,根目录下的 sa-token-doc 文件夹就是文档。 + + + + +### Q:还是有不明白到的地方? +请在`gitee` 、 `github` 提交 `issues`,或者加入qq群交流,[群链接](/more/join-group) + diff --git a/sa-token-doc/more/content-cooperation.md b/sa-token-doc/more/content-cooperation.md new file mode 100644 index 000000000..bd7f30776 --- /dev/null +++ b/sa-token-doc/more/content-cooperation.md @@ -0,0 +1,104 @@ +# Sa-Token 内容合作群 + +**好内容值得被看见!** + +为感谢 Sa-Token 的内容创作者们,我们特别创建了「Sa-Token 内容合作群」,帮助大家的内容触达更多 Sa-Token 的使用者 (加群方式在最下方)。 + +--- + + +### 📖 1、一些碎碎念,想和写公众号/录视频的朋友们聊聊 + +前几天,我在公众号上搜索 “Sa-Token”,想看看有没有人写过相关的教程或者踩坑心得。 + +说实话,当时没抱太大期望。毕竟我们开发团队这几年来几乎将所有精力都放在了代码开发,而一直疏于内容运营建设。 + +但结果让我挺意外的 —— **我发现了不少公众号都在写 Sa-Token 的文章,而且其中不少都写得很用心**。 + +有从零开始的入门教程,有深入源码的解析文章,有对框架各个功能的介绍,还有一些结合真实业务场景的实战案例 …… 解决的都是实实在在的问题。 + +但这些文章的阅读量… 很多都只有几百,有些甚至只有几十。 + +我知道这很正常。**技术公众号起步非常不容易,粉丝少的时候,再好的内容也很难被看到**。我自己也经历过这个阶段,知道那种 [ 写了一整天,发出来没人看 ] 的感觉。 + +为了不让这些有价值的内容埋没,我连夜将这些文章整理到了 Sa-Token 官网:[框架博客](/more/blog)。 + +在整理这些博客的过程中,我突然有了一个想法。 + + +### ✨ 2、一个可能 [三赢] 的想法 + +我在想,我为什么不拉一个群聊,把这些为 Sa-Token 写文章的博主们,给聚集起来呢? + +只要有朋友写了 Sa-Token 相关文章,都可以转发到群里,我们团队会把这个文章转发到 Sa-Token 所有粉丝群里: + +这可能是一个三赢的合作: + +- **对于 Sa-Token 来说**:能获得更多的优质内容,帮助新用户更快上手,生态也能更丰富。 +- **对写文章的朋友来说**:你的好文章能被更多人看到,公众号能涨涨粉,付出的时间更有价值。 +- **对 Sa-Token 的用户来说**:能看到更多的技术干货,学到更多知识,找到各种场景的解决方案,不用重复踩坑。 + +听起来好像…还不错? + +所以我打算建个群,名字就叫 「Sa-Token 内容合作群」。不是什么正式的组织,就是一群对技术内容感兴趣的朋友,凑在一起互相帮帮忙。 + + +### 🤝 3、这个群主要用来做什么? + +1、如果你写了 Sa-Token 相关的文章(或录制了视频课程),可以分享到群里,我们团队会把文章: + +- **转发到 Sa-Token 所有粉丝群里**:Sa-Token 目前拥有 30+ 微信粉丝群 (500人),10+ QQ粉丝群 (1000人 or 2000人)。 +- **挂载到 Sa-Token 在线文档博客栏目**:Sa-Token 目前在线文档访问量月PV 20万+。 + +相信这一定可以大大提高文章的曝光量。 + +2、我们团队偶尔也会为 Sa-Token 撰写技术文章,发到群里: + +- 如果你觉得内容不错,想转载到自己的公众号,**直接转就行**。 +- 不用专门申请授权,Sa-Token 官方订阅号所有内容均开放版权,任何人都可以自由转载。 + + +### ❤️ 4、几个你可能关心的小问题 + +#### Q:我没写过 Sa-Token 的文章,可以加入吗? +可以。完全没问题。哪怕你之前从来没写过 Sa-Token 的文章,但只要你有公众号,想试试写相关的内容,都欢迎。 + +如果你没有公众号,但是在别的平台,比如掘金、CSDN有写过文章,也可以加入。如果你在 B站/抖音录制过视频,也可以加入。 + +总之:只要你有意向在任意平台创作 Sa-Token 相关内容,就可以加入。 + + +#### Q:有 KPI 吗?是不是进了群就要为 Sa-Token 写文章? +没有。 想写就写,不想写就不写。哪怕加群后一篇都不写,也没关系。**这就是个「互助群」,不是「任务群」**。 + +#### Q:文章有什么要求吗? +就一点:认真写。 +可以是入门教程、源码解析、实战心得、bug排查、常见踩坑、对比测评…什么形式都行。不需要多长的篇幅,能把一个知识点讲清楚就好。唯一的要求是:不要是纯 AI 生成的粗制滥造文,不要写明显错误的技术观点。 + +#### Q:对粉丝量有要求吗? +没有要求。 我自己也是从小号做起来的,完全理解起步的难处。群里不分大号小号,只看内容用不用心。 + +#### Q:除了转发,还有别的吗? +有。 我偶尔会分享一些 Sa-Token 的更新动态、设计思路,或者我发现的其他好的技术文章。大家也可以互相**聊聊技术写作、视频制作的心得,分享好用的工具**。就是个普通的交流群,只不过主题稍微聚焦一点。 + +说到底,我希望这个群不只是一个内容分发渠道,更是一个 「Sa-Token内容共创伙伴」的聚集地。我们一起,让好的技术方案被更多人看见和使用。 + + +### 👋 5、怎么加群? + +你可以在 [加入讨论群](/more/join-group) 处,添加我们的微信账号, **请在添加时备注或者加好友成功后发送以下信息:[申请加入 Sa-Token 内容合作群]** 。 + +一定要备注以上信息,否则我们团队人员只会把你拉入到普通粉丝交流群。 + +Sa-Token 生态合作群 + + + + + + + + + + + diff --git a/sa-token-doc/more/demand-commit.md b/sa-token-doc/more/demand-commit.md new file mode 100644 index 000000000..362362fe9 --- /dev/null +++ b/sa-token-doc/more/demand-commit.md @@ -0,0 +1,25 @@ +# 需求提交 + +文档不清晰?功能不完善?脑袋里有好 idea?提!都可以提! + +比起浮夸的赞美,Sa-Token 更希望收到您的批评与建议。 + +我们深知一个优秀的项目不能闭门造车,它需要海纳百川:[点我在线提交需求](https://wj.qq.com/s2/10852322/0d8b/) + + + +我们将慎重对待每一位粉丝的珍贵意见 ❤️ ❤️ ❤️: + +- 对框架新增特性功能且比较简单,会在第一时间进行开发。 +- 对框架新增特性功能但比较复杂,会延后几个版本制定相应的计划后进行开发。 +- 与框架设计理念不太相符,或超出权限认证范畴,将会视需求人数决定是否开发。 + + +### 其它反馈途径 +除了问卷提交,你还可以从以下渠道向我们提交反馈: +- Gitee:[issue 提交](https://gitee.com/dromara/sa-token/issues) +- GitHub:[issue 提交](https://github.com/dromara/sa-token/issues) +- AtomGit:[issue 提交](https://atomgit.com/dromara/sa-token/issues) +- 交流群:[加群链接](/more/join-group) + +请大胆提交、大胆咨询,请在交流群中大胆艾特我们,请不要有任何害羞 🤭。就算我们不实现,你也不会损失什么,对吧! diff --git a/sa-token-doc/more/download-demos.md b/sa-token-doc/more/download-demos.md new file mode 100644 index 000000000..47e3c854c --- /dev/null +++ b/sa-token-doc/more/download-demos.md @@ -0,0 +1,25 @@ +# Sa-Token 集成 Demos 示例大全下载 + +**我们为框架几乎所有技术点均单独制作了对应的集成示例,此压缩包共计 60+ Demo**:涵盖 Sa-Token 登录认证、权限认证、SSO 单点登录、OAUth2 统一认证、微服务鉴权、API Key 认证、JWT集成、跨系统调用参数签名校验 等鉴权认证的方方面面。 + + + + +### 下载方式 + +加入 Sa-Token 交流群,即可在群公告查看下载地址: + +加入QQ交流群:1098917026 [点击加入](https://qm.qq.com/q/26OFBGd2Zy) + +加入微信交流群: + +微信群 + +PS:扫码添加微信 (备注:sa),邀您加入群聊。 + +
+ +微信群 + + + diff --git a/sa-token-doc/more/join-group.md b/sa-token-doc/more/join-group.md new file mode 100644 index 000000000..dae433af4 --- /dev/null +++ b/sa-token-doc/more/join-group.md @@ -0,0 +1,83 @@ +# 加入讨论群 + +加入 Sa-Token 专属讨论群,与众多大佬一起努力 (huá shuǐ) 成长 (mō yú)。 + +--- + +### 1、加入QQ交流群 + + + + + +QQ交流群:1098917026 [点击加入](https://qm.qq.com/q/26OFBGd2Zy) + +### 2、加入微信交流群: + + + +微信群 + +PS:扫码添加微信 (备注:sa),邀您加入群聊。 + +
+ + +微信群 + + +加入群聊的好处: +- 第一时间收到框架更新通知。 +- 第一时间收到框架 bug 通知。 +- 第一时间收到新增开源案例通知。 +- 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。 + + + +### 3、群规(碎碎念): +有同学质问我们,我加了群,为什么被踢了?我们很少踢人,一般只有严重违反群规了我们才会选择踢人。 +- 不要在群里发擦边图、视频。轻度我们会警告,重度我们会选择移出群聊。 +- 不要在群里聊代理、魔法上网等话题,如有需求请各自互相私聊,不要在群里聊。 +- 不要发和程序员无关的广告,和程序员有关的比如开源项目、IT网站等我们一般不管。 + +请体谅我们,我们拉一个群也不容易,辛辛苦苦几个月才能拉满一个500人群,结果因为一些违规消息就导致封群,很难受的! + +被踢了还能再次加群吗?可以,只要你想加,并保证不再发布违规消息,就可以在被踢7天之后再次申请加群。 + + + + +### 4、内部群: + +为感谢对 Sa-Token 生态做出贡献的同学,我们特创建了内部群:【Sa-Token 生态共享与合作】 + +加入群聊条件,以下满足其一即可: +- 写过5篇以上有关 Sa-Token 的原创博客。 +- 为 Sa-Token 开发过插件。 +- 有开源项目集成了 Sa-Token,并在 [Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) 完成提交。 +- 有为 Sa-Token 录制过教程视频,发表在公共平台(总时长>30分钟,且播放量>2000)。 +- 其它一些您认为有对 Sa-Token 生态做出贡献的行为,可以直接联系我们,经内部投票评审通过即可加入(不要害羞,大胆联系我们哦 😊 ) + +加入群聊的好处: + +- 更及时的获知 Sa-Token 下一步更新计划。 +- 在 Sa-Token 遇到的任何疑问都可以当面与作者沟通,可协助解决问题。 +- 可提出未来版本更新需求,将具有更高的优先级进行评审与开发。 + +QQ群聊号码:939849926 + +注:此为专属内部群聊,不满足上述条件的同学请勿过分申请打扰,谢谢合作。满足条件者可以在申请加入时备注上您的项目名称 +(例如:xx开源项目作者集成了 sa-token,申请加入群聊),如果字数太多无法写完,也可在开源交流群里@管理员协助交流。 + + +### 5、Sa-Token 内容合作群 +专门为 Sa-Token 内容创作者们准备的交流群:[Sa-Token 内容合作群](/more/content-cooperation) + + +### 6、商务合作 + +友联互换、商务合作请添加我的个人微信号:`kite2020_`,添加时请备注来意。 + +我的个人微信号好友数量已 9000+,马上满 1 万好友上限,所以请务必备注来意,否则不通过。如果仅是为了加入 Sa-Token 交流群请扫描上方二维码,会拉你进群。 + diff --git a/sa-token-doc/more/link.md b/sa-token-doc/more/link.md new file mode 100644 index 000000000..f6a0ca2a7 --- /dev/null +++ b/sa-token-doc/more/link.md @@ -0,0 +1,210 @@ +# 使用 Sa-Token 的开源项目 + + +> 集成 Sa-Token 的开源案例收集,取自 Awesome-Sa-Token,定期同步: +> [Gitee](https://gitee.com/sa-tokens/awesome-sa-token)、 +> [GitHub](https://github.com/sa-tokens/awesome-sa-token)、 +> [AtomGit](https://atomgit.com/sa-tokens/awesome-sa-token) + +--- + + +### 📊 后台管理 + +- [[ art-design-pro-java ]](https://github.com/anganing/art-design-pro-java):SpringBoot17+Sa-token+Art-Design-Pro+Unibest 技术栈的企业级后台开发管理系统。 + +- [[ wemirr-platform ]](https://gitee.com/battcn/wemirr-platform):JDK17、SCA2023、SC2024、Sa-Token、VBen5.x 全网最炫酷,功能最多,最优雅地真开源 多租户、SAAS 微服务项目。 + +- [[ Lucky-Admin-Vue ]](https://gitee.com/xiaodu6/lucky-admin-vue):一个基于vue-admin-template的后台管理框架,集成了动态角色权限,动态路由,角色权限动态配置,日志框架,代码生成,Sa-Token权限校验,快速构建一个后台的开发框架。 + +- [[ 灯灯]](https://github.com/dromara/lamp-cloud):基于java + SpringCloudAlibaba +SpringBoot 开发的微服务中后台快速开发平台,专注于多租户 (SaaS架构) 解决方案,亦可作为普通项目(非SaaS架构)的基础开发框架使用,目前已实现 数据源隔离、字段隔离、无租户隔离 等几种模式。 + +- [[ 橙单 ]](https://gitee.com/orangeform/orange-admin):技术栈Boot3 + Flowable7 + Sa-Token + Mybatis-Flex/Mybatis-Plus + Vue3,支持开箱即用且功能完成的工作流和在线表单功能,提供高颜值的流程和表单编辑器全部前后端源码。 + +- [[ Sz-Admin ]](https://github.com/feiyuchuixue):一个开源RBAC中后台框架,专为现代应用设计。它结合了最新的技术栈,包括后端的Spring Boot 3、JDK 21、Mybatis Flex、Sa-Token、Knife4j和Flyway,以及前端的Vue 3、Vite5、TypeScript和Element Plus,致力于为您提供一个直观、流畅且功能强大的开发体验。 + +- [[ newbie-boot3 ]](https://github.com/zhangyuge7/newbie-boot3):企业级中大型项目快速开发平台,后端使用JDK21+SpringBoot3+SaToken+MybatisPlus等,前端基于FiveAdminV2后台管理系统模板开发,使用js+vue3+vite5+ElementPlus等最新技术栈。 + +- [[ EuBackend ]](https://gitee.com/zhaoeryu/eu-backend):EuBackend 是一套全部开源的前后端分离 Java EE 企业级快速开发平台,基于最新技术栈SpringBoot、Sa-Token、MyBatisPlus等作为后端框架,使用RBAC作为权限控制模型,并且毫无保留给个人及企业免费使用。 + +- [[ srppms ]](https://gitee.com/cai-bin00/srppms):基于SpringBoot+Vue+sa-token前后端分离的科研项目管理平台。 + +- [[ twelvet-fast ]](https://gitee.com/twelvet/twelvet-fast):基于Spring Boot 3 JDK17的单体服务极速开发管理平台脚手架,先行体验最新技术栈。 + +- [[ Sa-Plus ]](https://gitee.com/click33/sa-plus):一个基于 SpringBoot 架构的快速开发框架,内置代码生成器。 + +- [[ dcy-fast ]](https://gitee.com/dcy421/dcy-fast):一个基于 SpringBoot + Sa-Token + Mybatis-Plus 的后台管理系统,前端vue-element-admin,并且内置代码生成器。 + +- [[ Helio-Boot ]](https://gitee.com/uncarbon97/helio-boot):基于 SpringBoot + Sa-Token + Mybatis-Plus 的单体开发脚手架,带有配套后台管理前端模板及代码生成器;拥有对应微服务版脚手架`Helio-Cloud` + +- [[ EasyAdmin ]](https://gitee.com/lakernote/easy-admin):一个基于SpringBoot2 + Sa-Token + Mybatis-Plus + Snakerflow + Layui 的后台管理系统,灵活多变可前后端分离,也可单体,内置代码生成器、权限管理、工作流引擎等 + +- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus):重写RuoYi-Vue所有功能 集成 Sa-Token+Mybatis-Plus+Jackson+Xxl-Job+knife4j+Hutool+OSS 定期同步 + +- [[ SpringBoot_v2 ]](https://gitee.com/bdj/SpringBoot_v2):SpringBoot_v2项目是努力打造springboot框架的极致细腻的脚手架。 + +- [[ Ruoyi-Satoken ]](https://gitee.com/wangming123456/ruoyi-satoken):为 ruoyi 进行配置 sa-token + +- [[ vue-satoken-admin ]](https://gitee.com/niluni/vue-satoken-admin):基于Vue2和Sa-Token1.18.0的后台权限系统。 + +- [[ bootx-platform ]](https://gitee.com/bootx/bootx-platform):包含支付收单(支付宝、微信、聚合、组合支付)、工作流(Flowable)、三方对接(微信、钉钉、企微、短信)等模块,前端基于Vue2和Vue3分别打造,可应用在不同业务场景中,目标是致力实现媲美商业版应用脚手架。 + +- [[ spba-admin ]](https://gitee.com/qkdja/spring-boot-admin):基于SpringBoot、Vue开发的通用后台管理系统,做到开箱即用,为新项目开发省去了基础功能开发的步骤。主要使用Sa-Token权限认证、MyBatis-Plus、MySQL、Redis、validation、七牛云等技术。 + +- [[ QForum-Core ]](https://github.com/Project-QForum/QForum-Core/):QForum 论坛系统官方核心,可拓展性强、轻量级、高性能、前后端分离,基于 SpringBoot2 + Sa-Token + Mybatis-Plus + +- [[ ExciteCMS-Layui ]](https://gitee.com/ExciteTeam/ExciteCMS-SpringBoot-Layui):ExciteCMS 快速开发脚手架:一款后端基于 SpringBoot2 + Sa-Token + Mybatis-Plus,前端基于 Layuimini 的内容管理系统,具备RBAC、日志管理、代码生成等功能,并集成常用的支付、OSS等第三方服务,拥有详细的开发文档 + +- [[ sra-admin ]](https://github.com/CoCoTeaNet/sra-admin):快速开发脚手架,核心依赖:springboot3+sqltoy+satoken+hutool | 轻量级 | 只实现了用户、字典、角色、权限等常见功能,能够快速搭建一个web项目。 + +- [[ QuickBuild ]](https://gitee.com/CodeLiQing/custom-quick-build-platform): 快速构建 | 基于springboot+sa-token+neety+代码生产器(生成vue页面和增删改查代码)| 以及前端vue3和字节arco.design框架整合 + +- [[ magic-boot ]](https://gitee.com/ssssssss-team/magic-boot):基于 magic-api + Sa-Token 搭建的快速开发平台,可以实现在浏览器编写Vue代码,既改即生效 + +- [[ chaos ]](https://gitee.com/qishanor/chaos):一个基于 SpringBoot + Sa-Token + Mybatis-Plus的快速开发框架,前端vue-element-avue,内置代码生成器,代码最简洁,最佳学习实践方案。 + +- [[ xzadmin ]](https://gitee.com/xiaozhizxj/xzadmin):一个基于 Spring Boot+mybatis-plus+sotaken+Redis+Thymeleaf+hutool+easy-captcha+log4j的后台管理系统 + +- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy):国内首个国密前后分离快速开发平台,采用 Vue3 + AntDesignVue3 + Vite + SpringBoot + Mp + HuTool + SaToken + +- [[ XyyAdmin ]](https://gitee.com/xyy12611/springboot-xyy-admin-v3):开箱即用的前后端分离后台权限系统,关键技术SpringBoot、Sa-Token、MySql、Vue3、AntDesignVue。 + +- [[ Frsimple ]](https://gitee.com/frsimple/springboot):一个基于 SpringBoot + Sa-token + Tdesign-next + vite + vue3 + typescript 的开箱即中后台服务解决方案。 + +- [[ sa-admin-server ]](https://gitee.com/wlf213/sa-admin-server):sa-admin-server是一个后台管理框架的服务端,核心技术:SpringBoot+SaToken+Quartz+Cache+Redis+Netty+MyBatisPlus; 亮点:RABC动态权限+零SQL+定时任务+缓存+在线IM; 前后端可分离也可一体部署,可选七牛云对象存储和本地存储两种方式。 + +- [[ RuoYi-Vue-CMS ]](https://gitee.com/liweiyi/RuoYi-Vue-CMS):RuoYi-Vue-CMS是前后端分离的内容管理系统,支持站群管理、多平台静态化、元数据模型扩展、多语言、全文检索,能轻松组织各种复杂内容形态。技术栈:SpringBoot3 + VUE2 + MybatisPlus + Sa-Token + xxl-job + Freemarker + ES + Redis + MySQL。 + +- [[ springboot-multi-tenant-sa-token ]](https://gitee.com/willf/springboot-multi-tenant-sa-token):轻量的多租户后台管理系统脚手架(SpringBoot,Sa-Token,mybatis-plus,Vue & Element)。 + +- [[ solon_angis_beetlsql ]](https://gitee.com/smartcity/solon_angis_beetlsql):并元国产开发平台 solon、sa-token、beetlsql、smart-http + +- [[ zeta-kotlin ]](https://gitee.com/xia5800/zeta-kotlin):zeta-kotlin是使用kotlin语言基于spring boot、mybatis-plus、sa-token等框架开发的项目脚手架。 + +- [[ nebula-swagger-demo ]](https://gitee.com/flgitee/nebula-swagger-demo):springboot+nebula 集成knife4j案例 + +- [[ warm-sun]](https://gitee.com/min290/warm-sun):基于solon+vue3开发,jdk17+satoken+redisx/redisson+mybaits-flex+hutool+jackson+mapstruct+poi + +- [[ContiNew Admin]](https://gitee.com/Charles7c/continew-admin):ContiNew Admin 中后台管理框架/脚手架,Continue New Admin,持续以最新流行技术栈构建,拥抱变化,迭代优化。当前采用的技术栈:Vue3、TypeScript、Arco Design Vue、Spring Boot3(JDK17)、Undertow、Sa-Token、JWT、MariaDB、MyBatis Plus、Redis、Redisson、Easy Excel、Hutool 等。 + +- [[laymini-admin]](https://gitee.com/wlf213/laymini-admin):基于layuimini前端框架开发的一个简单的后台管理前后端不分离框架,主体技术mybatisplus+sa_token+springboot+freemarker,主要功能:RABC认证授权,后台管理功能,集成Quartz动态定时任务。 + +- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin):SmartAdmin国内首个以「高质量代码」为核心,「简洁、高效、安全」中后台快速开发平台;基于SpringBoot + Sa-Token + Mybatis-Plus 和 Vue3 + Vite5 + Ant Design Vue 4.x (同时支持JavaScript和TypeScript双版本);满足国家三级等保要求、支持登录限制、接口数据国产加解密、高防SQL注入等一系列安全体系。 + +- [[ Halcyon-Admin ]](https://github.com/hhfb8848/halcyon-springboot):基于 Spring Boot 3 和 Vue 3 的通用后台管理系统,专注于提供基本的管理功能,而非特定的部门管理或业务功能。 + +- [ breeze-boot-satoken-xxx系统 ]:breeze-boot-satoken-xxx 是一个开源免费(前后端分离)中后台管理系统基础解决方案,前端技术栈:( Vue3、 TypeScript、Element Plus、Pinia 、Vite)后端技术栈:(jdk17、 springboot3、SaToken、MybatisPlus等) + - SSO 版本,后端:https://gitee.com/breeze-boot/breeze-boot-satoken-sso + - SSO 版本,前端:https://gitee.com/breeze-boot/breeze-vite-ui-satoken-sso + - OAUTH 版本,后端:https://gitee.com/breeze-boot/breeze-boot-satoken-oauth + - OAUTH 版本,前端: https://gitee.com/breeze-boot/breeze-vite-ui-satoken-oauth +- [[ Summer-Flowers · 夏花 ]](https://gitee.com/Luv404/summer-flowers):基于 **Spring Boot 3 + JPA + QueryDSL + Sa-Token** 的企业级后台开发框架,前端采用 **SoybeanAdmin**。不同于常见 MyBatis 体系,Summer-Flowers 以 **Entity 作为业务第一表达**,通过 QueryDSL 实现类型安全的复杂查询,配合代码生成器与模块化架构,显著降低中长期项目的维护成本。 + + + + +### 🚀 微服务相关 + +- [[ XHan Admin ]](https://gitee.com/sun-xiaohan/xh-admin-frontend):XHan Admin 是一个开源免费(前后端分离)中后台管理系统基础解决方案, 无专业版收费,所有功能毫无保留的贡献给开源社区,使用最新技术栈全新开发,无任何历史代码包袱。 + +- [[ RuoYi-Cloud-Plus ]](https://gitee.com/dromara/RuoYi-Cloud-Plus):重写RuoYi-Cloud所有功能 整合 SpringCloudAlibaba + Sa-Token + Dubbo + Mybatis-Plus + Xxl-Job 全方位升级 定期同步 + +- [[ Sp-Cloud ]](https://gitee.com/click33/sp-cloud):Sa-Plus的微服务版本, 基于Spring-Cloud-Alibaba,微服务下使用Sa-Token的样例 + +- [[ YC-Framework ]](http://framework.youcongtech.com/):致力于打造一款优秀的分布式微服务解决方案 + +- [[ falser-cloud ]](https://gitee.com/falser/falser-cloud): 基于 SpringCloud Alibaba + SpringCloud gateway + SpringBoot + Sa-Token + vue-admin-template + Nacos + Rabbit MQ + Redis 的一个后台管理系统,前后端分离,权限管理,菜单管理,数据字典,停车场系统管理等功能 + +- [[ dcy-fast-cloud ]](https://gitee.com/dcy421/dcy-fast-cloud):一个基于 SpringCloudAlibaba + Sa-Token + dubbo2.7.8 + Seata + knife4j + Mybatis-Plus + MapStruct + 的后台管理系统,前端vue-element-admin,并且内置代码生成器+动态路由权限等功能 + +- [[ fhs-framework ]](https://gitee.com/fhs-opensource/fhs-framework):基于Springboot+Springcloud + Mybatis Plus + Sa-Token + Vue + ElementUI 的快速开发平台(低代码开发平台),本框架永远免费,永久全开源 + +- [[ Pig-Satoken ]](https://gitee.com/wchenyang/cloud-satoken):重写 Pig 授权方式为 Sa-Token,其他代码不变。 + +- [[ Helio-Cloud ]](https://gitee.com/uncarbon97/helio-cloud):基于 SpringBoot + SpringCloud Alibaba + Sa-Token + Mybatis-Plus 的微服务开发脚手架,带有配套后台管理前端模板及代码生成器 + +- [[ BudWk-V7 ]](https://gitee.com/budwk/budwk):基于 NutzBoot + Sa-Token + Dubbo + Nacos注册&配置中心 的微服务开发脚手架(同时提供单应用版本),带有配套后台管理前端模板及代码生成器 + +- [[ xr-satoken-cloud ]](https://gitee.com/fzhxfw/xr-satoken-cloud):一款基于SaToken轻量级Java权限认证框架构建的微服务后台开发脚手架,基于SpringCloud + SpringCloudAlibaba + Nacos + SaToken + Mybatis等技术搭建,内置RBAC权限管理,代码生成器,文件分片速传等,本项目完全开源免费,定期提交代码到dev开发分支,由个人开发者业余时间维护升级。 + +- [[ CloudEon ]](https://gitee.com/dromara/CloudEon):一款基于kubernetes的开源大数据平台,旨在为用户提供一种简单、高效、可扩展的大数据解决方案。 + +- [[ quick-boot ]](https://github.com/csx-bill/quick-boot):一款基于 Spring Cloud 2022 、Spring Boot 3、AMIS 和 APIJSON 的低代码系统。 + +- [[ linkin-platform ]](https://gitee.com/paohaizi/linkin-platform):Springboot + Springcloud + nacos + Mybatis Plus + Sa-Token + Vue3 + ElementPlus +微服务下使用Sa-Token的样例,是一套比较简洁的后台系统。 + +- [[ LangChat ]](https://github.com/TyCoding/langchat):( OpenAI / Gemini / Ollama / Azure / 智谱 / 阿里通义大模型 / 百度千帆大模型), Java生态下AI大模型产品解决方案,快速构建企业级AI知识库、AI机器人应用 + +### 🛒 商城 + +- [[ litemall-plus ]](https://gitee.com/ysling-org/litemall-plus):微信小程序SaaS商城系统,可支持多小程序同时运行。 + +- [[ mall4j ]](https://gitee.com/gz-yami/mall4j):基于Spring Boot 3 JDK17的一个商城手脚架。 + +- [[ Huanxing-mall ]](https://gitee.com/lijiaxing_boy/huanxing-mall):HuanXing 商城基于SpringCloud 2021 & Alibaba + Sa-token,前端基于 Vue3 +Element plus 的微服务商城 + + +### 📝 博客 + +- [[ jthink ]](https://gitee.com/wtsoftware/jthink): 一个基于 SpringBoot + Sa-Token + Thymeleaf 的博客系统 + +- [[ 拾壹博客 ]](https://gitee.com/quequnlong/shiyi-blog):一款vue+springboot前后端分离的博客系统,博客后台管理系统使用了vue+elmentui开发,后端使用Sa-Token进行权限管理,支持动态菜单权限,动态定时任务,文件支持本地和七牛云上传,使用ElasticSearch作为全文检索服务,支持QQ、微博、码云登录。 + +- [[ June 12 ]](https://gitee.com/hanshaung/ants):June 12 是一个纯开源免费的资讯/博客类网站,基于Spring Boot + Sa-Token + Vue开发。 + +- [[ YuanBlog ]](https://gitee.com/wlf213/yuan-blog):一款代码简单,功能丰富的多人社交博客平台。前后端分离,Vue+SpringBoot3,博客前端使用Quasar,后台管理前端使用NaiveUI,博客后端,后台管理后端分为两个系统,均使用Sa-Token进行认证授权。支持邮箱验证码登录。 + +- [[ 鸢尾博客 ]](https://gitee.com/lxwise/iris-blog_parent):鸢尾博客是一个基于Spring Boot+Vue3 + TypeScript + Vite+JavaFx的客户端和服务器端的博客系统。项目采用前端与后端分离,支持移动端自适应,配有完备的前台和后台管理功能。后端使用Sa-Token进行权限管理,支持动态菜单权限,服务健康监控,数据流量统计,支持QQ、微博、码云、GitHub等三方登录。 + + + +### 🔌 插件 + +- [[ Sa-Token-Plugin ]](https://gitee.com/bootx/sa-token-plugin):Sa-Token第三方插件实现,基于Sa-Token-Core,提供一些与官方不同实现机制的的插件集合,作为Sa-Token开源生态的补充 + +- [[ quarkus-sa-token ]](https://github.com/quarkiverse/quarkus-sa-token): quarkus 整合 Sa-Token。 + + +### 🌐 多语言 + +- Rust:[[ sa-token-rust ]](https://github.com/sa-tokens/sa-token-rust): 一个轻量级、高性能的 Rust 认证授权框架。 + +- Go:[[ sa-token-go ]](https://github.com/sa-tokens/sa-token-go): 一个轻量级、高性能的 Go 权限认证框架。 + +- NodeJS:[[ xlt-token ]](https://github.com/xiaoLangtou/xlt-token): 框架无关 Token 鉴权库,灵感来源于 Sa-Token;核心零框架依赖,NestJS 一行接入。 + +- PHP:[[ real-token ]](https://gitee.com/jinan-jimeng-network_0/real-token): 一个轻量级 thinkphp6 权限认证框架,让鉴权变得简单、优雅! + + + +### 📦 其它 + +- [[ Glowxq-OJ ]](https://github.com/glowxq/glowxq-oj):Glowxq-OJ 专业开源在线编程测评系统 | 基于Spring Boot 3.x + Java 21 + Vue 3构建 | 支持ACM/ICPC竞赛、信奥赛训练、编程教育 | 多语言判题、实时竞赛、在线IDE | Docker一键部署 | Modern Online Judge Platform for Competitive Programming & Coding Education。 + +- [[ FlyFlow ]](https://gitee.com/junyue/flyflow):基于SaToken开发的开源工作流系统:FlyFlow借鉴了钉钉与飞书的界面设计理念,致力于打造一款用户友好、快速上手的工作流程工具。 + +- [[ Sa-Token-Study ]](https://gitee.com/sa-tokens/sa-token-study):以demo示例的方式讲解 Sa-Token 源码涉及到的技术点,连载中…… + +- [[ SpringMvc+Sa-Token ]](https://gitee.com/SRD_01/spring-mvc-sa-token): Jsp+SpringMVC+SSO+Sa-Token+Redis | Spring MVC 集成 SaToken Demo 项目 + +- [[ iot-kit ]](https://gitee.com/iotkit-open-source/iotkit-parent):一个轻量级低门槛的物联网平台,包含了多协议设备接入、规则引擎、第三方平台接入、智能家居小程序等模块的项目,基于SpringBoot架构并集成了Sa-Token的OAuth2认证。 + +- [[ cubic ]](https://gitee.com/dromara/cubic):一站式问题定位平台,实时线程栈监控、线程池监控、动态arthas命令集、依赖分析等等等,助你快速定位问题。 + +- [[ ChatGPT-WEB ]](https://github.com/dulaiduwang003/ChatGPT-WEB):基于JDK17+SpringBoot3+UniApp 绘图 聊天 充值应用。(Web版本) + +- [[ SuperBot-ChatGPTApp ]](https://github.com/dulaiduwang003/SuperBot-ChatGPTApp):基于JDK17+SpringBoot3+UniApp 绘图 聊天 充值应用。(小程序版本) + +- [[ ScribbleHub ]](https://github.com/dulaiduwang003/ScribbleHub):基于SpringBoot+satoken+wxss开发的博客小程序 + +- [[ TIME-SEA-chatgpt ]](https://github.com/dulaiduwang003/TIME-SEA-chatgpt):基于SpringBoot+satoken+vue3+uniapp开发的多端Ai平台应用 + +- [[ SUPERBOT-GPT]](https://github.com/dulaiduwang003/SUPERBOT-GPT):基于SpringBoot3+satoken+uniapp开发的流量主小程序 + +- [[ DaxPay ]](https://gitee.com/dromara/dax-pay):一款免费开源的支付网关系统,支持支付宝、微信、云闪付等通道,提供收单、退款、聚合支付、对账、分账等功能。 + +- [[ Dinky ]](https://github.com/DataLinkDC/dinky):基于Apache Flink的实时数据开发平台,实现敏捷的数据开发、部署和运维 + +- [[ mldong ]](https://gitee.com/mldong/mldong):SpringBoot + Vue3 快速开发平台、自研工作流引擎 + diff --git a/sa-token-doc/more/noun-intro.md b/sa-token-doc/more/noun-intro.md new file mode 100644 index 000000000..ce9cd5403 --- /dev/null +++ b/sa-token-doc/more/noun-intro.md @@ -0,0 +1,69 @@ +# Sa-Token 名词解释 + +Sa-Token 无意发明任何晦涩概念提升逼格,但在处理 issue 、Q群解答时还是发现不少同学因为一些基本概念理解偏差导致代码出错, +所以整理本篇针对一些比较容易混淆的地方加以解释说明。 + +也希望各位同学在提交 issue、Q群提问之前充分阅读本篇文章,保证不要因为基本概念理解偏差,增加不必要的沟通成本。 + + +--- + +#### 几种 Token +- token:指通过 `StpUtil.login()` 登录产生的身份令牌,用来维护用户登录状态,也称:satoken、会话Token。 +- temp-token:指通过 `SaTempUtil.createToken()` 临时验证模块产生的Token,也称:临时Token。 +- Access-Token:在 OAuth2 模块产生的身份令牌,也称:访问令牌、资源令牌。 +- Refresh-Token:在 OAuth2 模块产生的刷新令牌,也称:刷新令牌。 +- Same-Token:在 SaSameUtil 模块生成的Token令牌,用于提供子服务外网隔离功能。 + + +#### 两种过期时间: +- timeout:会话 Token 的长久有效期。 +- active-timeout:会话 Token 的最低活跃频率。 + +两者的差别详见:[Token有效期详解](/fun/token-timeout) + + +#### 三种Session: +- Account-Session:框架为每个账号分配的 Session 对象,也称:账号Session。 +- Token-Session:框架为每个 Token 分配的 Session 对象,也称:令牌Session。 +- Custom-Session:以一个特定的值作为SessionId,来分配的 Session 对象,也称:自定义Session。 + +三者差别详见:[Session模型详解](/fun/session-model) + + +#### 账号标识: +- loginId:账号id,用来区分不同账号,通过 `StpUtil.login(id)` 来指定。 +- device:登录设备类型,例如:`PC`、`APP`,通过 `StpUtil.login(id, device)` 来指定。 +- loginType:账号类型,用来区分不同体系的账号,如同一系统的 `User账号` 和 `Admin账号`,详见:[多账号认证](/up/many-account) + + +#### 几种登录策略: +- 单地登录:指同一时间只能在一个地方登录,新登录会挤掉旧登录,也可以叫:单端登录。 +- 多地登录:指同一时间可以在不同地方登录,新登录会和旧登录共存,也可以叫:多端登录。 +- 同端互斥登录:在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线,参考腾讯QQ的登录模式:手机和电脑可以同时在线,但不能两个手机同时在线。 +- 限量登录:限定账号登录设备总数量,低于此数量时可以正常登录,高于此数量后每次登录自动清退一个之前的登录。 +- 记住我模式:指在一个设备终端登录成功,该设备重启之后依然保持登录状态。 +- 单点登录:在进入多个系统时,只需要登录一次即可。解决用户在不同系统间频繁登录的问题。 +- 同端多登录:指在一个终端可以同时登录多个账号。 + + +#### 几种注销策略: +- 单端注销:只在调用登录的一端注销。 +- 全端注销:一端注销,全端下线。 +- 同端注销:发起注销后,同类型设备端一起下线,不同设备类型不受影响。 +- 单点注销:与单点登录对应,一个系统注销,所有系统一起下线。 + +

+ 视频讲解:如何设计出最优的登录会话策略?单端登录、强制下线、多端互踢、记住我登录 +

+ + +#### 几种鉴权方式: +- 代码鉴权:在代码里直接调用 `StpUtil.checkXxx` 相关 API 进行鉴权。 +- 注解鉴权:在方法或类上添加 `@SaCheckXxx` 注解进行鉴权。 +- 路由拦截鉴权:在全局过滤器或拦截里通过:`SaRouter.match()` 拦截路由进行鉴权。 + + + + + diff --git a/sa-token-doc/more/recommended.md b/sa-token-doc/more/recommended.md new file mode 100644 index 000000000..f13d29a86 --- /dev/null +++ b/sa-token-doc/more/recommended.md @@ -0,0 +1,15 @@ +# 推荐阅读 + +> 与 Sa-Token 无直接关联、但质量较高的技术文章,按发表日期倒序展示。 + +--- + +- [RBAC 权限模型实战:从 ACL 踩坑到 RBAC3 落地(2026)](https://cloud.tencent.com/developer/article/2702469) (2026-07-02) + +- [OAuth 2.0 的四种方式](https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html) (2019-04-09) + +- [前端安全系列之二:如何防止 CSRF 攻击?](https://juejin.cn/post/6844903689702866952) (2018-10-12) + +- [前端安全系列(一):如何防止 XSS 攻击?](https://juejin.cn/post/6844903685122703367) (2018-09-28) + +- [理解 OAuth 2.0](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) (2014-05-12) diff --git a/sa-token-doc/more/sa-token-donate-old.md b/sa-token-doc/more/sa-token-donate-old.md new file mode 100644 index 000000000..db70c9202 --- /dev/null +++ b/sa-token-doc/more/sa-token-donate-old.md @@ -0,0 +1,190 @@ +# 赞助 Sa-Token + +--- + +Sa-Token 采用 Apache-2.0 开源协议,**承诺框架本身与官网文档永久免费开放**, +但是框架的日常更新与社区运营需要付出大量的精力,靠爱发电难以长久,如果 Sa-Token 帮助到了您,您可以友情支持一下 Sa-Token。 + + +### 友情赞助 + +您可以在项目 [Gitee](https://gitee.com/dromara/sa-token) 主页进行捐赠 + + + + + +**已捐赠列表:** + +

+
+ +| 赞助人 | 赞助金额 | 留言 | 时间 | +| :-------- | :-------- | :-------- | :-------- | +| [时间很快](https://gitee.com/frsimple) | ¥ 220.0 | 感谢您的开源项目! | 2023-10-27 | +| [立秋](https://gitee.com/code_wh) | ¥ 2.5 | 感谢您的开源项目! | 2023-10-27 | +| [PotatoLoofah](https://gitee.com/PotatoLoofah) | ¥ 10.0 | 感谢您的开源项目! | 2023-10-27 | +| [ly-chn](https://gitee.com/ly-chn) | ¥ 99.0 | 一定的资金支持有助于开源项目走的更加长远 | 2023-10-17 | +| [yangs2w](https://gitee.com/yangs2w) | ¥ 10.0 | 感谢您的开源项目! | 2023-10-10 | +| [lee](https://gitee.com/cngeeklee) | ¥ 10.0 | 真正的轻量级权限安全框架,希望继续更新 | 2023-10-06 | +| [yang](https://gitee.com/hansdm) | ¥ 10.0 | 感谢您的开源项目! | 2023-09-27 | +| [明道云](https://gitee.com/lunan-yn) | ¥ 200.0 | 明道云2023年伙伴大会,[报名链接](https://www.mingdao.com/event/mpc/2023) | 2023-09-25 | +| [shenlicao](https://gitee.com/shenlicao) | ¥ 10.0 | 感谢您的开源项目! | 2023-09-15 | +| [lostyue](https://gitee.com/lostyue) | ¥ 20.0 | 感谢您的开源项目! | 2023-09-14 | +| [huni](https://gitee.com/simin_sizi) | ¥ 10.0 | 感谢您的开源项目! | 2023-09-11 | +| [T_T](https://gitee.com/wm26hua) | ¥ 20.0 | 感谢您的开源项目! | 2023-09-07 | +| [Meteor](https://gitee.com/meteoroc) | ¥ 2.5 | 感谢您的开源项目! | 2023-08-23 | +| [刘斌](https://gitee.com/xuanfather) | ¥ 20.0 | 感谢您的开源项目! | 2023-08-17 | +| [快快乐乐小码农](https://gitee.com/happy-little-farmer) | ¥ 1.0 | 感谢您的开源项目! | 2023-08-17 | +| [失败女神](https://gitee.com/failedgoddess) | ¥ 50.0 | 感谢您的开源项目! | 2023-08-03 | +| 结弦奏(微信打赏) | ¥ 50.0 | 感谢您的开源项目! | 2023-08-07 | +| [好心肠的老哥](https://gitee.com/ntdm) | ¥ 10.0 | 非常好的开源项目,希望越来越好! | 2023-08-02 | +| [XiaoYi](https://gitee.com/getianit) | ¥ 100.0 | [亚洲云深圳BGP云服务器](https://www.asiayun.com/cart?action=configureproduct&pid=300) | 2023-07-24 | +| [张兆伟](https://gitee.com/zhang865700) | ¥ 50.0 | 感谢您的开源项目! | 2023-07-24 | +| [mikeinshanghai](https://gitee.com/mikeinshanghai)| ¥ 50.0 | Sa-Token, MeterSphere共成长,共辉煌! | 2023-07-14 | +| 吴其敏(微信打赏) | ¥ 200.0 | [CAT 是基于 Java 开发的实时应用监控平台,为美团点评提供了全面的实时监控告警服务。](https://github.com/dianping/cat) | 2023-07-11 | +| [Dear胜哥](https://gitee.com/DearShengGe) | ¥ 10.0 | 有幸在摸鱼时间认真看完了全文档,感觉很是不错。开源不易,望作者继续扩展该框架功能! | 2023-06-30 | +| [SP](https://gitee.com/LSP1999) | ¥ 10.0 | 就是需要这种简单上手的项目 | 2023-06-15 | +| [javahuang](https://gitee.com/javahrp) | ¥ 200.0 | [SurveyKing:功能最强大的调查问卷系统和考试系统,开源](https://gitee.com/surveyking/surveyking) | 2023-06-08 | +| [dyjgitdyjgit](https://gitee.com/qtinfogit) | ¥ 20.0 | 感谢您的开源项目! | 2023-05-22 | +| 砰嚓嚓(QQ打赏) | ¥ 20.0 | 一点打赏不成敬意 | 2023-05-15 | +| [xc_Moving](https://gitee.com/fireZhang) | ¥ 20.0 | 感谢您的开源项目!感谢SA-token帮我度过项目的难关 | 2023-05-11 | +| [BeckJin](https://gitee.com/beckjin666) | ¥ 100.0 | [明道云-零代码开发平台,快速响应业务需求。从“IT背锅侠”,变成“IT英雄”。](https://mingdao.com?s=st) | 2023-05-08 | +| [SummerHy](https://gitee.com/hurumo) | ¥ 10.0 | 国产,就是棒,:) | 2023-05-07 | +| [gdl](https://gitee.com/gdl97) | ¥ 20.0 | 感谢您的开源项目!作者牛逼! | 2023-04-29 | +| [bootx](https://gitee.com/bootx) | ¥ 100.0 | [Bootx-Platform:支付收单、三方对接、后端基于 Spring Boot、Spring Cloud 应用脚手架](https://gitee.com/bootx/bootx-platform) | 2023-04-18 | +| c(微信打赏) | ¥ 100.0 | 感谢您的开源项目! | 2023-04-17 | +| [hurumo](https://gitee.com/hurumo) | ¥ 20.0 | 感谢您的开源项目! | 2023-04-17 | +| [李广龙](https://gitee.com/ak47-b) | ¥ 20.0 | 跟大哥学习一辈子学不完 | 2023-04-14 | +| [Admin](https://gitee.com/jinan-jimeng-network_0) | ¥ 20.0 | 感谢您的开源项目! | 2023-04-12 | +| [王宁波](https://gitee.com/wang-ningbo) | ¥ 20.0 | 感谢您的开源项目! | 2023-04-10 | +| F(微信打赏) | ¥ 20.0 | 感谢您的开源项目! | 2023-04-09 | +| [zhou](https://gitee.com/mrzhou1) | ¥ 50.0 | 感谢答疑 | 2023-03-29 | +| [Java_小生](https://gitee.com/zhang_hanzhe) | ¥ 10.0 | 感谢Sa-Token让我不用去B站肯几十个小时的教程,框架很优秀文档更优秀 | 2023-03-09 | +| 空空(微信打赏) | ¥ 10.0 | 感谢您的开源项目! | 2023-03-08 | +| [李一博](https://gitee.com/haust_lyb) | ¥ 8.88 | 感谢您的开源项目! | 2023-03-07 | +| [陈乾](https://gitee.com/qianpou) | ¥ 50.0 | 感谢您的开源项目! | 2023-03-07 | +| [陈乾](https://gitee.com/qianpou) | ¥ 20.0 | 感谢您的开源项目! | 2023-03-05 | +| [熊孩子](https://gitee.com/xhz1230) | ¥ 20.0 | 感谢您的开源项目! | 2023-02-17 | +| [不问烟雨](https://gitee.com/xiaominfagui) | ¥ 10.0 | 牛 | 2023-01-12 | +| [tsing](https://gitee.com/tsing666) | ¥ 10.0 | 感谢您的开源项目! | 2023-01-08 | +| [SWmachine](https://gitee.com/SWmachine) | ¥ 10.0 | 您的开源很好用! | 2023-01-07 | +| [Peter Z](https://gitee.com/zj1995) | ¥ 10.0 | 感谢您的开源项目! | 2022-12-26 | +| [ken](https://gitee.com/affction) | ¥ 10.0 | 感谢您的开源项目! | 2022-12-19 | +| [刘涛](https://gitee.com/doILike) | ¥ 10.0 | 感谢您的开源项目! | 2022-12-13 | +| [时间很快](https://gitee.com/frsimple) | ¥ 50.0 | 感谢您的开源项目! | 2022-11-29 | +| [ThatYear](https://gitee.com/wangmuqing) | ¥ 20.0 | 感谢您的开源项目! | 2022-11-24 | +| [IlovePea](https://gitee.com/IlovePea) | ¥ 10.0 | 感谢您的开源项目! | 2022-11-22 | +| [feel](https://gitee.com/xujiahuim) | ¥ 10.0 | 感谢您的开源项目! | 2022-11-17 | +| [laruui](https://gitee.com/laruui) | ¥ 10.0 | 感谢您的开源项目! | 2022-10-28 | +| [就眠儀式](https://gitee.com/Jmysy) | ¥ 50.0 | 感谢您的开源项目! | 2022-10-26 | +| [王文博](https://gitee.com/rl520) | ¥ 20.0 | 感谢您的开源项目! | 2022-10-24 | +| [feyong](https://gitee.com/feyong) | ¥ 10.0 | 感谢您的开源项目! | 2022-10-18 | +| [xueshize](https://gitee.com/xueshize) | ¥ 20.0 | 感谢您的开源项目! | 2022-10-12 | +| [西东](https://gitee.com/noear_admin) | ¥ 99.0 | 感谢您的开源项目! | 2022-10-05 | +| [BlueRose](https://gitee.com/Bluerose_2) | ¥ 20.0 | 感谢您的付出,项目非常棒! | 2022-09-22 | +| [邱道长](https://gitee.com/qiudaozhang) | ¥ 20.0 | 优秀的项目,赞 | 2022-09-09 | +| [jerrydo](https://gitee.com/jerrydo) | ¥ 10.0 | 感谢您的开源项目!很强大! | 2022-08-10 | +| [小北宸呀](https://gitee.com/a_aas) | ¥ 10.0 | 感谢您的开源项目!我就喜欢你这种把我当白痴的官方文档 | 2022-07-08 | +| [jwc_gitee](https://gitee.com/jwc-gitee) | ¥ 10.0 | 感谢您的开源项目! | 2022-07-07 | +| [zhihong](https://gitee.com/zzh13520704819) | ¥ 20.0 | 感谢您的开源项目! | 2022-06-20 | +| [风如歌](https://gitee.com/the-wind-is-like-a-song) | ¥ 10.0 | 这个框架简直满足了我所有对于安全框架的需求,赞一个,加油sa-token加油中国开源! | 2022-06-17 | +| [qiuyue](https://gitee.com/bmlt) | ¥ 10.0 | satoken牛逼 | 2022-06-16 | +| [刘时立](https://gitee.com/liu-shili) | ¥ 10.0 | 非常棒的开源项目! | 2022-06-13 | +| [yuncai929](https://gitee.com/null_448_5562) | ¥ 10.0 | 感谢您的开源项目! | 2022-06-10 | +| [sun_2020](https://gitee.com/sun-two-thousand-and-twenty) | ¥ 50.0 | 感谢您的开源项目! | 2022-06-08 | +| [LZ](https://gitee.com/FUNKBOY) | ¥ 6.66 | 感谢您的开源项目!顺便踩一脚Spring Security,sa加油! | 2022-05-18 | +| [cray](https://gitee.com/hyy6300) | ¥ 10.0 | 感谢您的开源项目! | 2022-05-10 | +| [别处理](https://gitee.com/zshnb) | ¥ 10.0 | 非常好的项目,希望能一直做下去 | 2022-05-01 | +| [李洪星](https://gitee.com/li_hong_xing) | ¥ 10.0 | 解决了很多之前项目中遇到的问题。感谢您的开源项目! | 2022-04-29 | +| [乡村阿土哥](https://gitee.com/895995040) | ¥ 10.0 | 感谢您的开源项目! | 2022-04-29 | +| [Horatio201](https://gitee.com/horatio201) | ¥ 20.0 | 太牛了! | 2022-04-25 | +| [阿文](https://gitee.com/qq921124136) | ¥ 20.0 | 很好的框架,在开发文档里学到了很多知识点 | 2022-04-21 | +| 行长 (微信打赏) | ¥ 20.0 | 微信打赏 | 2022-04-15 | +| [xq584](https://gitee.com/xq584) | ¥ 10.0 | 感谢您的开源项目! | 2022-04-08 | +| [yukihane](https://gitee.com/yukihane) | ¥ 10.0 | 感谢您的开源项目! | 2022-04-07 | +| [alkinn](https://gitee.com/alkinn) | ¥ 10.0 | 感谢您的开源项目! | 2022-03-29 | +| [lele](https://gitee.com/lelez) | ¥ 10.0 | 感谢您的开源项目! | 2022-03-29 | +| Robin Tin (微信打赏) | ¥ 28.88 | 微信打赏 | 2022-03-24 | +| [刘嘉威](https://gitee.com/liu_jiawei) | ¥ 6.66 | 真滴好用~ | 2022-03-23 | +| 秦政 (微信打赏) | ¥ 20.00 | 微信打赏 | 2022-03-22 | +| 秦政 (微信打赏) | ¥ 6.66 | 微信打赏 | 2022-03-22 | +| 黎子豪 (微信打赏) | ¥ 18.88 | 请你喝杯咖啡 | 2022-03-21 | +| [Charles7c](https://gitee.com/Charles7c) | ¥ 20.0 | 感谢您的开源项目!希望 SSO 模块越来越好! | 2022-03-17 | +| [晓辉](https://gitee.com/zxhShow) | ¥ 10 | 感谢您的开源项目! | 2022-03-07 | +| [老杨](https://gitee.com/yangs914) | ¥ 6.66 | 感谢您的开源项目! | 2022-03-01 | +| 赵津 (微信打赏) | ¥ 16.00 | 微信打赏 | 2022-02-20 | +| [前世男友](https://gitee.com/lanbaba666) | ¥ 10 | 感谢您的开源项目! | 2022-02-17 | +| 两岁 (微信打赏) | ¥ 188 | 微信打赏 | 2021-12-27 | +| 刚子 (微信打赏) | ¥ 50 | 微信打赏 | 2021-12-27 | +| [网络小渣渣](https://gitee.com/a9777) | ¥ 10 | 感谢您的开源项目! | 2021-12-24 | +| [周周周杨](https://gitee.com/ChaoGeWanJiu) | ¥ 10 | 感谢您的开源项目! | 2021-12-18 | +| [MrXionGe](https://gitee.com/MrXionGe) | ¥ 10 | SA加油~~ | 2021-12-17 | +| [duyiliu](https://gitee.com/duyiliu) | ¥ 10 | 化繁为简,是门艺术。 | 2021-12-16 | +| [liu](https://gitee.com/liuliuliu123456) | ¥ 50 | 感谢您的开源项目! | 2021-12-15 | +| [fuhouyin](https://gitee.com/fuhouyin) | ¥ 10 | 感谢您的开源项目! | 2021-12-01 | +| [图灵谷](https://gitee.com/stephenson37) | ¥ 20 | 感谢您的开源项目! | 2021-11-30 | +| [luyuan](https://gitee.com/meitesi) | ¥ 20 | 感谢您的开源项目! | 2021-11-29 | +| [xiaoyan](https://gitee.com/l-yun) | ¥ 200 | 感谢您的开源项目! | 2021-11-26 | +| [yijunzhao](https://gitee.com/yijunzhao) | ¥ 20 | 感谢您的开源项目! | 2021-11-21 | +| [万声鹉](https://gitee.com/wanshengwu) | ¥ 10 | 感谢您的开源项目! | 2021-11-15 | +| [Taller](https://gitee.com/evilatom) | ¥ 10 | 感谢您的开源项目! | 2021-11-13 | +| [公子骏](https://gitee.com/dt_flys) | ¥ 20 | 感谢您的开源项目! | 2021-11-08 | +| [铂赛东](https://gitee.com/bryan31) | ¥ 20 | 开源加油! | 2021-11-08 | +| [孔孔的空空](https://gitee.com/kongmr) | ¥ 100 | 感谢您的开源项目! | 2021-11-02 | +| [songfazhun](https://gitee.com/fzsong) | ¥ 10 | 感谢您的开源项目! | 2021-10-28 | +| [ithorns](https://gitee.com/ithorns) | ¥ 10 | 感谢您的开源项目! | 2021-10-25 | +| [xiaoyan](https://gitee.com/l-yun) | ¥ 200 | 节日快乐 | 2021-10-24 | +| [apifox001](https://gitee.com/apifox001) | ¥ 200 | [Apifox:API 文档、API 调试、API Mock、API 自动化测试](https://apifox.com/) | 2021-10-15 | +| [永夜](https://gitee.com/cn-src) | ¥ 20 | 感谢您的开源项目! | 2021-09-18 | +| [苏永晓](https://gitee.com/suyongxiao) | ¥ 10 | 感谢您的开源项目! | 2021-09-01 | +| [xiaoyan](https://gitee.com/l-yun) | ¥ 200 | 好的作者理应被认可 | 2021-08-24 | +| [xiaoyan](https://gitee.com/l-yun) | ¥ 50 | be better | 2021-07-31 | +| [孔孔的空空](https://gitee.com/kongmr) | ¥ 500 | 感谢您的开源项目! | 2021-07-30 | +| [Wizzer](https://gitee.com/wizzer) | ¥ 20 | 感谢您的开源项目! | 2021-05-22 | +| [二范先生](https://gitee.com/mr-er-fan) | ¥ 20 | 省长加油啊 喝杯茶 | 2021-03-16 | +| [萧瑟](https://gitee.com/fengduidui) | ¥ 20 | 感谢您的开源项目! | 2021-03-16 | +| [xue1992wz](https://gitee.com/xue1992wz) | ¥ 20 | 感谢您的开源项目! | 2021-03-16 | +| [whcrow](https://gitee.com/whcrow) | ¥ 20 | 军师加油! | 2021-03-16 | +| [RockMan](https://gitee.com/njx33) | ¥ 10 | 感谢您的开源项目! | 2020-12-17 | +| [zhangjiaxiaozhuo](https://gitee.com/zhangjiaxiaozhuo)| ¥ 10 | 感谢您的开源项目! | 2020-12-15 | +| [知知](https://gitee.com/double_zhi) | ¥ 10 | 感谢您的开源项目! | 2020-12-15 | +| [省长](https://gitee.com/click33) | ¥ 10 | java中最好用的权限认证框架! | 2020-12-15 | + +
+ +
+共 0 位用户赞助, + 点击展开 ↓ + 点击收起 ↑ +
+ + +感谢每一位小伙伴的热心支持 ❤️ ❤️ ❤️ ! + + + +### 商业赞助 + +一次性赞助 200 元或以上,可帮助您的产品在 Sa-Token 交流群艾特全体成员推广一次。 + +并同时在赞助列表处高亮产品链接。 + +Sa-Token 目前总计14+微信交流群(每个群人数大约400以上,总计人数5000+),4个QQ交流群(2000人群和1000人群,总计人数6000+),大部分为 java 开发工程师,可有效推广您的产品。 + +- 优先推广和程序员相关的互联网产品,比如:低代码开发平台、网课、开发软件、云服务器、个人博客等等,实体产品如键盘、显示器、耳机等等,如果是和程序员无关的产品,可酌情考虑是否推广。 +- 拒绝接受违反法律法规、以及灰色相关的产品推广,为避免不必要的麻烦,目前也拒绝推广IP代理、上网工具等等。 +- 为避免过多打扰群友,目前一天内至多在群里推广两次,超过次数的可顺延到第二天。 + +有意见合作者可直接在 gitee 发起赞赏后,将您的产品信息发至 [ sa-pro 小助手 ] ,加好友链接点 [这里](https://sa-max.cn/),点击后往下拉有二维码,添加时请备注:Sa-Token 商业赞助。 + +注:在群发消息时,我们会明确对群友声明,此条消息为赞助商的产品,属于硬广信息。如您介意这一点,我们将暂时无法与您合作。 + +群发消息类似于以下: +``` txt +感谢 xx 老板对 Sa-Token 的商业赞助,以下是老板的产品,大家感兴趣的可以关注一下: +xxx 商品名称 +链接:https://xxxxxn.com/xxx +``` + +> 目前不接受 Sa-Token 官网文档插入广告推广,只接受一次性商业赞助推广。 \ No newline at end of file diff --git a/sa-token-doc/more/sa-token-donate.md b/sa-token-doc/more/sa-token-donate.md new file mode 100644 index 000000000..8aabc4df1 --- /dev/null +++ b/sa-token-doc/more/sa-token-donate.md @@ -0,0 +1,79 @@ +# 赞助 Sa-Token + +--- + +Sa-Token 采用 Apache-2.0 开源协议,**承诺框架本身与在线文档永久免费开放**, +但是框架的日常更新与社区运营需要付出大量的精力,靠爱发电难以长久,如果 Sa-Token 帮助到了您,您可以友情支持一下 Sa-Token。 + + +### 友情赞助 + + + +微信赞赏码 + + +**已捐赠列表:** + +
+
+ 日期排序 + | + 赞助额排序 +
+ + + + + + + + + + + +
赞助人赞助金额留言时间
+ +
+ + 第 1/1 页 + +
+
+ +感谢每一位小伙伴的热心支持 ❤️ ❤️ ❤️ ! + + + + \ No newline at end of file diff --git a/sa-token-doc/doc/more/tj-gzh-hz.md b/sa-token-doc/more/tj-gzh-hz.md similarity index 100% rename from sa-token-doc/doc/more/tj-gzh-hz.md rename to sa-token-doc/more/tj-gzh-hz.md diff --git a/sa-token-doc/doc/more/tj-gzh.md b/sa-token-doc/more/tj-gzh.md similarity index 80% rename from sa-token-doc/doc/more/tj-gzh.md rename to sa-token-doc/more/tj-gzh.md index f2f836569..dc3777e60 100644 --- a/sa-token-doc/doc/more/tj-gzh.md +++ b/sa-token-doc/more/tj-gzh.md @@ -7,6 +7,48 @@ + + + + + + + + + + - - - - +
+ + Java大飞哥 + 专注软件开发、技术架构设计、源码分享、JAVA技术。 + + + 小简聊开发 + 偶尔更新前后端技术文章,偶尔发布生活散文,更新随缘。浏览器搜索JanYork了解我更多。 + + + Java笔记虾 + 专注于Java技术栈,推送 Spring全家桶,Dubbo等相关技术知识 + + + 程序猿DD + 一线技术工作者的学习、生活与见闻 + + + TJ君 + 一个励志推荐10000款开源项目与免费工具的程序猿 +
+ + JavaGuide + 专注Java后端学习和大厂面试的公众号! + + + Hollis + 《Java工程师成神之路》系列作者 + + + macrozheng + 专注Java技术分享,作者Github开源项目mall(40K+Star) + 终码一生 @@ -17,9 +59,6 @@ CodeSheep 一只爱技术的程序羊,想把分享变成一种习惯! ---
@@ -102,7 +141,7 @@ 推荐热门开源软件,介绍新开源项目,报导开源资讯!

-感谢以上公众号对 Sa-Token 项目的推荐,如果您也是java公众号运营者,欢迎 [相互推荐](/more/tj-gzh-hz) +感谢以上公众号对 Sa-Token 项目的推荐 diff --git a/sa-token-doc/more/update-log.md b/sa-token-doc/more/update-log.md new file mode 100644 index 000000000..17fe89033 --- /dev/null +++ b/sa-token-doc/more/update-log.md @@ -0,0 +1,1007 @@ +# 更新日志 + +### v1.45.0 @2026-3-8 +- core: + - 新增:新增重复登录处理策略,当同一账号不允许多客户端同时登录时支持选择踢人下线或拦截本次登录。 **[重要]** merge: [pr 349](https://gitee.com/dromara/sa-token/pulls/349) + - 修复:修复 `StpUtil.getLoginIdByTokenNotThinkFreeze` 方法缺少 `static` 修饰符的问题。 + - 优化:优化路由匹配 pattern 缓存算法,消除魔法值。merge: [pr 907](https://github.com/dromara/Sa-Token/pulls/907) + - 优化:移除冗余导包。 +- 插件: + - 新增:新增 `sa-token-jackson3` 插件,用于 Jackson 3 的 JSON 操作。 **[重要]** + - 新增:新增 `sa-token-jackson3-test` 单元测试。 + - 新增:新增 `sa-token-snack4` 插件。 **[重要]** merge: [pr 356](https://gitee.com/dromara/sa-token/pulls/356) + - 修复:修复 Dubbo 上下文清理问题。 merge: [pr 889](https://github.com/dromara/Sa-Token/pulls/889) + - 新增:loveqq-framework 版本更新。merge: [pr 351](https://gitee.com/dromara/sa-token/pulls/351) +- starter: + - 新增:新增 `sa-token-spring-boot4-starter` 集成包,支持 Spring Boot 4 环境集成。 **[重要]** + - 新增:新增 `sa-token-reactor-spring-boot4-starter` 集成包,支持 Reactor + Spring Boot 4 环境集成。 **[重要]** + - 新增:新增 `sa-token-demo-springboot4`、`sa-token-demo-webflux-springboot4` 示例。 + - 新增:新增 Spring Boot 4 整合 demo 示例。 +- 重构: + - 重构:重构 `sa-token-dependencies` 相关模块,优化依赖关系。 **[重要]** + - 重构:重构 Spring Boot WebMVC/Reactor 相关集成包,优化依赖关系。 **[重要]** + - 优化:优化整体模块依赖关系。 +- Solon: + - 优化:`sa-token-solon-plugin` 优化 Gateway 接口的处理,避免使用路由接口。merge: [pr 348](https://gitee.com/dromara/sa-token/pulls/348) +- SSO: + - 新增:sso-server 前后端分离模式下 平台中心模式 demo 示例。 + - 修复:SSO 模块 msgType 参数说明、API 说明修正。 + - 新增:SSO 模块视频讲解链接:B站 王清江唷 SSO篇(29集)。 **[重要]** + - 补全:SSO 模块内置消息处理器相关文档。 + - 新增:文档为 `sa-token-sso` 模块定义 STS 协议。 **[重要]** +- OAuth2: + - 修复:修复 `sa-token-oauth2` 组件使用 `sa-token-fastjson2` 序列化导致的类型转换问题。merge: [pr 355](https://gitee.com/dromara/sa-token/pulls/355) + - 优化:修改 `ClientIdSecretModel` 的读取构建逻辑。merge: [pr 346](https://gitee.com/dromara/sa-token/pulls/346) +- 文档: + - 同步:同步公众号文章列表、博客列表、赞助者名单、企业登记案例。 + - 新增:新增 Sa-Token 内容合作者群。 **[重要]** + - 新增:新增《Gitee 2025 年度开源项目 Web 应用开发 Top 2》证书展示。 + - 新增:新增赞赏码展示、文档首页 stars 对比图。 + - 新增:新增解决跨域专题文章。 + - 新增:增加微信群聊信息展示。 + - 优化:优化框架 Slogan。 + - 优化:优化 README、案例库展示。 + - 优化:文档主题切换增加水滴特效,调整主题色块顺序。 + - 优化:文档优化 [登录认证]、[权限认证]、[路由拦截鉴权] 篇。 + - 优化:补全全局策略说明、数据结构说明。 + - 新增:目录树增加专门栏目记录项目架构设计。 + - 优化:功能结构图增加点击事件跳转到对应功能文档。 + - 优化:子服务外网隔离章节增加示意图。 + - 优化:Same-Token 同源系统认证图示说明。 + - 修复:更换 GitCode logo 为 AtomGit。 + - 修复:更换 QQ 群链接、微信群聊展示图。 + - 修复:文档图片地址更换为本地文件。 + - 修复:错别字修复。 + - 修复:maven-pull.md 文档,解决父子项目依赖下载问题。 + - 新增:Maven 父子项目无法下载依赖的问题解决方案。merge: [pr 358](https://gitee.com/dromara/sa-token/pulls/358) + - 修复:订正文档错别字。merge: [pr 354](https://gitee.com/dromara/sa-token/pulls/354) + - 修复:文档内代码示例修正。merge: [pr 347](https://gitee.com/dromara/sa-token/pulls/347) +- AI: + - 新增:新增 organize-update-log SKILL,用于格式化整理版本更新日志信息。 + - 新增:新增 commit-message SKILL,用于整理 git commit 日志信息。 + - 新增:新增 upgrade-version SKILL,用于统一升级修改版本号。 + - 新增:新增 remove-redundancy-import SKILL,用于检查 Java 类中无效冗余导包并移除。 +- 其它: + - 新增:readme 增加快问快答区域。 + - 新增:增加忽略 .vscode 目录。 + - 优化:注释优化。 + - 重构:备忘录重构为专门的文件夹。 + - 重构:调整项目发布配置至 Maven Central Portal。merge: [pr 792](https://github.com/dromara/Sa-Token/pull/792) + - 优化:部分构建配置升级到最新版。 + + + + + + +### v1.44.0 @2025-6-7 + +- 修复:修复 sso-server 前后端分离示例无法正常登录的问题。 +- 修复:修复 SSO 模式三全端注销失效的问题。 +- 修复:修复 SSO `SaSsoClientModel` 部分场景下无法序列化的问题。 +- 新增:OAuth2 模块新增支持从 `SaOAuth2DataLoader` 接口获取高级权限与低级权限的方法。merge: [pr 339](https://gitee.com/dromara/sa-token/pulls/339) +- 修复:修复 `sa-token-dubbo` 与 `sa-token-dubbo3` 每次调用都强制需要上下文的问题。 +- 文档:新增 `sa-token-dubbo3` 的说明。 +- 文档:更新赞助者名单。 +- 文档:新增 `loveqq-framework` 框架集成包。 **[重要]** merge: [pr 339](https://gitee.com/dromara/sa-token/pulls/340) + + + +### v1.43.0 @2025-5-17 +- core: + - 新增:`SaLogoutParameter` 新增 `deviceId` 参数,用于控制指定设备 id 的注销。 **[重要]** + - 新增:新增 `SaHttpTemplate` 请求处理器模块。 + - 新增:TOTP 增加 `issuer` 字段。 merge: [pr 329](https://gitee.com/dromara/sa-token/pulls/329) + - 修复:修复 `Http Digest` 认证时 url 上带有查询参数时认证无法通过的问题。merge: [pr 334](https://gitee.com/dromara/sa-token/pulls/334) + - 新增:@SaCheckOr 注解添加 `append` 字段,用于抓取未预先定义的注解类型进行批量注解鉴权。 + - 新增:侦听器 `doRenewTimeout` 方法添加 loginType 参数。 + - 新增:`SaInterceptor` 新增 `beforeAuth` 认证前置函数。 +- SSO: + - 新增:单点注销支持单设备注销。 **[重要]** fix: [#IA6ZK0](https://gitee.com/dromara/sa-token/issues/IA6ZK0) 、[#747](https://github.com/dromara/Sa-Token/issues/747) + - 新增:新增消息推送机制。 **[重要]** fix: [#IBGXA7](https://gitee.com/dromara/sa-token/issues/IBGXA7) + - 新增:配置项 clients 用于单独配置每个 client 的授权信息。 **[重要]** + - 新增:配置项 `allowAnonClient` 决定是否启用匿名 client。 + - 新增:SSO 模块新增配置文件方式启用“不同 client 不同秘钥”能力。 + - 重构:sso-client 封装化获取 client 标识值。 + - 新增:新增 SSO Strategy 策略类。 + - 新增:sso-client 新增 `convertCenterIdToLoginId`、`convertLoginIdToCenterId` 策略函数,用于描述本地 LoginId 与认证中心 loginId 的转换规则。 + - 新增:sso-server 新增 `jumpToRedirectUrlNotice` 策略,用于授权重定向跳转之前的通知。 + - 优化:调整整体 SSO 示例代码。 + - 新增:新增 ReSdk 模式对接示例:`sa-token-demo-sso3-client-resdk`。 **[重要]** + - 新增:新增匿名应用模式对接示例:`sa-token-demo-sso3-client-anon`。 **[重要]** +- OAuth2: + - 新增:`SaClientModel` 新增 `isAutoConfirm` 配置项,用于决定是否允许应用可以自动确认授权。 **[重要]** + - 新增:多 `Access-Token` 并存、多 `Refresh-Token` 并存、多 `Client-Token` 并存能力。 **[重要]** fix: [#IBHFD1](https://gitee.com/dromara/sa-token/issues/IBHFD1) 、 [#IBLL4Q](https://gitee.com/dromara/sa-token/issues/IBLL4Q) 、[#724](https://github.com/dromara/Sa-Token/issues/724) + - 新增:Scope 分割符支持加号。merge: [pr 333](https://gitee.com/dromara/sa-token/pulls/333) + - 修复:修复 oidc 协议下,当用户数据变动后,id_token 仍是旧信息的问题。 + - 优化:对 `OAuth2 Password` 认证模式需要重写处理器添加强提醒。 + - 优化:将认证流程回调从 `SaOAuth2ServerConfig` 转移到 `SaOAuth2Strategy`。 + - 新增:新增 `SaOAuth2Strategy.instance.userAuthorizeClientCheck` 策略,用于检查指定用户是否可以授权指定应用。fix: [#553](https://github.com/dromara/Sa-Token/issues/553) + - 优化:优化调整 `sa-token-oauth2` 模块代码结构及注释。 + - 新增:`currentAccessToken()`、`currentClientToken()`,简化读取 `access_token`、`client_token` 步骤 +- 插件: + - 新增:新增 `sa-token-forest` 插件,用于在 Http 请求处理器模块整合 Forest。 + - 新增:新增 `sa-token-okhttps` 插件,用于在 Http 请求处理器模块整合 OkHttps。 + - 拆分:API Key 模块拆分独立插件包:`sa-token-apikey`。 + - 拆分:API Sign 模块拆分独立插件包:`sa-token-sign`。 + - 修复:修复 `sa-token-dubbo` 插件部分场景上下文控制出错的问题。 + - 修复:修复 `sa-token-sanck3` `SaSessionForSnack3Customized:getModel` 接收 map 值时会出错的问题。 merge: [pr 330](https://gitee.com/dromara/sa-token/pulls/330) + - 修复:修复使用 `sa-token-redis-template-jdk-serializer` 时反序列化错误。merge: [pr 331](https://gitee.com/dromara/sa-token/pulls/331) + - 修复:`sa-token-snack3` 优化 `objectToJson` 序列化处理(增加类名,但不增加根类名)。 + - 重构:重构 `sa-token-redis-template`、`sa-token-redis-template-jdk-serializer` 插件中 update 方法 ttl 获取方式改为毫秒,以减少 update 时的 ttl 计算误差。 **[重要]** +- 示例: + - 新增:新增 SSE 鉴权示例。 +- 文档: + - 新增:新增文档离线版下载。 + - 新增:新增框架功能列表插图。 + - 新增:新增示例:如何在响应式环境下的 Filter 里调用 Sa-Token 同步 API。 + - 新增:新增 QA:在 idea 导入源码,运行报错:java: 程序包cn.dev33.satoken.oauth2不存在。 + - 新增:新增 QA:新增QA:报错:SaTokenContext 上下文尚未初始化。 + - 新增:新增 QA:在 idea 导入源码,运行报错:java: 程序包cn.dev33.satoken.oauth2不存在。 + - 新增:重写路由匹配算法修正为最新写法。 + - 新增:修复 OAuth2 UnionId 章节相关不正确描述。 + - 优化:完善 QA:访问了一个不存在的路由,报错:SaTokenContext 上下文尚未初始化。 fix: [#771](https://github.com/dromara/Sa-Token/issues/771) + - 优化:补充 sso 模块遗漏的配置字段介绍。 + - 优化:OAuth2-Server 示例添加真正表单。 + - 新增:文档新增重写 `PasswordGrantTypeHandler` 处理器示例。 + - 新增:sso 章节和 oauth2 章节文档增加可重写策略说明。 +- 其它: + - 新增:readme 新增框架功能介绍图。 + - 新增:SSO 模块新增思维导图说明。 + - 新增:readme 新增 Forest 的友情链接。 + + + + +### v1.42.0 @2025-4-11 +更新导读:[视频](https://www.bilibili.com/video/BV1h85izzEe8/)、[文字版](https://juejin.cn/post/7491971657201451062) + +- core: + - 新增: 新增 `API Key` 模块。 **[重要]** + - 新增: 新增 `TOTP` 实现。 **[重要]** + - 重构:重构 `TempToken` 模块,新增 value 反查 token 机制。 **[重要]** + - 升级: 重构升级 `SaTokenContext` 上下文读写策略。 **[重要]** + - 新增: 新增 Mock 上下文模块。 **[重要]** + - 删除: 删除二级上下文模块。 + - 新增: 新增异步场景使用 demo。 **[重要]** + - 新增: 新增 `Base32` 编码工具类。 + - 新增:新增 `CORS` 跨域策略处理函数,提供不同架构下统一的跨域处理方案。 + - 新增:`renewTimeout` 续期方法增加 token 终端信息有效性校验。 + - 新增: 全局配置项 `cookieAutoFillPrefix`:cookie 模式是否自动填充 token 前缀。 + - 新增: 全局配置项 `rightNowCreateTokenSession`:在登录时,是否立即创建对应的 `Token-Session`。 + - 优化:优化 `Token-Session` 获取算法,减少缓存读取次数。 + - 新增:`SaLoginParameter` 支持配置 `SaCookieConfig`,以配置 Cookie 相关参数。 + - 修改:防火墙校验过滤器的注册顺序 修改为 -102。 + - 新增:防火墙 `hook` 注册新增 `registerHookToFirst`、`registerHookToSecond` 方法,以便更灵活的控制 hook 顺序。 +- 插件: + - 新增: `sa-token-quick-login` 插件支持 `Http Basic` 方式通过认证。 +- 单元测试: + - 补全:补全 `Temp Token` 模块单元测试。 +- 文档: + - 补全:补全赞助者名单。 + - 修复:修复 `Thymeleaf` 集成文档不正确的依赖示例说明。 + - 修复:修复 `unionid` 章节错误描述。 + - 优化:采用更细致的描述优化SSO模式三单点注销步骤。 + - 新增:登录认证文档添加 Cookie 查看步骤演示图。 + - 新增:多账号模式新增注意点:运行时不可更改 `LoginType`。 + - 新增: 多账号模式QA:在一个接口里获取是哪个体系的账号正在登录。 + - 新增:新增QA:解决低版本 `SpringBoot (<2.2.0)` 引入 Sa-Token 报错的问题。 + - 新增:新增QA:前后端一体项目下,在拦截未登录进入登录页面时,如何登录完成后原路返回? + - 新增:新增QA:Sa-Token 集成 Redis 如何集群? + - 新增:新增QA:如何自定义框架读取 token 的方式? + - 新增:新增QA:修改 `hosts` 文件无效可能原因排查。 + - 新增:新增QA:如何防止 CSRF 攻击。 + - 新增: “异步 & Mock 上下文” 章节。 + - 升级:升级“自定义 SaTokenContext 指南”章节文档。 + + + +### v1.41.0 @2025-3-21 +更新导读:[视频](https://www.bilibili.com/video/BV1aNo4YCEM1/)、[文字版](https://juejin.cn/post/7484191942358499368) + +- core: + - 修复:修复 `StpUtil.setTokenValue("xxx")`、`loginParameter.getIsWriteHeader()` 空指针的问题。 fix: [#IBKSM0](https://gitee.com/dromara/sa-token/issues/IBKSM0) + - 修复:将 `SaDisableWrapperInfo.createNotDisabled()` 默认返回值封禁等级改为 -2,以保证向之前版本兼容。 + - 新增:新增基于 SPI 的插件体系。 **[重要]** + - 重构:JSON 转换器模块。 **[重要]** + - 新增:新增 serializer 序列化模块,控制 `Object` 与 `String` 的序列化方式。 **[重要]** + - 重构:重构防火墙模块,增加 hooks 机制。 **[重要]** + - 新增:防火墙新增:请求 path 禁止字符校验、Host 检测、请求 Method 检测、请求头检测、请求参数检测。重构目录遍历符检测算法。 + - 重构:重构 `SaTokenDao` 模块,将序列化与存储操作分离。 **[重要]** + - 重构:重构 `SaTokenDao` 默认实现类,优化底层设计。 + - 新增:`isLastingCookie` 配置项支持在全局配置中定义了。 + - 重构:`SaLoginModel` -> `SaLoginParameter`。 **[不向下兼容]** + - 重构:`TokenSign` -> `SaTerminalInfo`。 **[不向下兼容]** + - 新增:`SaTerminalInfo` 新增 `extraData` 自定义扩展数据设置。 + - 新增:`SaLoginParameter` 支持配置 `isConcurrent`、`isShare`、`maxLoginCount`、`maxTryTimes`。 + - 新增:新增 `SaLogoutParameter`,用于控制注销会话时的各种细节。 **[重要]** + - 新增:新增 `StpLogic#isTrustDeviceId` 方法,用于判断指定设备是否为可信任设备。 + - 新增:新增 `StpUtil.getTerminalListByLoginId(loginId)`、`StpUtil.forEachTerminalList(loginId)` 方法,以更方便的实现单账号会话管理。 + - 升级:API 参数签名配置支持自定义摘要算法。 + - 新增:新增 `@SaCheckSign` 注解鉴权,用于 API 签名参数校验。 + - 新增:API 参数签名模块新增多应用模式。 fix: [#IAK2BI](https://gitee.com/dromara/sa-token/issues/IAK2BI), [#I9SPI1](https://gitee.com/dromara/sa-token/issues/I9SPI1), [#IAC0P9](https://gitee.com/dromara/sa-token/issues/IAC0P9) **[重要]** + - 重构:全局配置 `is-share` 默认值改为 false。 **[不向下兼容]** + - 重构:踢人下线、顶人下线默认将删除对应的 token-session 对象。 + - 优化:优化注销会话相关 API。 + - 重构:登录默认设备类型值改为 DEF。 **[不向下兼容]** + - 重构:`BCrypt` 标注为 `@Deprecated`。 + - 新增:`sa-token-quick-login` 支持 `SpringBoot3` 项目。 fix: [#IAFQNE](https://gitee.com/dromara/sa-token/issues/IAFQNE)、[#673](https://github.com/dromara/Sa-Token/issues/673) + - 新增:`SaTokenConfig` 新增 `replacedRange`、`overflowLogoutMode`、`logoutRange`、`isLogoutKeepFreezeOps`、``isLogoutKeepTokenSession`` 配置项。 +- OAuth2: + - 重构:重构 sa-token-oauth2 插件,使注解鉴权处理器的注册过程改为 SPI 插件加载。 +- 插件: + - 新增:`sa-token-serializer-features` 插件,用于实现各种形式的自定义字符集序列化方案。 + - 新增:`sa-token-fastjson` 插件。 + - 新增:`sa-token-fastjson2` 插件。 + - 新增:`sa-token-snack3` 插件。 + - 新增:`sa-token-caffeine` 插件。 +- 单元测试: + - 新增:`sa-token-json-test` json 模块单元测试。 + - 新增:`sa-token-serializer-test` 序列化模块单元测试。 +- 文档: + - 新增:QA “多个项目共用同一个 redis,怎么防止冲突?” + - 优化:补全 OAuth2 模块遗漏的相关配置项。 + - 优化:优化 OAuth2 简述章节描述文档。 + - 优化:完善 “SSO 用户数据同步 / 迁移” 章节文档。 + - 修正:补全项目目录结构介绍文档。 + - 新增:文档新增 “登录参数 & 注销参数” 章节。 + - 优化:优化“技术求助”按钮的提示文字。 + - 新增:新增 `preview-doc.bat` 文件,一键启动文档预览。 + - 完善:完善 Redis 集成文档。 + - 新增:新增单账号会话查询的操作示例。 + - 新增:新增顶人下线 API 介绍。 + - 新增:新增 自定义序列化插件 章节。 +- 其它: + - 新增:新增 `sa-token-demo/pom.xml` 以便在 idea 中一键导入所有 demo 项目。 + - 删除:删除不必要的 `.gitignore` 文件 + - 重构:重构 `sa-token-solon-plugin` 插件。 + - 新增:新增设备锁登录示例。 + + +### v1.40.0 @2025-2-1 +更新导读:[视频](https://www.bilibili.com/video/BV1uNATeeEvg/)、[文字版](https://juejin.cn/post/7467969744307306505) + +- core: + - 新增:新增 `Cookie` 自定义属性支持。 fix: [#693](https://github.com/dromara/Sa-Token/issues/693) **[重要]** + - 新增:`SaFirewallStrategy` 防火墙策略:请求 path 黑名单校验、非法字符校验、白名单放行。 **[重要]** + - 修复:新增对分号字符的 path 路径校验。 参考:[Sa-Token对url过滤不全存在的风险点](https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA) **[漏洞修复]** + - 修复: 修复部分场景下登录后已存在的 `token-session` 没有被续期的问题。 fix: [#IA8U1O](https://gitee.com/dromara/sa-token/issues/IA8U1O) + - 优化:优化 `active-timeout` 的检查与续期操作,同一请求内只会检查与续期一次。 + - 修复:`SaFoxUtil.joinSharpParam` 方法中不正确的注释。 + - 新增:封禁模块新增支持实时从数据库查询数据。 +- SSO: + - 优化:SSO 示例代码的跨域处理由原生方式改为 Sa-Token 过滤器模式。 + - 新增:文档新增 “SSO整合 - NoSdk 模式与非 java 项目” 章节。 + - 新增:“不同 SSO Client 配置不同秘钥” 章节增加部分异常的处理方案提示,fix: [#IAFZXL](https://gitee.com/dromara/sa-token/issues/IAFZXL) + - 删除:sso demo 示例中部分不必要的代码内容。 +- OAuth2: + - 新增:OAuth2 Client 前端测试页。 **[重要]** + - 新增:`UnionId` 联合id 实现。 **[重要]** + - 新增:`oauth2-server` 端前后台分离示例与文档。 fix: [#I9DQGA](https://gitee.com/dromara/sa-token/issues/I9DQGA)、[#I9W2RU](https://gitee.com/dromara/sa-token/issues/I9W2RU) **[重要]** + - 新增:`OIDC` 模式 `nonce` 随机数响应校验。 merge: [pr311](https://gitee.com/dromara/sa-token/pulls/311) + - 修复:错误方法名 `deleteGrantScope(String state)` -> `deleteState(String state)`。 + - 修复:全局配置项 `sa-token.oauth2-server.oidc.iss` 无效的问题。 + - 新增:回收 Refresh-Token 方法: `revokeRefreshToken`、`revokeRefreshTokenByIndex`。 + - 新增:为 `CodeModel`、`AccessTokenModel`、`RefreshTokenModel`、`ClientTokenModel` 添加 `createTime` 字段,以记录该数据的创建时间。 + - 新增:为 Access-Token、Client-Token 添加 `grantType` 字段,以记录该数据的授权类型。 + - 新增:`SaOAuth2Util.getCode` 等方法,以更方便的获取、校验授权码。 +- 插件: + - 新增:新增 `sa-token-freemarker` 插件,整合 `Freemarker` 视图引擎。 fix: [#651](https://github.com/dromara/sa-token/issues/651) **[重要]** + - 新增:新增 `sa-token-spring-el` 插件,用于支持 SpEL 表达式注解鉴权。 fix: [#IB3GBB](https://gitee.com/dromara/sa-token/issues/IB3GBB)、fix: [#IAIXSL](https://gitee.com/dromara/sa-token/issues/IAIXSL)、fix: [#I9P24F](https://gitee.com/dromara/sa-token/issues/I9P24F) **[重要]** +- 文档: + - 新增:新增 `MongoDB` 集成示例。 感谢 `@lilihao` 提供的示例。 merge: [pr322](https://gitee.com/dromara/sa-token/pulls/322)、[pr667](https://github.com/dromara/Sa-Token/pull/667) **[重要]** + - 新增:“fox说技术” 视频教程链接。 + - 新增:“API接口参数签名”章节 视频讲解链接(B站抓蛙师)。 + - 优化:文档首页首屏增加需求提交按钮。 + - 其它:补全赞助者名单、`Dromara` 项目链接等信息。 + - 新增:`SpringBoot3.x` 版本配置 Redis 注意事项。fix: [#688](https://github.com/dromara/Sa-Token/issues/688) + - 新增:`gitcode` g-star badge 展示。 + - 修复:`OAuth2` 滞后的配置信息示例。 + - 新增:新增视频账号链接。 + - 新增:新增团队成员展示。 + + + + +### v1.39.0 @2024-8-28 +- 核心: + - 升级:重构注解鉴权底层,支持自定义鉴权注解了。 **[重要]** + - 修复:修复前端提交同名 `Cookie` 时的框架错读现象。 + - 更名:`NotBasicAuthException` -> `NotHttpBasicAuthException`。 +- 插件: + - 修复:修复 `sa-token-quick-login` 插件无法正常拦截的问题。 +- SSO: + - 优化:优化 sso-server 前后端分离 demo 代码。 + - 优化:优化 sso-server 前后端分离时的跳转流程。 +- OAuth2: + - 重构:`sa-token-oauth2` 模块整体重构。 **[重要]** **[不向下兼容]** + - 新增:新增支持自定义 `scope` 处理器。 **[重要]** + - 新增:新增支持自定义 `grant_type`。 **[重要]** + - 新增:新增 `scope` 划分等级。 **[重要]** + - 新增:新增 `oidc` 协议支持。 **[重要]** + - 新增:新增支持默认 `openid` 生成算法。 **[重要]** + - 新增:新增 `OAuth2` 注解鉴权支持。 **[重要]** + - 修复:`redirect_url` 参数校验增加规则:不允许出现@字符、*通配符只能出现在最后一位。关联 [issue](https://github.com/dromara/Sa-Token/issues/529) **[重要]** + - 优化:优化代码注释、异常提示信息。 + - 升级:兼容 `Http Basic` 提交 `client` 信息的场景。感谢 github `@CuiGeekYoung` 提交的pr。 + - 升级:兼容 `Bearer Token` 方式提交 `access_token` 和 `client_token`。 + - 升级:适配拆分式路由。 + - 新增:将 `scope` 字段改为 List 类型。 + - 重构:抽离 `SaOAuth2Strategy` 全局策略接口,定义一些创建 token 的算法策略。 + - 新增:新增 `addAllowUrls` `addContractScopes` 方法,简化 `SaClientModel` 构建代码。 + - 重构:抽离 `SaOAuth2Dao` 接口,负责数据持久。 + - 重构:抽离 `SaOAuth2DataLoader` 数据加载器接口。 + - 重构:抽离 `SaOAuth2DataGenerate` 数据构造器接口。 + - 重构:抽离 `SaOAuth2DataConverter` 数据转换器接口。 + - 重构:抽离 `SaOAuth2DataResolver` 数据解析器接口。 + - 重构:重构 `SaOAuth2Handle` -> `SaOAuth2ServerProcessor` 更方便的逻辑重写。 + - 重构:重构 `PastToken` -> `LowerClientToken`。 + - 新增:新增 `state` 值校验,同一 `state` 参数不可重复使用。 + - 优化:完善 `SaOAuth2Util` 相关方法,更方便的二次开发。 + - 新增:新增部分异常类,细分异常 `ClassType`。 + - 优化:优化 `sa-token-oauth2` 异常细分状态码。 +- 文档: + - 新增:新增数据结构说明。 + - 新增:新增不同 `client` 不同登录页说明。 + - 优化:优化文档 [将权限数据放在缓存里] 示例。 + - 新增:新增 从 Shiro、SpringSecurity、JWT 迁移 示例。 **[重要]** + + + +### v1.38.0 @2024-5-12 +- sa-token-core: + - 修复:修复 `StpUtil.getSessionByLoginId(xx)` 参数为 null 时创建无效 `SaSession` 的 bug。 + - 优化:在 `SpringBoot 3.x` 版本下错误的引入依赖时将得到启动失败的提示。 (感谢`Uncarbon`提交的pr) + - 优化:进一步优化权限校验算法,hasXxx API 只会返回 true 或 false,不再抛出异常。 + - 重构:`InvalidContextException` 更名为 `SaTokenContextException`。 **[已做向下兼容处理]** + - 重构:彻底删除 `NotPermissionException` 异常中的 `getCode()` 方法。 **[过期API清理]** + - 重构:重构 `SaTokenException` 类方法 `throwBy-`>`notTrue`、`throwByNull-`>`notEmpty`。**[已做向下兼容处理]** + - 重构:`StpUtil.getSessionBySessionId` 提供的 `SessionId` 为空时将直接抛出异常,而不是再返回null。**[不向下兼容]** + - 新增:新增 `Http Digest` 认证模块简单实现。 **[重要]** + - 重构:更换 `HttpBasic` 认证模块包名。 **[已做向下兼容处理]** + - 新增:新增 `StpUtil.getLoginDeviceByToken(xxx)` 方法,用于获取任意 token 的登录设备类型。 + - 新增:新增 `StpUtil.getTokenLastActiveTime()` 方法,获取当前 token 最后活跃时间。 + - 修复:修复“当登录时指定 timeout 小于全局 timeout 时,`Account-Session` 有效期为全局 timeout”的问题。 + - 优化:首次获取 `Token-Session` 时,其有效期将保持和 token 有效期相同,而不是再是全局 timeout 值。 + - 移除:移除 `SaSignConfig` 的 `isCheckNonce` 配置项。 **[不向下兼容]** + - 新增:`SaSignTemplate#checkRequest` 增加“可指定参与签名参数”的功能。 + - 重构:将部分加密算法设置为过期。 + - 重构:优化 token 读取策略,空字符串将视为没有提交token。 + - 修复:`sa-token-bom` 补全缺失依赖。 + - 优化:二级认证校验之前必须先通过登录认证校验。 + - 修复:修复 `StpUtil.getLoginId(T defaultValue)` 传入 null 时无法正确返回值的bug。 +- sa-token-sso: + - 优化:SSO 模式三,API 调用签名校验时,限定参与签名的参数列表,更安全。 + - 新增:新增 `autoRenewTimeout` 配置项:是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) + - 新增:`SaSsoConfig` 新增配置 `isCheckSign`(是否校验参数签名),方便本地开发时的调试。 + - 新增:`SaSsoConfig` 新增配置 `currSsoLogin`,用于强制指定当前系统的 sso 登录地址。 + - 重构:整体重构 `sa-token-sso` 模块,将 `server` 端和 `client` 端代码拆分。 **[重要]** **[不向下兼容]** + - 新增:`SaSsoConfig` 配置项 `ssoLogoutCall` 重命名为 `currSsoLogoutCall`。**[已做向下兼容处理]** + - 重构:模式三在校验 Ticket 时,也将强制校验签名才能调通请求。**[不向下兼容]** + - 新增:新增 `maxRegClient` 配置项,用于控制模式三下 client 注册数量。 + - 新增:新增不同 SSO Client 配置不同 `secret-key` 的方案。 **[重要]** + - 重构:匿名 client 将不再能解析出所有应用的 ticket。**[不向下兼容]** + - 新增:新增 `homeRoute` 配置项:在 ``/sso/auth`` 登录后不指定 redirect 参数的情况下默认跳转的路由。 + - 优化:优化登录有效期策略,SSO Client 端登录时将延续 SSO Server 端的会话剩余有效期。 + - 新增:新增 `checkTicketAppendData` 策略函数,用于在校验 ticket 后,给 sso-client 端追加返回信息。 + - 新增:SSO章节文档新增用户数据同步/迁移方案的建议。 + - 修复:修复利用@字符可以绕过域名允许列表校验的漏洞。 **[漏洞修复]** + - 修复:禁止 `allow-url` 配置项 * 符号出现在中间位置,因为这有可能导致校验被绕过。 **[漏洞修复]** +- 新增插件/示例: + - 新增:新增插件 `sa-token-hutool-timed-cache`,用于整合 Hutool 缓存插件 TimedCache。 **[重要]** + - 新增:新增 SSM 架构整合 Sa-Token 简单示例。 **[重要]** + - 新增:新增 beetl 整合 Sa-Token 简单示例。 **[重要]** +- 文档: + - 部分章节将 `@Autowired` 更换为更合适的 `@PostConstruct` + - 新增过滤器执行顺序更改示例。 +- 其它: + - 优化:将跨域处理demo拆分为独立仓库。 + - 优化:解决 springboot 集成 sa-token 后排除 jackson 依赖无法成功启动的问题。 + - 优化:解决 `sa-token-jwt` 模块重复设置 keyt 秘钥问题。(感谢`KonBAI`提交的pr) + - 优化:jwt模式 token 过期后,抛出的异常描述是 token 已过期,而不再是 token 无效。 + - 修复:补齐 `sa-token-spring-aop` 模块中遗漏监听的注解。 + + +### v1.37.0 @2023-10-18 +- 修复:修复路由拦截鉴权可被绕过的问题。 **[漏洞修复]** +- 重构:未登录时调用鉴权 API 抛出未登录异常而不再是无权限异常。 +- 优化:优化 SaTokenDao 组件更换时的逻辑。 +- 文档:提供 SpringBoot3.x 路由匹配出错的解决方案。 + + +### v1.36.0 @2023-9-22 +- sa-token-core: + - 修复:API接口签名校验参数接口NPE问题,增加必须参数的非空校验处理。 + - 新增:加密工具类新增 sha384、sha512 实现。 感谢 `@若初995` 提交的pr。 **[重要]** + - 修复:`SaFoxUtil.vagueMatch()` 正则匹配的一些问题。 **[漏洞修复]** + - 修复:`SaRouter.match()` 路由匹配的一些问题。 **[漏洞修复]** +- 其它: + - 优化:`sa-token-alone-redis` 去掉不必要的配置项判断。 + - 新增:`sa-token-solon-plugin` 增加对 solon 网关的支持。 + - 新增:新增第三方插件专用仓库:`sa-token-three-plugin` 。 + - 升级:`sa-token-solon-plugin` 增加对 solon 网关的支持。 +- 文档: + - 新增:新增开启全局懒加载时不能注入上下文处理器的处理方案 。 + - 新增:新增 RefreshToken 示例。 **[重要]** + - 新增:文档新增 sa-token 小助手,可在线实时技术提问。 **[重要]** + - 优化:其它一些优化。 +- 新增插件: + - `sa-token-redisson-jackson2`:通用 redisson 集成方案 (spring, solon, jfinal 等都可用) + + + +### v1.35.0 @2023-6-23 +- sa-token-core: + - 优化:前端未提供 token 时,`getTokenSession()` 将抛出未登录异常,而不是返回 null。 **[不向下兼容]** + - 新增:SaSession 新增字段:`type`、`loginType`、`loginId`、`token`。 + - 重构:全局过滤器抽离 SaFilter 统一接口。 + - 重构:全局过滤器 `includeList`、`excludeList` 改为 public,同时移除对应的 getter 方法。 **[不向下兼容]** + - 重构:将全局过滤器的 BeforeAuth 认证设为不受 `includeList` 与 `excludeList` 的限制,所有请求都会进入。 **[不向下兼容]** + - 新增:新增循环生成 token 的算法,用于确保 Token 的唯一性。 **[重要]** + - 重构:API 接口签名所有方法均迁移至 core 核心模块。 **[重要]** + - 新增:新增彩色日志打印,更方便的分辨不同日志等级。 **[重要]** + - 重构:重构概念:临时有效期 -> token 最低活跃频率,过期后 token 冻结。 + - 重构:重构概念:`User-Session` -> `Account-Session`。 + - 新增:新增 `getTokenTimeout(String token)` 方法,获取任意 token 剩余有效期。 + - 优化:在登录时增加判断当前 StpLogic 是否支持 extra 扩展参数模式,如果不支持则打印警告信息。 + - 新增:NotLoginException 增加新场景值 -6、-7,提供更精确的未登录异常描述信息。 + - 新增:TokenSign 新增 tag 挂载参数,可在登录时方便的存储一些客户端特有数据。 **[重要]** + - 新增:新增 `SaStrategy#createStpLogic`,用于指定动态创建 StpLogic 时的算法策略。 + - 新增:新增 `@SaCheckOr` 批量注解鉴权:只要满足其中一个注解即可通过验证。 **[重要]** + - 重构:重命名:`SaStrategy.me` -> `SaStrategy.instance`。 + - 重构:在登录时强制性检查账号 id 是否为异常值,如果是则登录失败。 + - 重构:重构概念:`activity-timeout` -> `active-timeout`。 **[重要]** + - 新增:新增动态 `active-timeout` 能力,可在每次登录时指定 `active-timeout` 值。 **[重要]** + - 优化:将 `SaStrategy` 所有策略声明抽离为单独的函数式接口。 + - 新增:增加为 StpLogic 单独配置 `SaTokenConfig` 参数的能力。 + +- sa-token-sso: + - 修复:在 SSO 模式三中 `ticket` 校验地址配错时,会出现 NPE 的问题 + - 新增:新增 `getData` 接口配置,在模式三拉取数据时可以传递任意参数。 **[重要]** + - 重构:模式三秘钥配置更改:`sa-token.sso.secretkey=xxx` -> `sa-token.sign.secret-key=xxx`。 **[不向下兼容]** + - 重构:模式三校验签名方法更改:`SaSsoUtil.checkSign(req)` -> `SaSignUtil.checkRequest(req)`。 **[不向下兼容]** + - 新增:新增 `sa-token.sso.mode` 配置项,用于约定此系统使用的 SSO 模式。 + - 优化:优化校验 ticket 的逻辑。 + +- sa-token-jwt: + - 修复:jwt 令牌的签名类型可以被篡改的问题。 **[重要]** + +- 其它: + - 优化:所有模块优化注释,更方便开发者阅读源码。 + - 优化:在所有 .java 文件中添加 license 头说明。 + - 优化:修复大部分代码警告。 + - 新增:新增 thymeleaf 标签方言命名空间,增强 ide 代码提示。 **[重要]** + - 新增:定义 `sa-token-bom` 包,方便引入 sa-token 时对齐版本。 + - 新增:sa-token-dubbo3 插件新增代码示例。 + - 新增:新增跨域文章和示例:Header 参数版和第三方 Cookie 版。 **[重要]** + - 修复:修复 `sa-token-alone-redis` 在低版本 springboot 下无法启动成功(缺少 `username` 属性)的问题。 + +- 新增插件: + - 新增:新增 `sa-token-context-dubbo3` 插件。 感谢 `@qiudaozhang` 提交的 pr。 **[重要]** + +- 文档: + - 新增:部分常见报错排查。 + - 新增:首页图片增加懒加载效果,节省流量。 + - 新增:增加 Cookie 配置示例。 + - 修复:整理 demo 结构目录结构,修复不正确的路径说明。 + - 新增:新增 api-sign 模块文档。 **[重要]** + +- 简化包名 **[重要]** **[不向下兼容]** + - `sa-token-dao-redis` -> `sa-token-redis` + - `sa-token-dao-redis-jackson` -> `sa-token-redis-jackson`。 + - `sa-token-dao-redis-fastjson` -> `sa-token-redis-fastjson`。 + - `sa-token-dao-redis-fastjson2` -> `sa-token-redis-fastjson2`。 + - `sa-token-dao-redisson-jackson` -> `sa-token-redisson-jackson`。 + - `sa-token-dao-redisx` -> `sa-token-redisx`。 + - `sa-token-context-dubbo` -> `sa-token-dubbo`。 + - `sa-token-context-dubbo3` -> `sa-token-dubbo3`。 + - `sa-token-context-grpc` -> `sa-token-grpc`。 + + +### v1.34.0 @2023-1-11 + +新增插件: +- 新增:新增 `SpringBoot3.x` 集成插件,感谢 `@jry` 提供的参考思路。 **[重要]** +- 新增:新增 `sa-token-dao-redisson-jackson` 插件,感谢 `@疯狂的狮子Li` 提交的pr。 **[重要]** + +sa-token-core 核心包: +- 升级:升级 Sign 签名模块,增加部分重载方法。 +- 重构:`SaSignTemplate#joinParams` 更名为 `joinParamsDictSort`。 **[不向下兼容]** +- 升级:升级临时 Token 认证模块,可指定 service 参数。 +- 删除:彻底删除过期类 `SaAnnotationInterceptor` 和 `SaRouteInterceptor`。 +- 修复:修复源码注释和文档的部分不合适之处。 + +sa-token-sso 单点登录: +- 删除:SSO 模块移除过期类 `SaSsoHandle` 类。 +- 新增:SSO 模块增加 ticket 的 client 锁定功能,解决部分场景下的 ticket 劫持问题。 **[重要]** +- 修复:修复 SSO 模式2,在 client 端配置 `is-share=false` 时无法单点注销的问题。 +- 修复:修复 SSO 模式3 部分场景下注销时无法正常回退页面的问题。 + +其它模块: +- sa-token-oauth2:修复 OAuth2 模块示例 getClientModel 方法 clientId 写错的问题。 +- sa-token-alone-redis:新增:Alone-Redis 新增集群配置能力,感谢 `@appleOfGray` 提交的pr。 **[重要]** +- sa-token-jwt:重构:使用 jwt-simple 模式后 is-share 恒等于 false,无论是否有设定 `setExtra` 数据。 + + + +### v1.33.0 @2022-11-16 +- 重构:重构异常状态码机制。 **[重要]** +- 重构:重构 sa-token-sso 模块异常码改为 300 开头,sa-token-jwt 异常码改为 302 开头。 **[不向下兼容]** +- 新增:新增全局 Log 模块。 **[重要]** +- 重构:`SaTokenListenerForConsolePrint` 改名 `SaTokenListenerForLog`。 **[不向下兼容]** +- 修复:修复多线程下 `SaFoxUtil.getRandomString()` 随机数重复问题。 +- 修复:修复 sa-token-demo-sso3-client-nosdk 项目中单点注销 url 配置错误的问题 +- 文档:文档优化。 + + + +### v1.32.0 @2022-10-28 +- 修复:修复 sa-token-dao-redis-fastjson 插件多余序列化 `timeout` 字段的问题。 +- 修复:修复 sa-token-dao-redis-fastjson 插件 `session.getModel` 无法反序列化实体类的问题。 +- 修复:修复 `sa-token-quick-login` 插件指定拦截排除路由不生效的问题。 +- 修复:修复 `sa-token-alone-redis` + `sa-token-dao-redis-fastson` 时 Redis 无法分离的问题。 +- 修复:修复在配置了 cookie.path 后,注销时无法彻底清除 Cookie 的问题。 +- 升级:`SaFoxUtil.getValueByType()` 新增对 char 类型的转换。 +- 新增:新增 `sa-token-dao-redis-fastjson2` 插件。 **[重要]** +- 新增:新增全局配置 `is-write-header`,控制登录后是否将 Token 写入响应头。 **[重要]** +- 新增:二级认证模块新增指定业务标识能力。 **[重要]** +- 重构:Id-Token 模块更名为 Same-Token。 **[重要]** **[不向下兼容]** +- 重构:重构会话查询参数作用:由`start=-1`时查询全部会话,改为 `start=0,size=-1` 时查询全部。 **[不向下兼容]** +- 重构:`SaManager.getStpLogic("type")` 默认当对应type不存在时不再抛出异常,而是自动创建并返回。 +- 重构:重构SSO模块,静态式API改为实例式:SaSsoHandle -> SaSsoProcessor。 **[重要]** **[不向下兼容]** +- 重构:SSO-Server 端单点注销地址修改 `/sso/logout` -> `/sso/signout`,避免与 SSO-Client 端同 path 的冲突。 **[不向下兼容]** +- 新增:文档新增 SSO 平台中心模式示例,跳连接进入子系统。 **[重要]** +- 新增:新增SSO前后端分离集成示例 vue2 & vue3 版本。 **[重要]** +- 重构:SSO 示例项目 http 请求工具改为 Forest。 +- 新增:SSO模块文档新增单个项目同时搭建 `sso-server` 和 `sso-client` 的示例。 **[重要]** +- 新增:SSO模块文档新增一个项目同时搭建两个 `sso-server` 服务 的示例。 **[重要]** +- 文档:在线文档新增代码示例。 +- 文档:在线文档增加全局调色功能。 +- 文档:[自定义 SaTokenContext 指南] 章节新增对三种模型的解释。 +- 文档:新增多账号体系混合鉴权代码示例。 +- 文档:文档增加 `Gradle` 依赖方式和 `properties` 风格配置。 +- 新增:新增 `sa-token-dependencies`,统一定义依赖版本。 **[重要]** + +##### 已知问题: + +> 部分场景下 Token 重复问题,受影响版本 `=v1.32.0` +> - 受影响模块: +> - sa-token-core 切换了 Token 风格:tik、random-32、random-64、random-128,如果使用 默认uuid、simple-uuid 风格则不受影响。 +> - sa-token-core 使用了临时 Token 认证模块,如果集成了 sa-token-temp-jwt 则不受影响。 +> - sa-token-core 使用了 Same-Token 模块。 +> - sa-token-jwt 全模块 +> - sa-token-oauth2 全模块 +> - sa-token-sso 模式二和模式三 + + + +### v1.31.0 @2022-9-8 +- 文档:新增优秀开源案例展示。 +- 文档:新增博客展示,欢迎大家投稿。 +- 新增:新增 `SaInterceptor` 综合拦截器。 **[重要]** **[不向下兼容]** +- 新增:新增 新增 `@SaIgnore` 忽略鉴权注解。 **[重要]** +- 新增:新增插件 `sa-token-dao-redis-fastjson`,感谢 `@sikadai` 提交的pr。 **[重要]** +- 新增:新增插件 `sa-token-context-grpc`,感谢 `@LiYiMing666` 提交的pr。 **[重要]** +- 重构:SaSession 取消 `tokenSignList` 的 final 修饰符。 +- 新增:SaSession 添加 `setTokenSignList` 方法。 +- 重构:TokenSign 新增 `setValue` 和 `setDevice` 方法。 +- 修复:修复多账号模式下不能正确重置 `StpLogic` 的问题。 +- 修复:修复 SaSession 对象中 TokenSign 判断有可能空指针的问题。 +- 修复:解决当权限码为 null 时可能带来的空指针问题。 +- 新增:新增 `StpUtil.getExtra(tokenValue, key)` 方法,用于获取任意 token 的扩展参数。 +- 优化:优化 `StpLogic#logoutByTokenValue` 方法逻辑,精简代码。 +- 重构:`SaTokenConfig` 配置类字段 `isReadHead` 改为 `isReadHeader`。 **[不向下兼容]** +- 修复:修复部分场景下踢人下线会抛出异常 `非Web上下文无法获取Request` 的问题。 +- 新增:新增方法 `StpLogic#getAnonTokenSession`,可在未登录情况下安全的获取 Token-Session。 **[重要]** +- 新增:新增 `SaApplication` 对象,用于全局作用域存取值。 **[重要]** +- 重构:将 `SaTokenListener` 改为事件发布订阅模式,允许同时注册多个侦听器。 **[重要]** **[不向下兼容]** +- 重构:**StpUtil.login(id) 不再强制校验账号是否禁用,需要手动校验。** **[不向下兼容]** +- 重构:`DisableLoginException` 更换名称为 `DisableServiceException`。 **[不向下兼容]** +- 新增:新增对账号限制、分类封禁、阶梯封禁功能。 **[重要]** +- 新增:会话查询API增加反序获取会话方式。 +- 新增:SSO模块增加 server-url 属性,用于简化各种 url 配置。 **[重要]** +- 修复:修复单点登录模块 `ssoLogoutCall` 配置项无效的问题。 +- 优化:优化 `SaSsoHandle.checkTicket(ticket, currUri);` 方法,使其不提供 currUri 参数时将不再注册单点注销回调。 +- 修复:修复 `SaOAuth2Handle` 类中 `doLogin` 方法没有使用 `Param.pwd` 常量的问题。 +- 新增:新增 `SaOAuth2Util.checkClientTokenScope(clientToken, scopes)` 方法,校验 Client-Token 是否含有指定 Scope。 +- 删除:删除 `sa-token-jwt` 模块过期 class。 +- 重构:`sa-token-jwt` 模块依赖改为 `hutool-jwt`,并升级版本为 5.8.5。 +- 重构:`sa-token-jwt` 模块改为 `Util + Template` 形式,方便针对部分代码重写。 **[重要]** +- 新增:在线文档添加API手册。 +- 重构:`sa-token-oauth2` 模块密码模式新增 `client_secret` 参数校验。**[不向下兼容]** +- 新增:集成 `jacoco` 插件,核心包单元测试覆盖率提高至 90% 以上。 +- 优化:开源案例分离专属仓库:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) + + + +### v1.30.0 @2022-05-9 +- 新增:新增集成 Web-Socket 鉴权示例。 **[重要]** +- 新增:新增集成 Web-Socket(Spring封装版) 鉴权示例。 +- 新增:新增 jfinal 集成包 `sa-token-jfinal-plugin` **[重要]** +- 新增:新增 jboot 集成包 `sa-token-jboot-plugin` (感谢 @nxstv 提交的pr) +- 修复:修复整合 sa-token-jwt Style 模式时,`StpUtil.getExtra("key")` 无效的bug +- 升级:升级 `sa-token-context-dubbo` dubbo版本:`2.7.11` -> `2.7.15` +- 升级:借助 `flatten-maven-plugin` 统一版本号定义 (感谢 @ruansheng8 提交的pr) **[重要]** +- 修复:修复在 `springboot 2.6.x` 下 `quick-login` 插件循环依赖无法启动的问题 +- 优化:`sa-token-spring-aop` 依赖改为 `sa-token-core`,避免在webflux环境下启动报错的问题 +- 优化:源码注释 设备标识 改为 设备类型 更符合语义 +- 修复:解决部分协议下 dubbo 参数变为小写导致 `Id-Token` 鉴权无效的问题 +- 升级:单元测试升级为 JUnit5 +- 新增:新增 `maxLoginCount` 配置,指定同一账号可同时在线的最大数量 **[重要]** +- 升级:彻底删除 SaTokenAction 接口,完全由 SaStrategy 代替 +- 新增:新增 `sa-token-dao-redisx` 插件,感谢 @noear 提交的pr **[重要]** +- 优化:增加 parseToken 未配置 jwt 密钥时的异常提示,感谢 @BATTLEHAWK00 提交的pr +- 优化:sso,oauth2 插件中调用配置类使用 getter 方法,感谢 @Naah 提交的pr +- 新增:新增 json 转换器模块 +- 重构:SaTokenListener#doLogin 方法新增 tokenValue 参数 **[不向下兼容]** +- 升级:SpringBoot 相关组件依赖版本升级至 `2.5.12` +- 文档:在线文档所有 `AjaxJson` 改为 `SaResult` +- 文档:“多账号认证” -> 改为 “多账户认证” +- 文档:部分章节新增动态演示图 **[重要]** +- 升级:顶级异常类 `SaTokenException` 增加 code 异常细分状态码。[详见](/fun/exception-code) **[重要]** +- **注意升级:受异常细分状态码影响,`NotPermissionException` 类中 `getCode()` 方法改为 `getPermission()`。** **[不向下兼容]** +- SSO 模块升级: + - 重构:SSO 模块从核心包拆分为独立插件 `sa-token-sso` **[重要]** + - 优化:SSO模式三单点注销回调方法中,注销语句改为:`stpLogic.logout(loginId)` 更符合情景 + - 修复:解决 sso 构建认证地址时,部分 Servlet 版本内部实现不一致带来的双 back 参数问题。 + - 升级:SSO 模块提供精细化异常处理 + - 重构:SSO 模式三接口 `/sso/checkTicket`、`/sso/logout`,更改响应体格式 **[不向下兼容]** + - 优化:SSO 模式三单点注销搭建示例增加 `try-catch`,提高容错性 + - 优化:`SsoUtil.singleLogout` 改为 `SsoUtil.ssoLogout`,且无需再提供 secretkey 参数 **[不向下兼容]** + - 升级:将 SSO 模式三的接口调用改为签名式校验。 **[重要]** **[不向下兼容]** + - 新增:新增 SSO 模式三下无 sdk 的对接示例, 感谢 @Sa-药水 的建议反馈 **[重要]** +- sa-token-jwt 模块升级: + - 重构:`sa-token-jwt` 的创建,强制校验loginType **[不向下兼容]** + - 重构:`StpLogicJwtForStateless` 由重写 login 方法改为重写 createLoginSession + - 重构:`SaJwtUtil` 工具类不再吞并异常消息,且提供精细化异常 code 码。 + - 重构:改名:StpLogicJwtForStyle -> StpLogicJwtForSimple + - 重构:改名:StpLogicJwtForMix -> StpLogicJwtForMixin + - 修复:修复 `StpLogicJwtForSimple` 模式下 Extra 数据可能受到旧 token 影响的bug + + +### v1.29.0 @2022-02-10 +- 升级:sa-token-jwt插件可在登录时添加额外数据。 +- 重构:优化Dubbo调用时向下传递Token的规则,可避免在项目启动时由于Context无效引发的bug。 +- 重构:OAuth2 授权模式开放由全局配置和Client单独配置共同设定。 +- 重构:OAuth2 模块部分属性支持每个 Client 单独配置。 +- 重构:OAuth2 模块部分方法名修复单词拼写错误:converXxx -> convertXxx。 +- 重构:修复 OAuth2 模块 `deleteAccessTokenIndex` 回收 token 不彻底的bug。 +- 新增:OAuth2 模块新增 `pastClientTokenTimeout`,用于指定 PastClientToken 默认有效期。 +- 文档:常见报错章节增加目录树,方便查阅。 +- 文档:优化文档样式。 +- 新增:新增 BCrypt 加密。 +- 修复:修复StpUtil.getLoginIdByToken(token) 在部分场景下返回出错的bug。 +- 重构:优化OAuth2模块密码式校验步骤。 +- 新增:新增Jackson定制版Session,避免timeout属性的序列化。 +- 新增:SaLoginModel新增setToken方法,用于预定本次登录产生的Token。 +- 新增:新增 StpUtil.createLoginSession() 方法,用于无Token注入的方式创建登录会话。 +- 新增:OAuth2 与 StpUtil 登录会话数据互通。 +- 新增:新增 `StpUtil.renewTimeout(100);` 方法,用于 Token 的 Timeout 值续期。 +- 修复:修复默认dao实现类中 `updateObject` 无效的bug +- 完善:完善单元测试。 + + +### v1.28.0 @2021-11-5 +- 新增:新增 `sa-token-jwt` 插件,用于与jwt的整合 **[重要]** +- 新增:新增 `sa-token-context-dubbo` 插件,用于与 Dubbo 的整合 **[重要]** +- 文档:文档新增章节:Sa-Token 插件开发指南 **[重要]** +- 文档:文档新增章节:名称解释 +- 优化:抽离 `getSaTokenDao()` 方法,方便重写 +- 新增:单元测试新增多账号模式数据不互通测试 +- 优化:优化在线文档,修复部分错误之处 +- 优化:优化未登录异常抛出提示,标注无效的Token值 +- 修复:修复单词拼写错误 `getDeviceOrDefault` +- 优化:优化 jwt 集成示例 +- 文档:新增常见问题总结 + + +### v1.27.0 @2021-10-11 +- 升级:增强 SaRouter 链式匹配能力 **[重要]** +- 新增:新增插件 Thymeleaf 标签方言 **[重要]** +- 新增:@SaCheckPermission 增加 orRole 字段,用于权限角色“双重or”匹配 **[重要]** +- 升级:Cookie模式增加 `secure`、`httpOnly`、`sameSite`等属性的配置 **[重要]** +- 重构:重构SSO三种模式,抽离出统一的认证中心 **[重要]** +- 新增:新增 SaStrategy 策略类,方便内部逻辑按需重写 **[重要]** +- 新增:临时认证模块新增 deleteToken 方法用于回收 Token +- 新增:新增 kickout、replaced 等注销会话的方法,更灵活的控制会话周期 **[重要]** +- 新增:权限认证增加API:`StpUtil.hasPermissionAnd`、`StpUtil.hasPermissionOr` +- 新增:角色认证增加API:`StpUtil.hasRoleAnd`、`StpUtil.hasRoleOr` +- 新增:新增 `StpUtil.getRoleList()` 和 `StpUtil.getPermissionList()` 方法 +- 新增:新增 StpLogic 自动注入特性,可快速方便的扩展 StpLogic 对象 +- 优化:优化同端互斥登录逻辑,如果登录时没有指定设备类型标识,则默认顶替所有设备类型下线 +- 优化:在未登录时调用 hasRole 和 hasPermission 不再抛出异常,而是返回false +- 升级:升级注解鉴权算法,并提供更简单的重写方式 +- 文档:新增常见报错排查,方便快速排查异常报错 +- 文档:文档新增SSO单点登录与OAuth2技术选型对比 +- 破坏式更新: + - [向下兼容] 废弃 SaTokenAction 接口,替代方案: SaStrategy + - [向下兼容] 移除 `StpUtil.logoutByLoginId()` 更换为 `StpUtil.kickout()`; + - [不向下兼容] 侦听器 doLogoutByLoginId 与 doReplaced 方法移除 device 参数 + - [不向下兼容] 侦听器 doLogoutByLoginId 方法重命名为 doKickout + + +### v1.26.0 @2021-9-2 +- 优化:优化单点登录文档 +- 新增:新增 `Http Basic` 认证 **[重要]** +- 新增:文档新增跨域解决方案 +- 文档:新增 Nginx 转发请求丢失uri的解决方案 +- 文档:新增 SSO 自定义 API 路由示例 **[重要]** +- 示例:新增 `SSO-Server` 端前后端分离示例 **[重要]** + + +### v1.25.0 @2021-8-16 +- 新增:`SaRequest`新增`getHeader(name, defaultValue)`方法,用于获取header默认值 +- 新增:`SaRequest` 添加 `forward` 转发方法 +- 新增:Readme新增源码模块介绍、友情链接、正在使用Sa-Token的项目 +- 重构:重构SSO单点登录模块源码,增加可读性 +- 新增:SSO配置表新增所属端说明 +- 新增:SSO模式三新增账号资料同步示例 **[重要]** +- 新增:前后端分离模式下接入SSO的示例 **[重要]** +- 优化:优化SSO单点注销重定向逻辑 +- 重构:重构SSO单点登录模块部分API +- 优化:优化SaQuickBean中过滤器处理逻辑 +- 文档:优化文档样式,增加示例 +- 文档:代码鉴权、注解鉴权、路由拦截鉴权,选择指南 +- 文档:文档新增 SSO旧有系统改造指南 +- 文档:SSO集成文档里添加API列表 +- 文档:新增 `Sa-Token-Study` 链接,讲解 Sa-Token 源码涉及到的技术点 +- 不兼容更新重构: + - 重构:修复 `SaReactorHolder.getContent()` 拼写错误:`content` -> `context` + + +### v1.24.0 @2021-7-24 +- 修复:修复部分场景下Alone-Redis插件导致项目无法启动的问题 +- 优化:增加对SpringBoot1.x版本的兼容性 +- 新增:SaOAuth2Util新增checkScope函数,用于校验令牌是否具备指定权限 +- 新增:OAuth2.0模块新增revoke接口,用于提前回收 Access-Token 令牌 +- 新增:新增`Sa-Id-Token` 模块,解决微服务内部调用鉴权 **[重要]** +- 文档:新增OAuth2.0模块常用方法说明 +- 优化:大幅度优化文档示例 + + +### v1.23.0 @2021-7-19 +- 新增:Sa-Token-OAuth2 模块正式发布 **[重要]** +- 修复:修复jwt集成demo无法正确注册StpLogic的bug +- 修复:修复登录时某些场景下Session续期可能不正常的bug +- 优化:代码注释优化,文档优化 + + +### v1.22.0 @2021-7-10 +- 新增:SaSsoConfig 部分属性增加set连缀风格 +- 优化:SaSsoUtil 可定制化底层的 `StpLogic` +- 新增:新增 `SaSsoHandle` 大幅度简化单点登录整合步骤 **[重要]** +- 新增:新增Sa-Token在线测评,链接:[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx) **[重要]** +- 新增:Sa-Token-Quick-Login 插件新增拦截与放行路径配置 +- 优化:大幅度优化文档示例 + + +### v1.21.0 @2021-7-2 +- 新增:新增Token二级认证 **[重要]** +- 新增:新增`Sa-Token-Alone-Redis`独立Redis插件 **[重要]** +- 新增:新增SSO三种模式,彻底解决所有场景下的单点登录问题 **[重要]** +- 新增:新增多账号模式下,注解合并示例 **[重要]** +- 新增:新增`SaRouter.back()`函数,用于停止匹配返回结果 +- 不兼容更新重构: + - 更改yml配置前缀:原`[spring.sa-token.]` 改为 `[sa-token.]`,目前版本暂时向下兼容,请尽快更新 + + +### v1.20.0 @2021-6-17 +- 新增:新增Solon适配插件,感谢大佬 `@刘西东` 提供的pr **[重要]** +- 新增:新增`SaRouter.stop()`函数,用于一次性跳出匹配链功能 **[重要]** +- 新增:新增单元测试 **[重要]** +- 新增:新增临时令牌验证模块 **[重要]** +- 新增:新增`sa-token-temp-jwt`模块整合jwt临时令牌鉴权 **[重要]** +- 新增:会话 `SaSession.get()` 增加缓存API,简化代码 +- 新增:新增框架调查问卷 +- 修复:修复同时引入 `Spring Cloud Bus` 与 `Sa-Token` 冲突的问题 **[重要]** +- 修复:修复`SaServletFilter`异常函数中无法自定义`Content-Type`的问题 +- 文档:新增微服务依赖引入说明 +- 文档:新增认证流程图 +- 不兼容更新重构: + - 方法:`StpUtil.setLoginId(id)` -> `StpUtil.login(id)` + - 方法:`StpUtil.getLoginKey()` -> `StpUtil.getLoginType()` (注意其它所有地方的`LoginKey`均已更改为`loginType`) + - 工具类:`SaRouterUtil` -> `SaRouter` + - 配置类:`allowConcurrentLogin` -> `isConcurrent` + - 配置类:`isV` -> `isPrint` + - 为保证平滑更新,旧API仍旧保留,但已增加`@Deprecated`注解,请尽快更新至新API + + +### v1.19.0 @2021-5-10 +- 新增:注解鉴权新增定制loginType功能 **[重要]** +- 重构:重构目录结构,抽离`plugin`模块 **[重要]** +- 新增:新增 `sa-token-quick-login` 插件,零代码集成登录功能 **[重要]** +- 优化:所有函数式接口增加`@FunctionalInterface`注解,感谢群友`@MrXionGe`提供的建议 +- 优化:文档优化... + + +### v1.18.0 @2021-4-24 +- 新增:新增权限通配符功能,灵活设置权限 **[重要]** +- 修复:修复自动续签处的逻辑错误 +- 新增:新增Web开发常见漏洞防护建议 +- 修复:修复`SaRequest`中缺少`getMethod()`的bug +- 修复:修复自动续签时的逻辑错误,感谢群成员`@N`的建议 +- 新增:全局过滤器新增 `beforAuth` 前置函数 +- 修复:修复在带有上下文的项目中无法正确获取请求路径的bug,感谢群成员`@dlwlrma`提供的建议 +- 新增:新增`SaHolder`上下文持有类,可方便的在上下文中读写数据 +- 重构:`SaTokenManager` -> `SaManager` +- 重构:`SaTokenInsideUtil` -> `SaFoxUtil` + + +### v1.17.0 @2021-4-17 +- 修复:在WebFlux环境中引入Redis集成包无法启动的问题 +- 修复:修复JWT集成示例中版本升级API的变更 +- 优化:优化启动时字符画打印 +- 文档:新增集成环境说明 +- 文档:新增功能介绍图 +- 新增:全局过滤器增加限定[拦截路径]与[排除路径]功能 +- 重构:全局过滤器执行函数放到成员变量里,连缀风格配置 +- 新增:新增全局侦听器,可在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作 **[重要]** + + +### v1.16.0 @2021-4-12 +- 新增:新增账号封禁功能,指定时间内账号无法登陆 **[重要]** +- 新增:核心包脱离`ServletAPI`,彻底零依赖! **[重要]** +- 新增:新增基于`ThreadLocal`的上下文容器 **[重要]** +- 新增:新增`Reactor`响应式编程支持,`WebFlux`集成! **[重要]** +- 新增:新增全局过滤器,解决拦截器无法拦截静态资源的问题 **[重要]** +- 新增:新增微服务网关鉴权方案!可接入`ShenYu`、`Gateway`等网关组件! **[重要]** +- 新增:AOP切面定义`Order`顺序为`-100`,可保证在多个自定义切面前执行 +- 文档:新增推荐公众号列表 + + +### v1.15.0 @2021-3-23 +- 新增:文档添加源码涉及技术栈说明 +- 优化:优化路由拦截器模块文档,更简洁的示例 +- 修复:修复非web环境下的错误提示,Request->Response +- 修复:修复Cookie注入时path判断错误,感谢@zhangzi0291提供的PR +- 新增:文档集成Redis章节新增redis配置示例说明,感谢群友 `@-)` 提供的建议 +- 新增:增加token前缀模式,可在配置token读取前缀,适配`Bearer token`规范 **[重要]** +- 优化:`SaTokenManager`初始化Bean去除`initXxx`方法,优化代码逻辑 +- 新增:`SaTokenManager`新增`stpLogicMap`集合,记录所有`StpLogic`的初始化,方便查找 +- 新增:`Session`新增timeout操作API,可灵活修改Session的剩余有效时间 +- 新增:token前缀改为强制校验模式,如果配置了前缀,则前端提交token时必须带有 +- 优化:精简`SaRouteInterceptor`,只保留自定义验证和默认的登陆验证,去除冗余功能 +- 优化:`SaRouterUtil`迁移到core核心包,优化依赖架构 +- 优化:默认Dao实现类里`Timer定时器`改为子线程 + sleep 模拟 +- 新增:`Session`新增各种类型转换API,可快速方便存取值 **[重要]** +- 升级注意: + - `SaRouterUtil`类迁移到核心包,注意更换import地址 + - `SaRouteInterceptor`去出冗余API,详情参考路由鉴权部分 + + +### v1.14.0 @2021-3-12 +- 新增:新增`SaLoginModel`登录参数Model,适配 [记住我] 模式 **[重要]** +- 新增:新增 `StpUtil.login()` 时指定token有效期,可灵活控制用户的一次登录免验证时长 +- 新增:新增Cookie时间判断,在`timeout`设置为-1时,`Cookie`有效期将为`Integer.MAX_VALUE` **[重要]** +- 新增:新增密码加密工具类,可快速MD5、SHA1、SHA256、AES、RSA加密 **[重要]** +- 新增:新增 OAuth2.0 模块 **[重要]** +- 新增:`SaTokenConfig`配置类所有set方法支持链式调用 +- 新增:`SaOAuth2Config` sa-token oauth2 配置类所有set方法新增支持链式调用 +- 优化:`StpLogic`类所有`getKey`方法重名为`splicingKey`,更语义化的函数名称 +- 新增:`IsRunFunction`新增`noExe`函数,用于指定当`isRun`值为`false`时执行的函数 +- 新增:`SaSession`新增数据存取值操作API +- 优化:优化`SaTokenDao`接口,增加Object操作API +- 优化:jwt示例`createToken`方法去除默认秘钥判断,只在启动项目时打印警告 +- 文档:常见问题新增示例(修改密码后如何立即掉线) +- 文档:权限认证文档新增[如何把权限精确搭到按钮级]示例说明 +- 文档:优化文档,部分模块添加图片说明 + + +### v1.13.0 @2021-2-9 +- 优化:优化源码注释与文档 +- 新增:文档集成Gitalk评论系统 +- 优化:源码包`Maven`版本号更改为变量形式 +- 修复:文档处方法名`getPermissionList`错误的bug +- 修复:修复`StpUtil.getTokenInfo()`会触发自动续签的bug +- 修复:修复接口 `SaTokenDao` 的 `searchData` 函数注释错误 +- 新增:`SaSession`的创建抽象到`SaTokenAction`接口,方便按需重写 +- 新建:框架内异常统一继承 `SaTokenException` 方便在异常处理时分辨处理 +- 新增:`SaSession`新增`setId()`与`setCreateTime()`方法,方便部分框架的序列化 +- 新增:新增`autoRenew`配置,用于控制是否打开自动续签模式 +- 新增:同域模式下的单点登录 **[重要]** +- 新增:完善分布式会话的文档说明 + + +### v1.12.0 @2021-1-12 +- 新增:提供JWT集成示例 **[重要]** +- 新增:新增路由式鉴权,可方便的根据路由匹配鉴权 **[重要]** +- 新增:新增身份临时切换功能,可在一个代码段内将会话临时切换为其它账号 **[重要]** +- 优化:将`SaCheckInterceptor.java`更名为`SaAnnotationInterceptor.java`,更语义化的名称 +- 优化:优化文档 +- 升级:v1.12.1,新增`SaRouterUtil`工具类,更方便的路由鉴权 **[重要]** + + +### v1.11.0 @2021-1-10 +- 新增:提供AOP注解鉴权方案 **[重要]** +- 优化自动生成token的算法 + + +### v1.10.0 @2021-1-9 +- 新增:提供查询所有会话方案 **[重要]** +- 修复:修复token设置为永不过期时无法正常被顶下线的bug,感谢github用户 @zjh599245299 提出的bug + + +### v1.9.0 @2021-1-6 +- 优化:`spring-boot-starter-data-redis` 由 `2.3.7.RELEASE` 改为 `2.3.3.RELEASE` +- 修复:补上注解拦截器里漏掉验证`@SaCheckRole`的bug +- 新增:新增同端互斥登录,像QQ一样手机电脑同时在线,但是两个手机上互斥登录 **[重要]** + + +### v1.8.0 @2021-1-2 +- 优化:优化源码注释 +- 修复:修复部分文档错别字 +- 修复:修复项目文件夹名称错误 +- 优化:优化文档配色,更舒服的代码展示 +- 新增:提供`sa-token`集成 `redis` 的 `spring-boot-starter` 方案 **[重要]** +- 新增:新增集成 `redis` 时,以`jackson`作为序列化方案 **[重要]** +- 新增:dao层默认实现增加定时清理过期数据功能 **[重要]** +- 新增:新增`token专属session`, 更灵活的会话管理 **[重要]** +- 新增:增加配置,指定在获取`token专属session`时是否必须登录 +- 新增:在无token时自动创建会话,完美兼容`token-session`会话模型! **[重要]** +- 修改:权限码限定必须为String类型 +- 优化:注解验证模式由boolean属性改为枚举方式 +- 删除:`StpUtil`删除部分冗长API,保持API清爽性 +- 新增:新增角色验证 (角色验证与权限验证已完全分离) **[重要]** +- 优化:移除`StpUtil.kickoutByLoginId()`API,由`logoutByLoginId`代替 +- 升级:开源协议修改为`Apache-2.0` + + +### v1.7.0 @2020-12-24 +- 优化:项目架构改为maven多模块形式,方便增加新模块 **[重要]** +- 优化:与`springboot`的集成改为`springboot-starter`模式,无需`@SaTokenSetup`注解即可完成自动装配 **[重要]** +- 新增:新增`activity-timeout`配置,可控制token临时过期与续签功能 **[重要]** +- 新增:`timeout`过期时间新增-1值,代表永不过期 +- 新增:`StpUtil.getTokenInfo()`改为对象形式,新增部分常用字段 +- 优化:解决在无cookie模式下,不集成redis时会话无法主动过期的问题 +- 修复:修复文档首页样式问题 + + +### v1.6.0 @2020-12-17 +- 新增:花式token生成方案 **[重要]** +- 优化:优化`readme.md` +- 修复:修复`SaCookieOper`与`SaTokenAction`无法自动注入的问题 + + +### v1.5.1 @2020-12-16 +- 新增:细化未登录异常类型,提供五种场景值:未提供token、token无效、token已过期 、token已被顶下线、token已被踢下线 **[重要]** +- 修复:修复`StpUtil.getSessionByLoginId(String loginId)`方法转换key出错的bug,感谢群友 @(#°Д°)、@一米阳光 发现的bug +- 优化:修改方法`StpUtil.getSessionByLoginId(Object loginId)`的isCreate值默认为true +- 修改:`方法delSaSession`修改为`deleteSaSession`,更加语义化的函数名称 +- 新增:新增`StpUtil.getTokenName()`方法,更语义化的获取tokenName +- 新增:新增`SaTokenAction`框架行为Bean,方便重写逻辑 +- 优化:`Cookie操作`改为接口代理模式,使其可以被重写 +- 优化:文档里集成redis部分增加redis的pom依赖示例 +- 修复:登录验证-> `StpUtil.getLoginId_defaultNull()` 修复方法名错误的问题 +- 优化:优化`readme.md` +- 升级:开源协议修改为`MIT` + + +### v1.4.0 @2020-9-7 +- 优化:修改一些函数、变量名称,使其更符合阿里java代码规范 +- 优化:`tokenValue`的读取优先级改为:`request` > `body` > `header` > `cookie` **[重要]** +- 新增:新增`isReadCookie`配置,决定是否从`cookie`里读取`token`信息 +- 优化:如果`isReadCookie`配置为`false`,那么在登录时也不会把`cookie`写入`cookie` +- 新增:新增`getSessionByLoginId(Object loginId, boolean isCreate)`方法 +- 修复:修复文档部分错误,修正群号码 + + +### v1.3.0 @2020-5-2 +- 新增:新增 `StpUtil.checkLogin()` 方法,更符合语义化的鉴权方法 +- 新增:注册拦截器时可设置 `StpLogic` ,方便不同模块不同鉴权方式 +- 新增:抛出异常时增加 `loginType` 区分,方便多账号体系鉴权处理 +- 修复:修复启动时的版本字符画版本号打印不对的bug +- 修复:修复文档部分不正确之处 +- 新增:新增文档的友情链接 + + +### v1.2.0 @2020-3-7 +- 新增:新增注解式验证,可在路由方法中使用注解进行权限验证 **[重要]** +- 参考:[注解式验证](use/at-check) + + +### v1.1.0 @2020-2-12 +- 修复:修复`StpUtil.getLoginId(T defaultValue)`取值转换错误的bug + + +### v1.0.0 @2020-2-4 +- 第一个版本出炉 diff --git a/sa-token-doc/more/wenjuan.md b/sa-token-doc/more/wenjuan.md new file mode 100644 index 000000000..7324ccb04 --- /dev/null +++ b/sa-token-doc/more/wenjuan.md @@ -0,0 +1,5 @@ +# 问卷调查 + +我们想以运营一款产品的心态来运营一个开源框架,所以我们迫切希望您能够填写这份问卷,这有6道选择题,应该只会略微占用您1-3分钟的时间,Sa-Token将会非常重视每一位粉丝的宝贵意见! + +[https://wj.qq.com/s2/14587150/b5b4/](https://wj.qq.com/s2/14587150/b5b4/) diff --git a/sa-token-doc/oauth2/oauth2-apidoc.md b/sa-token-doc/oauth2/oauth2-apidoc.md new file mode 100644 index 000000000..2cda65aca --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-apidoc.md @@ -0,0 +1,350 @@ +# Sa-Token-OAuth2 Server端 API列表 +基于官方仓库的搭建示例,`OAuth2-Server`端会暴露出以下API,`OAuth2-Client`端可据此文档进行对接 + +--- + +## 1、模式一:授权码(Authorization Code) + +### 1.1、获取授权码 + +根据以下格式构建URL,引导用户访问 (复制时请注意删减掉相应空格和换行符) +``` url +http://{host}:{port}/oauth2/authorize + ?response_type=code + &client_id={client_id} + &redirect_uri={redirect_uri} + &scope={scope} + &state={state} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| response_type | 是 | 返回类型,这里请填写:`code` | +| client_id | 是 | 应用 id | +| redirect_uri | 是 | 用户确认授权后,重定向的 url 地址 | +| scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | +| state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加,如果填写则每次填写的值不可以重复 | + +注意点: +1. 如果用户在 `OAuth-Server` 端尚未登录:会被转发到登录视图,你可以参照文档或官方示例自定义登录页面。 +2. 如果 `scope` 参数为空,或者请求的 `scope` 用户近期已确认授权过,则无需用户再次确认,达到静默授权的效果,否则需要用户手动确认,服务器才可以下放 `code` 授权码。 + +用户确认授权之后,会被重定向至`redirect_uri`,并追加 `code` 参数与 `state` 参数,形如: +``` url +redirect_uri?code={code}&state={state} +``` + +`Code` 授权码具有以下特点: +1. 每次授权产生的 `Code` 码都不一样。 +2. `Code` 码用完即废,不能二次使用。 +3. 一个 `Code` 的有效期默认为五分钟,超时自动作废。 +4. 每次授权产生新 `Code` 码,会导致旧 `Code` 码立即作废,即使旧 `Code` 码尚未使用。 + + +
+RestAPI 登录接口:/oauth2/doLogin + +如果用户在 OAuth-Server 端尚未登录,则会被阻塞在登录界面,开始登录,需要在页面上调用`/oauth2/doLogin`完成登录(此接口非 OAuth2 标准协议接口) + +``` url +http://{host}:{port}/oauth2/doLogin + ?name={name} + &pwd={pwd} +``` +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| name | 否 | 账号 | +| pwd | 否 | 密码 | + +访问此接口将进入自定义的 `cfg.doLoginHandle` 函数开始登录,你只要在此函数内调用 `StpUtil.login(xxx)` 即代表登录成功。 + +另外需要注意:此接口并非只能携带 `name`、`pwd` 参数,因为你可以在方法里通过 `SaHolder.getRequest().getParam("xxx")` 来获取前端提交的其它参数。 + +
+ + +
+RestAPI 确认授权接口:/oauth2/doConfirm + +如果 oauth-client 端申请的 scope 在 OAuth-Server 端需要用户手动确认授权,则会被阻塞在授权界面, +需要在页面上调用`/oauth2/doConfirm`完成授权(此接口非 OAuth2 标准协议接口) + +``` url +http://{host}:{port}/oauth2/doConfirm + ?client_id={value} + &scope={value} + &build_redirect_uri={true|false} + &response_type={value} + &redirect_uri={value} + &state={value} +``` +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| client_id | 是 | 应用 id | +| scope | 是 | 具体确认的权限,多个用逗号(或空格)隔开 | +| build_redirect_uri | 否 | 是否立即构建 `redirect_uri` 授权地址,取值:true | false | +| response_type | 否 | 取 url 上的 `response_type` 参数来提交 | +| redirect_uri | 否 | 取 url 上的 `redirect_uri` 参数来提交 | +| state | 否 | 取 url 上的 `state` 参数来提交 | + +此接口有两种调用方式,一种只提供 `client_id`、`scope` 两个参数,此时返回结果代表是否确认授权成功: +``` js +{ + code: 200, + msg: 'ok', + data: null, +} +``` + +一种是指定 `build_redirect_uri: true`,并同时提供 `client_id`、`scope`、`response_type`、`redirect_uri`、`state` 全部参数, +此时返回结果包括最终的 code 授权地址: +``` js +{ + code: 200, + msg: 'ok', + data: null, + redirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC' +} +``` + +前端在 ajax 回调函数中直接使用 `location.href=res.redirect_uri` 跳转即可,无需再重复访问 `/oauth2/authorize` 接口。 + +
+ + + +### 1.2、根据授权码获取 Access-Token +获得 `Code` 码后,我们可以通过以下接口,获取到用户的 `Access-Token`、`Refresh-Token` 等信息。 + +``` url +http://{host}:{port}/oauth2/token + ?grant_type=authorization_code + &client_id={client_id} + &client_secret={client_secret} + &code={code} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| grant_type | 是 | 授权类型,这里请填写:`authorization_code` | +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| code | 是 | 步骤 1.1 中获取到的授权码 | + +也可以通过 `Basic Authorization` 方式提交 `client` 信息,格式为在请求 `header` 头添加 `Authorization` 参数: +``` js +header['Authorization'] = base64(`${client_id}:${client_secret}`); +``` + +接口返回示例: + +``` js +{ + "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 + "msg": "ok", + "data": null, + "token_type": "Bearer", + "access_token": "Gly7mnnXSdCxkOqmOwcA5SbG6ZtPmJVX7ZgSn1pidhRmnenBEgxbWJS8VWxA", // Access-Token值 + "refresh_token": "EuYNwpxdc18MpaZLPyhFeyAyzr2IOWEr4q3QUGgPWqdJujQqvohjQEDJpwOm", // Refresh-Token值 + "expires_in": 7199, // Access-Token剩余有效期,单位秒 + "refresh_expires_in": 2591999, // Refresh-Token剩余有效期,单位秒 + "client_id": "1001", // 应用 id + "scope": "userinfo" // 此令牌包含的权限 +} +``` + + +### 1.3、根据 Refresh-Token 刷新 Access-Token (如果需要的话) +Access-Token的有效期较短,如果每次过期都需要重新授权的话,会比较影响用户体验,因此我们可以在后台通过`Refresh-Token` 刷新 `Access-Token` + +``` url +http://{host}:{port}/oauth2/refresh + ?grant_type=refresh_token + &client_id={client_id} + &client_secret={client_secret} + &refresh_token={refresh_token} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| grant_type | 是 | 授权类型,这里请填写:`refresh_token` | +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| refresh_token | 是 | 步骤1.2中获取到的 `Refresh-Token` 值 | + +接口返回值同章节1.2,此处不再赘述 + + +### 1.4、回收 Access-Token (如果需要的话) +在A ccess-Token 过期之前主动将其回收 + +``` url +http://{host}:{port}/oauth2/revoke + ?client_id={client_id} + &client_secret={client_secret} + &access_token={access_token} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| access_token | 是 | 步骤1.2中获取到的`Access-Token`值 | + +返回值样例: +``` js +{ + "code": 200, + "msg": "ok", + "data": null +} +``` + + +### 1.5、根据 Access-Token 获取相应用户的账号信息 +注:此接口非 OAuth2 标准协议接口,为官方仓库 demo 模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数 + +``` url +http://{host}:{port}/oauth2/userinfo?access_token={access_token} +``` + +返回值样例: +``` js +{ + "code": 200, + "msg": "ok", + "nickname": "shengzhang_", // 账号昵称 + "avatar": "http://xxx.com/1.jpg", // 头像地址 + "age": "18", // 年龄 + "sex": "男", // 性别 + "address": "山东省 青岛市 城阳区" // 所在城市 +} +``` + +除了直接在 url 中以 query 参数方式提交 `access_token`,你也可以在 `Authorization` 请求头以 `Bearer Token` 方式提交: +``` js +header['Authorization'] = 'Bearer access_token'; +``` + + +## 2、模式二:隐藏式(Implicit) + +根据以下格式构建URL,引导用户访问: +``` url +http://{host}:{port}/oauth2/authorize + ?response_type=token + &client_id={client_id} + &redirect_uri={redirect_uri} + &scope={scope} + &state={state} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| response_type | 是 | 返回类型,这里请填写:`token` | +| client_id | 是 | 应用 id | +| redirect_uri | 是 | 用户确认授权后,重定向的url地址 | +| scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | +| state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加,如果填写则每次填写的值不可以重复 | + +此模式会越过授权码的步骤,直接返回 `Access-Token` 到前端页面,形如: +``` url +redirect_uri#token=xxxx-xxxx-xxxx-xxxx +``` +注意 token 是以 `#` 锚参数的形式拼接到 url 上的。 + + +## 3、模式三:密码式(Password) +首先在Client端构建表单,让用户输入 Server 端的账号和密码,然后在 Client 端访问接口 +``` url +http://{host}:{port}/oauth2/token + ?grant_type=password + &client_id={client_id} + &client_secret={client_secret} + &username={username} + &password={password} + &scope={scope} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| grant_type | 是 | 返回类型,这里请填写:`password`| +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| username | 是 | 用户的 `OAuth2-Server` 端账号 | +| password | 是 | 用户的 `OAuth2-Server` 端密码 | +| scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | + +接口返回示例: + +``` js +{ + "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 + "msg": "ok", + "access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ", // Access-Token 值 + "refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66", // Refresh-Token 值 + "expires_in": 7199, // Access-Token 剩余有效期,单位秒 + "refresh_expires_in": 2591999, // Refresh-Token 剩余有效期,单位秒 + "client_id": "1001", // 应用 id + "scope": "", // 此令牌包含的权限 +} +``` + +> [!WARNING| label:重写认证处理器] +> 在正式项目中,password 认证模式需要重写 `PasswordGrantTypeHandler` 处理器,在后面的 [自定义 grant_type](/oauth2/oauth2-custom-grant_type) 章节我们会详细介绍 + + +## 4、模式四:凭证式(Client Credentials) +以上三种模式获取的都是用户的 `Access-Token`,代表用户对第三方应用的授权, +在OAuth2.0中还有一种针对 Client级别的授权, 即:`Client-Token`,代表应用自身的资源授权 + +在 Client 端的后台访问以下接口: + +``` url +http://{host}:{port}/oauth2/client_token + ?grant_type=client_credentials + &client_id={client_id} + &client_secret={client_secret} + &scope={scope} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| grant_type | 是 | 返回类型,这里请填写:`client_credentials`| +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | + +接口返回值样例: +``` js +{ + "code": 200, + "msg": "ok", + "client_token": "HmzPtaNuIqGrOdudWLzKJRSfPadN497qEJtanYwE7ZvHQWDy0jeoZJuDIiqO", // Client-Token 值 + "expires_in": 7199, // Token剩余有效时间,单位秒 + "client_id": "1001", // 应用 id + "scope": null // 包含权限 +} +``` + +注:`Client-Token`具有延迟作废特性,即:在每次获取最新`Client-Token`的时候,旧`Client-Token`不会立即过期,而是作为`Lower-Client-Token`再次储存起来, +资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”, 保证了服务的高可用。 + diff --git a/sa-token-doc/oauth2/oauth2-at-check.md b/sa-token-doc/oauth2/oauth2-at-check.md new file mode 100644 index 000000000..aa0fbfe8a --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-at-check.md @@ -0,0 +1,94 @@ +# Sa-Token OAuth2 模块相关注解 + +sa-token-oauth2 模块扩展了三个注解用于相关数据校验: +- `@SaCheckAccessToken`:指定请求中必须包含有效的 `access_token` ,并且包含指定的 `scope`。 +- `@SaCheckClientToken`:指定请求中必须包含有效的 `client_token` ,并且包含指定的 `scope`。 +- `@SaCheckClientIdSecret`:指定请求中必须包含有效的 `client_id` 和 `client_secret` 信息。 + +和 Sa-Token-Code 模块的注解一样,你必须先注册框架的内置拦截器,才可以使用这些注解,详细参考:[注解鉴权](/use/at-check) 。 + +--- + + +### 1、@SaCheckAccessToken 示例 + +``` java +@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("访问成功"); + } + +} +``` + + +### 2、@SaCheckClientToken 示例 + +``` java +@RestController +@RequestMapping("/test") +public class TestController { + + // 测试:携带有效的 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("访问成功"); + } + +} +``` + + +### 3、@SaCheckClientIdSecret 示例 +``` java +@RestController +@RequestMapping("/test") +public class TestController { + + // 测试:携带有效的 client_id 和 client_secret 信息,才可以进入请求 + // 你可以在请求参数中携带 client_id 和 client_secret 参数,或者从请求头以 Authorization: Basic base64(client_id:client_secret) 的形式携带 + @SaCheckClientIdSecret + @RequestMapping("/checkClientIdSecret") + public SaResult checkClientIdSecret() { + return SaResult.ok("访问成功"); + } + +} +``` diff --git a/sa-token-doc/oauth2/oauth2-check-domain.md b/sa-token-doc/oauth2/oauth2-check-domain.md new file mode 100644 index 000000000..16b06b8b2 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-check-domain.md @@ -0,0 +1,101 @@ +# OAuth2 整合-配置域名校验 + +--- + +### 1、code 劫持攻击 +在前面章节的 OAuth-Server 搭建示例中: + +``` java +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + // 根据 clientId 获取 Client 信息 + @Override + public SaClientModel getClientModel(String clientId) { + if("1001".equals(clientId)) { + return new SaClientModel() + // ... + .addAllowRedirectUris("*") // 所有允许授权的 url + // ... + } + return null; + } + // 其它代码 ... +} +``` + +配置项 `AllowRedirectUris` 意为配置此 `Client` 端所有允许的授权地址,不在此配置项中的 URL 将无法下发 `code` 授权码。 + +为了方便测试,上述代码将其配置为`*`,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 `code` 劫持的风险。 + +假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL: + +> [http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com](http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com) + +当不知情的小红被诱导访问了这个 URL 时,它将被重定向至百度首页。 + +oauth2-ticket-jc + +可以看到,代表着用户身份的 code 授权码也显现到了URL之中,借此漏洞,攻击者完全可以构建一个 URL 将小红的 code 授权码自动提交到攻击者自己的服务器,伪造小红身份登录网站。 + + +### 2、防范方法 + +造成此漏洞的直接原因就是我们对此 client 配置了过于宽泛的 `AllowRedirectUris` 允许授权地址,防范的方法也很简单,就是缩小 `AllowRedirectUris` 授权范围。 + +我们将其配置为一个具体的URL: + +``` java +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + // 根据 clientId 获取 Client 信息 + @Override + public SaClientModel getClientModel(String clientId) { + if("1001".equals(clientId)) { + return new SaClientModel() + // ... + .addAllowRedirectUris("http://sa-oauth-client.com:8002/") // 所有允许授权的 url + // ... + } + return null; + } + // 其它代码 ... +} +``` + +再次访问上述链接: + +oauth2-feifa-rf + +URL 没有通过校验,拒绝授权! + + +### 3、配置安全性参考表 + +| 配置方式 | 举例 | 安全性 | 建议 | +| :-------- | :-------- | :-------- | :-------- | +| 配置为* | `*` | | **禁止在生产环境下使用** | +| 配置到域名 | `http://sa-oauth-client.com:8002/*` | | 不建议在生产环境下使用 | +| 配置到详细地址 | `http://sa-oauth-client.com:8002/xxx/xxx` | | 可以在生产环境下使用 | + + +### 4、其它规则 + +1、AllowRedirectUris 配置的地址不允许出现 `@` 字符。 + +- 反例:`http://user@sa-token.com` +- 反例:`http://sa-oauth-client.com@sa-token.com` + +*详见源码:[SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) +`checkRedirectUri` 方法。* + +2、AllowRedirectUris 配置的地址 `*` 通配符只允许出现在字符串末尾,不允许出现在字符串中间位置。 + +- 反例:`http*://sa-oauth-client.com/` +- 反例:`http://*.sa-oauth-client.com/` + +*详见源码: [SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) +`checkRedirectUriListNormalStaticMethod` 方法。* + +参考:[github/issue/529](https://github.com/dromara/Sa-Token/issues/529) +感谢这位 `@m4ra7h0n` 用户反馈的漏洞。 + diff --git a/sa-token-doc/oauth2/oauth2-custom-api.md b/sa-token-doc/oauth2/oauth2-custom-api.md new file mode 100644 index 000000000..1802a4f1d --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-custom-api.md @@ -0,0 +1,104 @@ +# OAuth2-自定义 API 路由 + +--- + +### 方式一:修改全局变量 + +在之前的章节中,我们演示了如何搭建一个 OAuth2 认证中心: +``` java +/** + * Sa-Token-OAuth2 Server端 Controller + */ +@RestController +public class SaOAuth2ServerController { + + // OAuth2-Server 端:处理所有 OAuth2 相关请求 + @RequestMapping("/oauth2/*") + public Object request() { + return SaOAuth2ServerProcessor.instance.dister(); + } + + // ... 其它代码 + +} +``` + +这种写法集成简单但却不够灵活。例如获取 code 授权码地址只能是:`http://{host}:{port}/oauth2/authorize`,如果我们想要自定义其API地址,应该怎么做呢? + +打开 OAuth2 模块相关源码,有关 API 的设计都定义在: +[SaOAuth2Consts.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java) +中,我们可以对其进行二次修改。 + +例如,我们可以在 Main 方法启动类或者 OAuth2 配置方法中修改变量值: +``` java +// 配置 OAuth2 相关参数 +@Autowired +private void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + // 自定义API地址 + SaOAuth2Consts.Api.authorize = "/oauth2/authorize2"; + // ... +} +``` + +启动项目,统一认证地址就被我们修改成了:`http://{host}:{port}/oauth2/authorize2` + + +### 方式二:拆分路由入口 +根据上述路由入口:`@RequestMapping("/oauth2/*")`,我们给它起一个合适的名字 —— 聚合式路由。 + +与之对应的,我们可以将其修改为拆分式路由: + +``` java +/** + * Sa-Token-OAuth2 Server端 Controller + */ +@RestController +public class SaOAuth2ServerController { + + // 模式一:Code授权码 || 模式二:隐藏式 + @RequestMapping("/oauth2/authorize") + public Object authorize() { + return SaOAuth2ServerProcessor.instance.authorize(); + } + + // 用户登录 + @RequestMapping("/oauth2/doLogin") + public Object doLogin() { + return SaOAuth2ServerProcessor.instance.doLogin(); + } + + // 用户确认授权 + @RequestMapping("/oauth2/doConfirm") + public Object doConfirm() { + return SaOAuth2ServerProcessor.instance.doConfirm(); + } + + // Code 换 Access-Token || 模式三:密码式 + @RequestMapping("/oauth2/token") + public Object token() { + return SaOAuth2ServerProcessor.instance.token(); + } + + // Refresh-Token 刷新 Access-Token + @RequestMapping("/oauth2/refresh") + public Object refresh() { + return SaOAuth2ServerProcessor.instance.refresh(); + } + + // 回收 Access-Token + @RequestMapping("/oauth2/revoke") + public Object revoke() { + return SaOAuth2ServerProcessor.instance.revoke(); + } + + // 模式四:凭证式 + @RequestMapping("/oauth2/client_token") + public Object clientToken() { + return SaOAuth2ServerProcessor.instance.clientToken(); + } + +} +``` + +拆分式路由 与 聚合式路由 在功能上完全等价,且提供了更为细致的路由管控。 + diff --git a/sa-token-doc/oauth2/oauth2-custom-grant_type.md b/sa-token-doc/oauth2/oauth2-custom-grant_type.md new file mode 100644 index 000000000..407727ea2 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-custom-grant_type.md @@ -0,0 +1,209 @@ +# OAuth2-自定义权限处理器 + + +### 1、需求场景 + +OAuth2 协议的 `/oauth2/token` 接口定义了两种获取 `access_token` 的 `grant_type`,分别是: +- `authorization_code`:使用用户授权的授权码获取 access_token。 +- `password`:使用用户提交的账号、密码来获取 access_token。 + +你可以重写内置 `grant_type` 处理器,或添加自定义 `grant_type` 处理器,来支持更多的场景。 + + + +--- + + +### 2、重写 password 认证模式处理器 + +当我们按照文档搭建的代码直接测试 password 认证模式时,控制台会得到警告: +``` txt +警告信息:当前 password 认证模式,使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle),仅供开发测试 +正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法 +``` + +这是因为为方便测试,框架内部直接将 password 认证请求转发到了 `SaOAuth2Strategy.instance.doLoginHandle` 来处理, +在真正的项目中需要大家重写 password 认证模式处理器: + +``` java +/** + * 自定义 Password Grant_Type 授权模式处理器认证过程 + */ +@Component +public class CustomPasswordGrantTypeHandler extends PasswordGrantTypeHandler { + + @Override + public PasswordAuthResult loginByUsernamePassword(String username, String password) { + if("sa".equals(username) && "123456".equals(password)) { + long userId = 10001; + return new PasswordAuthResult(userId); + } else { + throw new SaOAuth2Exception("无效账号密码"); + } + } + +} +``` + + +### 3、添加自定义 grant_type 处理器 + +假设有以下需求:通过 手机号+验证码 登录,返回 `access_token`。 + +#### 3.1、新增验证码发送接口 + +首先在 oauth2-server 端开放一个接口,为指定手机号发送验证码。 + +``` java +/** + * 自定义手机登录接口 + */ +@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("验证码发送成功"); + } + +} +``` + +真实项目肯定是要对接短信服务商的,此处我们仅做模拟代码,将发送的验证码打印在控制台上。 + + +#### 3.2、自定义 grant_type 处理器 + +在 oauth2-server 新建 `PhoneCodeGrantTypeHandler` 实现 `SaOAuth2GrantTypeHandlerInterface` 接口: + +``` java +/** + * 自定义 phone_code 授权模式处理器 + */ +@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, atm -> atm.grantType = "phone_code"); + return at; + } +} +``` + +#### 3.3、为应用添加允许的授权类型 + +在 `SaOAuth2DataLoader` 实现类中,为 client 的允许授权类型增加自定义的 `phone_code` + +``` java +// Sa-Token OAuth2:自定义数据加载器 +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + @Override + public SaClientModel getClientModel(String clientId) { + if("1001".equals(clientId)) { + return new SaClientModel() + .setClientId("1001") + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") + .addAllowRedirectUris("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo") + .addAllowGrantTypes( + GrantType.authorization_code, + GrantType.implicit, + GrantType.refresh_token, + GrantType.password, + GrantType.client_credentials, + "phone_code" // 重要代码:自定义授权模式 手机号验证码登录 + ) + ; + } + return null; + } + // 其它代码 ... +} +``` + +完工,开始测试。 + + +### 4、测试步骤 + +#### 1、先发送验证码 + +``` url +http://sa-oauth-server.com:8000/oauth2/sendPhoneCode?phone=13144556677 +``` + +#### 2、请求 token + +注意 `grant_type` 要填写我们自定义的 `phone_code`,code 的具体值可以在后端的控制台上看到 + +``` url +http://sa-oauth-server.com:8000/oauth2/token + ?grant_type=phone_code + &client_id=1001 + &client_secret=aaaa-bbbb-cccc-dddd-eeee + &scope=openid + &phone=13144556677 + &code={value} +``` + +返回结果参考如下: + +``` js +{ + "code": 200, + "msg": "ok", + "data": null, + "token_type": "Bearer", + "access_token": "pfxRz6KVacwvKNu4IHmDsCJs33kvvARs2z1lTch7stog8nRt6rfVLowtAZ0E", + "refresh_token": "qcFD6Wo2qZidofXQtWF5oK5ML6ljHKufQ5SbouBxzGnHhnMjUG4VV0iXZhdE", + "expires_in": 7199, + "refresh_expires_in": 2591999, + "client_id": "1001", + "scope": "openid", + "openid": "ded91dc189a437dd1bac2274be167d50" +} +``` + + + + + + + + + + diff --git a/sa-token-doc/oauth2/oauth2-custom-login.md b/sa-token-doc/oauth2/oauth2-custom-login.md new file mode 100644 index 000000000..35028d58e --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-custom-login.md @@ -0,0 +1,94 @@ +# OAuth2 定制化登录页面 + +--- + + + +### 1、如何自定义 OAuth-Server 端的登录视图? + +重写 `SaOAuth2Strategy.instance.notLoginView` 策略: + +``` java +@Autowired +public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + // 配置:未登录时返回的View + SaOAuth2Strategy.instance.notLoginView = ()->{ + return new ModelAndView("xxx.html"); + }; +} +``` + +在以上返回的视图中 ajax 方式调用 `/oauth2/doLogin` 接口,该接口接受以下参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| name | 否 | 账号 | +| pwd | 否 | 密码 | + +接口返回值根据你重写的 `cfg.doLoginHandle` 策略进行自由决定。 + + + +### 2、如何自定义登录API的接口地址? +根据需求点选择解决方案: + +#### 2.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数? +``` java +// 在任意代码处获取前端提交的参数 +String xxx = SaHolder.getRequest().getParam("xxx"); +``` + +#### 2.2、想完全自定义一个接口来接受前端登录请求? +``` java +// 直接定义一个拦截路由为 `/oauth2/doLogin` 的接口即可 +@RequestMapping("/oauth2/doLogin") +public SaResult ss(String name, String pwd) { + System.out.println("------ 请求进入了自定义的API接口 ---------- "); + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功!"); + } + return SaResult.error("登录失败!"); +} +``` + +#### 2.3、不想使用`/oauth2/doLogin`这个接口,想自定义一个API地址? + +答:直接在前端更改点击按钮时 Ajax 的请求地址即可 + + + +### 3、如何自定义 OAuth-Server 端的确认授权视图? + +重写 `SaOAuth2Strategy.instance.confirmView` 策略: + +``` java +@Autowired +public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + // 配置:授权确认视图 + SaOAuth2Strategy.instance.confirmView = (clientId, scopes)->{ + Map map = new HashMap<>(); + map.put("clientId", clientId); + map.put("scope", scopes); + return new ModelAndView("confirm.html", map); + }; +} +``` + +在以上返回的视图中 ajax 方式调用 `/oauth2/doConfirm` 接口,即可完成授权,该接口接受以下参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| client_id | 是 | 应用 id | +| scope | 是 | 具体授予的权限,多个用逗号(或空格)隔开 | + +接口返回值样例: +``` js +{ + "code": 200, + "msg": "ok", + "data": null, +} +``` + + diff --git a/sa-token-doc/oauth2/oauth2-custom-scope.md b/sa-token-doc/oauth2/oauth2-custom-scope.md new file mode 100644 index 000000000..d8a22773f --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-custom-scope.md @@ -0,0 +1,235 @@ +# OAuth2-自定义 Scope 权限及处理器 + +--- + +### 1、需求场景 +一般情况下,对于第三方 oauth2-client 来讲,仅仅拿到用户的 access_token 是不够的,还需要拿到更多的信息,比如用户昵称、头像等资料。 + +sa-token-oauth2 提供两种模式,让 access_token 可以得到更多信息。 + +- 自定义接口模式:在 oauth2-server 端开放一个资料查询接口,在 oauth2-client 得到 `access_token` 后,再次调用这个接口来获取 `userinfo` 信息。 +- 自定义权限处理器模式:自定义一个 `ScopeHandler`,直接在返回 `access_token` 时追加字段,将 `userinfo` 信息和 `access_token` 一并返回到 oauth2-client。 + + +### 2、自定义接口模式 + +#### 1、新建查询接口 + +在 oauth2-server 新建接口,查询指定 `access_token` 代表的 `userId` 其 `userinfo`: + +``` java +// 获取 userinfo 信息:昵称、头像、性别等等 +@RequestMapping("/oauth2/userinfo") +public SaResult userinfo() { + // 获取 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.checkAccessTokenScope(accessToken, "userinfo"); + + // 模拟账号信息 (真实环境需要查询数据库获取信息) + 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.ok().setMap(map); +} +``` + + +#### 2、申请 code 时指定权限 +oauth2-client 申请 `code` 时,一定需要加上 `userinfo` 权限 + +``` url +http://sa-oauth-server.com:8000/oauth2/authorize + ?response_type=code + &client_id=1001 + &redirect_uri=http://sa-oauth-client.com:8002/ + &scope=userinfo +``` + + +#### 3、code 换 access_token +访问上述链接后,得到 `code` 授权码,然后我们拿着 `code` 换 `access_token` + +``` url +http://sa-oauth-server.com:8000/oauth2/token + ?grant_type=authorization_code + &client_id=1001 + &client_secret=aaaa-bbbb-cccc-dddd-eeee + &code=${code} +``` + +#### 4、access_token 取 userinfo +使用返回的 `access_token` 再次访问接口 `/oauth2/userinfo` + +``` url +http://sa-oauth-server.com:8000/oauth2/userinfo?access_token=${access_token} +``` + +返回以下结果: +``` js +{ + "code": 200, + "msg": "ok", + "data": null, + "nickname": "林小林", + "avatar": "http://xxx.com/1.jpg", + "age": "18", + "sex": "男", + "address": "山东省 青岛市 城阳区" +} +``` + +拿到 userinfo。 + + + +### 3、自定义权限处理器模式 + +#### 1、新建权限处理器 +在 oauth2-server 新建 `UserinfoScopeHandler.java` 实现 `SaOAuth2ScopeHandlerInterface` 接口: + +``` java +/** + * 自定义 userinfo scope 处理器 + */ +@Component +public class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface { + + // 指示当前处理器所要处理的 scope + @Override + public String getHandlerScope() { + return "userinfo"; + } + + // 当构建的 AccessToken 具有此权限时,所需要执行的方法 + @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.putAll(map); + } + + // 当构建的 ClientToken 具有此权限时,所需要执行的方法 + @Override + public void workClientToken(ClientTokenModel ct) { + } + + // 当使用 RefreshToken 刷新 AccessToken 时,是否重新执行 workAccessToken 构建方法 + // 在一些实时性较高的数据中需要指定为 true + @Override + public boolean refreshAccessTokenIsWork() { + return true; + } + +} +``` + +如上所述,所有写入到 `extraData` 中的数据,都将追加返回到 oauth2-client 端。 + + +#### 2、申请 code 时指定权限 +oauth2-client 申请 `code` 时,一定需要加上 `userinfo` 权限 +``` url +http://sa-oauth-server.com:8000/oauth2/authorize + ?response_type=code + &client_id=1001 + &redirect_uri=http://sa-oauth-client.com:8002/ + &scope=userinfo +``` + + +#### 3、code 换 access_token +访问上述链接后,得到 `code` 授权码,然后我们拿着 `code` 换 `access_token` +``` url +http://sa-oauth-server.com:8000/oauth2/token + ?grant_type=authorization_code + &client_id=1001 + &client_secret=aaaa-bbbb-cccc-dddd-eeee + &code=${code} +``` + +返回结果如下 +``` js +{ + "code": 200, + "msg": "ok", + "data": null, + "token_type": "Bearer", + "access_token": "LQ24xI0hX25vIzvciHPA0PNsnGCweSFM1Bzl8783li07VAXpw8sEfn9xsta2", + "refresh_token": "rKB8mby1Mw8yZXHbWzliHx6lmatcLcULLw5C5cUMBhMMRx72DFg5u0owZgrA", + "expires_in": 7199, + "refresh_expires_in": 2591999, + "client_id": "1001", + "scope": "openid,userid,userinfo", + "userinfo": { + "userId": "10008", + "nickname": "shengzhang_", + "avatar": "http://xxx.com/1.jpg", + "age": "18", + "sex": "男", + "address": "山东省 青岛市 城阳区" + } +} +``` + +拿到 userinfo。 + +#### 总结 +相比于自定义接口模式,自定义权限处理器模式可以少一次网络请求,让 oauth2-client 端提前拿到 `userinfo` 信息。 + + + +### 4、最终权限处理器 +当一个自定义权限处理器,监听的 scope 字符串为 `_FINALLY_WORK_SCOPE` 时,则代表这个权限处理器为“最终权限处理器”。 + +最终权限处理器会永远在所有权限处理器工作完成之后执行一次,即使 oauth2-client 端没有申请任何 scope,最终权限处理器也会固定执行。 + +示例: +``` java +/** + * 最终权限处理器:在所有权限处理器工作完成之后,执行此权限处理器 + */ +@Component +public class FinallyWorkScopeHandler implements SaOAuth2ScopeHandlerInterface { + + @Override + public String getHandlerScope() { + return SaOAuth2Consts._FINALLY_WORK_SCOPE; + } + + @Override + public void workAccessToken(AccessTokenModel at) { + // 在所有权限处理器工作完成之后,执行此处方法加工 AccessToken + // System.out.println(123); + } + + @Override + public void workClientToken(ClientTokenModel ct) { + // System.out.println(456); + } +} +``` + + + + + + + + + + diff --git a/sa-token-doc/oauth2/oauth2-data-loader.md b/sa-token-doc/oauth2/oauth2-data-loader.md new file mode 100644 index 000000000..b2abc3279 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-data-loader.md @@ -0,0 +1,151 @@ +# OAuth2-自定义数据加载器 + + + +### 1、基于内存的数据加载 + +在之前搭建 OAuth2-Server 示例中,我们演示了 client 信息配置方案: +``` java +// Sa-Token OAuth2 定制化配置 +@Autowired +public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + + // 添加 client + oauth2Server.addClient( + new SaClientModel() + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowRedirectUris("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 + .addAllowGrantTypes( // 所有允许的授权模式 + GrantType.authorization_code, // 授权码式 + GrantType.implicit, // 隐式式 + GrantType.refresh_token, // 刷新令牌 + GrantType.password, // 密码式 + GrantType.client_credentials, // 客户端模式 + ) + ) + + // 可以添加更多 client 信息,只要保持 clientId 唯一就行了 + // oauth2Server.addClient(...) + +} +``` + +你也可以在 `application.yml` 配置中 `client` 信息: + + + +``` yaml +# sa-token配置 +sa-token: + # OAuth2.0 配置 + oauth2-server: + # client 列表 + clients: + # 客户端1 + 1001: + # 客户端id + client-id: 1001 + # 客户端秘钥 + client-secret: aaaa-bbbb-cccc-dddd-eeee + # 所有允许授权的 url + allow-redirect-uris: + - http://sa-oauth-client.com:8002 + - http://sa-oauth-client.com:8002/* + # 所有签约的权限 + contract-scopes: + - openid + - userid + - userinfo + # 所有允许的授权模式 + allow-grant-types: + - authorization_code + - implicit + - refresh_token + - password + - client_credentials + # 客户端2 + 1002: + # 客户端id + client-id: 1002 + # 更多配置 ... +``` + +``` properties +########### 客户端1 +# 客户端id +sa-token.oauth2-server.clients.1001.client-id=1001 +# 客户端秘钥 +sa-token.oauth2-server.clients.1001.client-secret=aaaa-bbbb-cccc-dddd-eeee +# 所有允许授权的 url +sa-token.oauth2-server.clients.1001.allow-redirect-uris[0]=http://sa-oauth-client.com:8002 +sa-token.oauth2-server.clients.1001.allow-redirect-uris[1]=http://sa-oauth-client.com:8002/* +# 所有签约的权限 +sa-token.oauth2-server.clients.1001.contract-scopes[0]=openid +sa-token.oauth2-server.clients.1001.contract-scopes[1]=userid +sa-token.oauth2-server.clients.1001.contract-scopes[2]=userinfo +# 所有允许的授权模式 +sa-token.oauth2-server.clients.1001.allow-grant-types[0]=authorization_code +sa-token.oauth2-server.clients.1001.allow-grant-types[1]=implicit +sa-token.oauth2-server.clients.1001.allow-grant-types[2]=refresh_token +sa-token.oauth2-server.clients.1001.allow-grant-types[3]=password +sa-token.oauth2-server.clients.1001.allow-grant-types[4]=client_credentials + +########### 客户端2 +sa-token.oauth2-server.clients.1002.client-id=1002 +sa-token.oauth2-server.clients.1002.client-secret=... +``` + + + +这两种方案都是基于内存形式的 client 信息配置,只适合简单的测试,一般真实项目的 client 信息都是保存在数据库中的,下面演示一下如何在数据库中动态获取 client 信息 + + +### 2、基于数据库的数据加载 + +你只需要自定义数据加载器:新建 `SaOAuth2DataLoaderImpl` 实现 `SaOAuth2DataLoader` 接口。 + + +``` java +/** + * Sa-Token OAuth2:自定义数据加载器 + */ +@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") // 所有签约的权限 + .addAllowGrantTypes( // 所有允许的授权模式 + GrantType.authorization_code, // 授权码式 + GrantType.implicit, // 隐式式 + GrantType.refresh_token, // 刷新令牌 + GrantType.password, // 密码式 + GrantType.client_credentials // 客户端模式 + ) + ; + } + 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-doc/oauth2/oauth2-dev.md b/sa-token-doc/oauth2/oauth2-dev.md new file mode 100644 index 000000000..bfe6361d9 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-dev.md @@ -0,0 +1,237 @@ +# Sa-Token-OAuth2 Server端 二次开发用到的所有函数说明 + +官方示例只提供了基本的授权流程,以及 userinfo 资源的开放,如果您需要开放更多的接口,则二次开发时可能用到以下相关 API 方法 + +--- + +### Client 信息相关 + +``` java +// 获取 ClientModel,根据 clientId +SaOAuth2Util.getClientModel(clientId); + +// 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 +SaOAuth2Util.checkClientModel(clientId); + +// 校验:clientId 与 clientSecret 是否正确 +SaOAuth2Util.checkClientSecret(clientId, clientSecret); + +// 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes +SaOAuth2Util.checkClientSecretAndScope(clientId, clientSecret, scopes); + +// 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false +SaOAuth2Util.isContractScope(clientId, scopes); + +// 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 +SaOAuth2Util.checkContractScope(clientId, scopes); + +// 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 +SaOAuth2Util.checkContractScope(clientModel, scopes); + +// 校验:该 Client 使用指定 url 作为回调地址,是否合法 +SaOAuth2Util.checkRedirectUri(clientId, url); + +// 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope +SaOAuth2Util.isGrantScope(loginId, clientId, scopes); + +// 删除:指定 loginId 针对指定 Client 的授权信息 +SaOAuth2Util.deleteGrantScope(loginId, clientId); +``` + + +### Code 相关 +``` java +// 获取 CodeModel,无效的 code 会返回 null +SaOAuth2Util.getCode(code); + +// 校验 Code,成功返回 CodeModel,失败则抛出异常 +SaOAuth2Util.checkCode(code); + +// 获取 Code,根据索引: clientId、loginId +SaOAuth2Util.getCodeValue(clientId, loginId); +``` + + +### Access-Token 相关 +``` java +// 获取 AccessTokenModel,无效的 AccessToken 会返回 null +SaOAuth2Util.getAccessToken(accessToken); + +// 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 +SaOAuth2Util.checkAccessToken(accessToken); + +// 获取 Access-Token 列表:此应用下 对 某个用户 签发的所有 Access-token +SaOAuth2Util.getAccessTokenValueList(clientId, loginId); + +// 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false +SaOAuth2Util.hasAccessTokenScope(accessToken, ...scopes); + +// 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 +SaOAuth2Util.checkAccessTokenScope(accessToken, ...scopes); + +// 获取 Access-Token 所代表的LoginId +SaOAuth2Util.getLoginIdByAccessToken(accessToken); + +// 获取 Access-Token 所代表的 clientId +SaOAuth2Util.getClientIdByAccessToken(accessToken); + +// 回收一个 Access-Token +SaOAuth2Util.revokeAccessToken(accessToken); + +// 回收全部 Access-Token:指定应用下 指定用户 的全部 Access-Token +SaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId); +``` + + +### Refresh-Token 相关 +``` java +// 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null +SaOAuth2Util.getRefreshToken(refreshToken); + +// 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 +SaOAuth2Util.checkRefreshToken(refreshToken); + +// 获取 Refresh-Token 列表:此应用下 对 某个用户 签发的所有 Refresh-Token +SaOAuth2Util.getRefreshTokenValueList(clientId, loginId); + +// 回收一个 Refresh-Token +SaOAuth2Util.revokeRefreshToken(refreshToken); + +// 回收全部 Refresh-Token:指定应用下 指定用户 的全部 Refresh-Token +SaOAuth2Util.revokeRefreshTokenByIndex(clientId, loginId); + +// 根据 RefreshToken 刷新出一个 AccessToken +SaOAuth2Util.refreshAccessToken(refreshToken); +``` + + +### Client-Token 相关 + +``` java +// 获取 ClientTokenModel,无效的 ClientToken 会返回 null +SaOAuth2Util.getClientToken(clientToken); + +// 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 +SaOAuth2Util.checkClientToken(clientToken); + +// 获取 Client-Token 列表:此应用下 对 某个用户 签发的所有 Client-token +SaOAuth2Util.getClientTokenValueList(clientId); + +// 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false +SaOAuth2Util.hasClientTokenScope(clientToken, ...scopes); + +// 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 +SaOAuth2Util.checkClientTokenScope(clientToken, ...scopes); + +// 回收一个 ClientToken +SaOAuth2Util.revokeClientToken(clientToken); + +// 回收全部 Client-Token:指定应用下的全部 Client-Token +SaOAuth2Util.revokeClientTokenByIndex(clientId); +``` + + +### 请求查询 + +``` java +// 数据读取:从当前请求对象中读取 access_token,并查询到 AccessTokenModel 信息,无效 access_token 抛出异常 +// 1、请求参数 access_token,2、请求头 Authorization Bearer access_token +SaOAuth2Util.currentAccessToken(); + +// 数据读取:从当前请求对象中读取 client_token,并查询到 ClientTokenModel 信息,无效 client_token 抛出异常 +// 1、请求参数 client_token,2、请求头 Authorization Bearer client_token +SaOAuth2Util.currentClientToken(); +``` + + + +详情请参考源码:[码云:SaOAuth2Util.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java) + + +### OAuth2-Server 所有可重写策略 + + +#### 权限处理器 +``` java +// 根据 scope 信息对一个 AccessTokenModel 进行加工处理 +SaOAuth2Strategy.instance.workAccessTokenByScope = at -> { + // ... +} + +// 当使用 RefreshToken 刷新 AccessToken 时,根据 scope 信息对一个 AccessTokenModel 进行加工处理 +SaOAuth2Strategy.instance.refreshAccessTokenWorkByScope = at -> { + // ... +} + +// 根据 scope 信息对一个 ClientTokenModel 进行加工处理 +SaOAuth2Strategy.instance.workClientTokenByScope = at -> { + // ... +} +``` + + +#### grant_type 处理器 +``` java +// 根据 grantType 构造一个 AccessTokenModel +SaOAuth2Strategy.instance.grantTypeAuth = req -> { + // ... +} +``` + + +#### 凭证创建 +``` java +// 创建一个 code value +SaOAuth2Strategy.instance.createCodeValue = (clientId, loginId, scopes) -> { + // ... +} + +// 创建一个 AccessToken value +SaOAuth2Strategy.instance.createAccessToken = (clientId, loginId, scopes) -> { + // ... +} + +// 创建一个 RefreshToken value +SaOAuth2Strategy.instance.createRefreshToken = (clientId, loginId, scopes) -> { + // ... +} + +// 创建一个 ClientToken value +SaOAuth2Strategy.instance.createClientToken = (clientId, scopes) -> { + // ... +} +``` + + +#### 认证流程回调 +``` java +// OAuth-Server端:未登录时返回的View +SaOAuth2Strategy.instance.notLoginView = () -> { + // ... +} + +// OAuth-Server端:确认授权时返回的View +SaOAuth2Strategy.instance.confirmView = (clientId, scopes) -> { + // ... +} + +// OAuth-Server端:登录函数 +SaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> { + // ... +} + +// OAuth-Server端:用户在授权指定 client 前的检查,如果检查不通过,请直接抛出异常 +SaOAuth2Strategy.instance.userAuthorizeClientCheck = (loginId, clientId) -> { + // ... +} +``` + + +#### 其它 +``` java +// 在创建 SaClientModel 时,设置其默认字段 +SaOAuth2Strategy.instance.setSaClientModelDefaultFields = (clientModel) -> { + // ... +} +``` + diff --git a/sa-token-doc/oauth2/oauth2-h5.md b/sa-token-doc/oauth2/oauth2-h5.md new file mode 100644 index 000000000..391574521 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-h5.md @@ -0,0 +1,128 @@ +# OAuth2-Server 端前后台分离 + +### 1、设计分析 + +要使 OAuth2-Server 端做到前后台分离,则需要对接口进行一部分改造: + +- 改造前的接口列表: + - `http:{后端主机}/oauth2/authorize` + - `http:{后端主机}/oauth2/token` + - `http:{后端主机}/oauth2/refresh` + - 更多... +- 改造后的接口列表: + - `http:{前端主机}/oauth2/authorize` + - `http:{后端主机}/oauth2/token` + - `http:{后端主机}/oauth2/refresh` + - 更多... + +也就是,只需要重点改造 `/oauth2/authorize` 一个接口即可,`/oauth2/authorize` 接口主要做了三件事: +1. 判断用户在 oauth2-server 端是否登录,未登录会进入 [登录页面],已登录则进入下一步。 +2. 判断应用请求的 scope 是否需要用户手动确认授权,需要会进入 [确认授权页面],不需要则进入下一步。 +3. 重定向至 `redirect_uri` 指定的 url 地址,并携带 code 授权码参数。 + +我们只需要把上述逻辑从 oauth2-server 的后端搬到 oauth2-server 的前端即可。 + + +### 2、OAuth2-Server 后端添加接口 + +首先在 `oauth2-server` 的后端添加一个接口,用于获取最终授权重定向地址: + +``` java +/** + * Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口) + */ +@RestController +public class SaOAuth2ServerH5Controller { + + /** + * 获取最终授权重定向地址,形如:http://xxx.com/xxx?code=xxxxx + * + *

情况1:客户端未登录,返回 code=401,提示用户登录

+ *

情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认

+ *

情况3:已登录且请求的 scope 已确认授权,返回 code=200,redirect_uri=最终重定向 url 地址(携带code码参数)

+ * + * @return / + */ + @PostMapping("/oauth2/getRedirectUri") + public Object getRedirectUri() { + + // 获取变量 + SaRequest req = SaHolder.getRequest(); + SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); + SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); + SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); + String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type); + + // 1、先判断是否开启了指定的授权模式 + SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg); + + // 2、如果尚未登录, 则先去登录 + long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L); + if(loginId == 0L) { + return SaResult.get(401, "need login", null); + } + + // 3、构建请求 Model + RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId); + + // 4、开发者自定义的授权前置检查 + SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId); + + // 5、校验:重定向域名是否合法 + oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri); + + // 6、校验:此次申请的Scope,该Client是否已经签约 + oauth2Template.checkContractScope(ra.clientId, ra.scopes); + + // 7、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面 + boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes); + if(isNeedCarefulConfirm) { + SaClientModel cm = oauth2Template.checkClientModel(ra.clientId); + if( ! cm.getIsAutoConfirm()) { + // code=411,需要用户手动确认授权 + return SaResult.get(411, "need confirm", null); + } + } + + // 8、判断授权类型,重定向到不同地址 + // 如果是 授权码式,则:开始重定向授权,下放code + if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) { + CodeModel codeModel = dataGenerate.generateCode(ra); + String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); + return SaResult.ok().set("redirect_uri", redirectUri); + } + + // 如果是 隐藏式,则:开始重定向授权,下放 token + if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) { + AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null); + String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); + return SaResult.ok().set("redirect_uri", redirectUri); + } + + // 默认返回 + throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125); + } + +} +``` + +### 3、新建前端项目 + +既然是前后台分离,那肯定要有一个独立的前端项目,所需代码比较冗长,不便于在文档处直接展示,大家可以参考仓库示例`/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5`(完整压缩包见 [Sa-Token 集成示例大全下载](/more/download-demos) ): + + + + +### 4、运行测试 + +在前端 ide 中导入 demo 案例的 `sa-token-demo-oauth2-server-h5` 项目,然后直接预览 `oauth2-authorize.html` 页面,如图所示: + +sa-oauth2-server-authorize-h5.png + +复制上述地址,然后将其配置到 “OAuth2前端测试页” 的 “OAuth2 Server 授权页地址” 选项中,其它选项保持默认不变: + +sa-oauth2-client-test-h5-page-setting.png + +然后根据 “OAuth2前端测试页” 的页面提示进行测试即可,此处不再赘述。 + + diff --git a/sa-token-doc/oauth2/oauth2-interworking.md b/sa-token-doc/oauth2/oauth2-interworking.md new file mode 100644 index 000000000..f2c102245 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-interworking.md @@ -0,0 +1,72 @@ +# OAuth2 与登录会话实现数据互通 + +--- + +### 前提 + +前提,我们: +- 把 OAuth2 模块生成的令牌称作资源令牌(access_token), +- 把 StpUtil 登录会话生成的令牌称作会话令牌(satoken)。 + +正常情况下,资源令牌 与 会话令牌 的数据是不互通的,具体表现就是:当我们拿着 access_token 去访问 satoken 令牌的接口,会被抛出异常:`无效Token:xxxxx` + +那么,有什么办法可以做到这两个模块的数据互通呢? + + + +### OAuth2-Server 端数据互通 + +很简单,你只需要在 `configOAuth2Server` 中重写 Access-Token 的生成策略: + +``` java +// Sa-Token OAuth2 定制化配置 +@Autowired +public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + // 其它配置 ... + + // 重写 AccessToken 创建策略,返回会话令牌 + SaOAuth2Strategy.instance.createAccessToken = (clientId, loginId, scopes) -> { + System.out.println("----返回会话令牌"); + return StpUtil.getOrCreateLoginSession(loginId); + }; + +} +``` + +重启项目,然后在 OAuth2 模块授权登录,现在生成的 `access_token` ,可以用来访问 `satoken` 的会话接口了。 + + +> [!WARNING| label:注意点] +> 数据互通,让前端与后端的交互更加方便,一个 token 即可访问所有接口,但也一定程度上失去了OAuth2的 “不同 Client 不同权限” 的设计意义, +> 同时也默认每个 Client 都拥有了账号的会话权限(access_token 与 satoken 为同一个)。 +> +> 应该根据自己的架构合理分析是否应该整合数据互通。 + + + +### OAuth2-Client 数据互通 +除了Server端,Client端也可以打通 `access_token` 与 `satoken` 会话。做法是在 Client 端拿到 `access_token` 后进行登录时,使用 `SaLoginParameter` 预定登录生成的 Token 值 + +``` java +// 1. 获取到access_token +String access_token = ... + +// 2. 登录时预定生成的token +StpUtil.login(uid, new SaLoginParameter().setToken(access_token)); + +// 3. 其它代码... +``` + + + +**疑问:数据互通后,两个 token 的过期策略是什么?** + +会话 token 由 `sa-token.timeout` 决定,`access_token` 由 `sa-token.oauth2-server.access-token-timeout` 决定。 + +数据互通只是将 token 拷贝一份进行复用,动作完成之后两者不再有任何联系。 + + + + + + diff --git a/sa-token-doc/oauth2/oauth2-oidc.md b/sa-token-doc/oauth2/oauth2-oidc.md new file mode 100644 index 000000000..6b6f0dde5 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-oidc.md @@ -0,0 +1,173 @@ +# OAuth2 开启 OIDC 协议 (OpenID Connect) + +--- + +### 1、开启步骤 + +1、引入 `sa-token-jwt` 依赖,用来签发 `id_token` + + + +``` xml + + + cn.dev33 + sa-token-jwt + ${sa.top.version} + +``` + +``` gradle +// sa-token-jwt 签发 OIDC id_token 令牌 +implementation 'cn.dev33:sa-token-jwt:${sa.top.version}' +``` + + + +2、在 `SaOAuth2DataLoader` 实现类中,返回的 `SaClientModel` 中添加 `oidc` 的签约权限。 + +``` java +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + @Override + public SaClientModel getClientModel(String clientId) { + // 此为模拟数据,真实环境需要从数据库查询 + if("1001".equals(clientId)) { + return new SaClientModel() + // .... + .addContractScopes("openid", "userid", "userinfo", "oidc") // 此处添加上签约权限:oidc + .addAllowGrantTypes( + // ... + ) + ; + } + return null; + } + // 其它代码 ... +} +``` + +3、在 `application.yml` 配置文件中配置 jwt 生成秘钥: + + + +``` yaml +sa-token: + # jwt秘钥 + jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk +``` + +``` properties +# jwt秘钥 +sa-token.jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk +``` + + +注:为了安全起见请不要直接复制官网示例这个字符串(随便按几个字符就好了) + + + +### 2、测试 + +1、在 OAuth2-Client 端申请授权码时,添加上 `oidc` 权限: +``` url +http://sa-oauth-server.com:8000/oauth2/authorize + ?response_type=code + &client_id=1001 + &redirect_uri=http://sa-oauth-client.com:8002/ + &scope=oidc +``` + +2、得到授权码后,然后拿着 `code` 换 `access_token` +``` url +http://sa-oauth-server.com:8000/oauth2/token + ?grant_type=authorization_code + &client_id=1001 + &client_secret=aaaa-bbbb-cccc-dddd-eeee + &code=${code} +``` + +3、返回的结果中将包含 `id_token` 字段: +``` js +{ + "code": 200, + "msg": "ok", + "data": null, + "token_type": "Bearer", + "access_token": "WdpjZdGlXdOzsAcr7gqPwmLVInHrhpznQa2pDOVqZmLXQynBflkcWqE6f5o2", + "refresh_token": "hKHwBm3eH6iqSHlXRGWQaziV8OoyHvzmUb97lKEEZnZJLt3NunBFx7rVZWbT", + "expires_in": 7199, + "refresh_expires_in": 2591999, + "client_id": "1001", + "scope": "oidc", + "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzI0NDI1OTg5LCJpYXQiOjE3MjQ0MjUzODksImF1dGhfdGltZSI6MTcyNDQwMDUyNiwibm9uY2UiOiJLTHlNR08zZ1R0YVdhMEFRcHF0RUNpTk9SWkY1QkhvRCIsImF6cCI6IjEwMDEifQ.gP3UYMexaQ9v0huKUuqhV9-dPxPpaEuFPIlPb2UZaOI" +} +``` + +4、解析 `id_token` 将得到以下载荷 +``` js +{ + "iss": "http://sa-oauth-server.com:8000", // 签发人 + "sub": "10001", // userId + "aud": "1001", // clientId + "exp": 1724425989, // 令牌到期时间,10位时间戳 + "iat": 1724425389, // 签发此令牌的时间,10位时间戳 + "auth_time": 1724400526, // 用户认证时间,10位时间戳 + "nonce": "KLyMGO3gTtaWa0AQpqtECiNORZF5BHoD", // 随机数,防止重放攻击 + "azp": "1001" // clientId +} +``` + +如果默认携带的载荷无法满足你的业务需求,你还可以自定义追加扩展字段,让 `id_token` 返回更多信息 + + +### 3、扩展 id_token 载荷 + +新建 `CustomOidcScopeHandler` 继承 `OidcScopeHandler`,扩展 OIDC 权限处理器,返回更多字段: +``` java +/** + * 扩展 OIDC 权限处理器,返回更多字段 + */ +@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.com/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; + } + +} +``` + +重启项目,再次请求授权,返回的 `id_token` 载荷将包含更多字段: + +``` js +{ + "iss": "http://sa-oauth-server.com:8000", + "sub": "10001", + "aud": "1001", + "exp": 1724430149, + "iat": 1724429549, + "auth_time": 1724400526, + "nonce": "SBRLOcfeo9FFmLTB8OINvuulam5FMOre", + "azp": "1001", + "uid": "10001", + "nickname": "lin_xiao_lin", + "picture": "https://sa-token.com/logo.png", + "email": "456456@xx.com", + "phone_number": "13144556677" +} +``` + + diff --git a/sa-token-doc/oauth2/oauth2-openid.md b/sa-token-doc/oauth2/oauth2-openid.md new file mode 100644 index 000000000..3f327e8a7 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-openid.md @@ -0,0 +1,191 @@ +# OpenId 与 UnionId + +

+ 视频讲解:OAuth2 授权流程中的 clientId、openId、unionId、userId 都是干嘛的? +

+ + +### 1、OpenId + +openid 是用户在某一 client 下的唯一标识,其有如下特点: + +- 一个用户在同一个 client 下,openid 是固定的,每次请求都会返回相同的值。 +- 一个用户在不同的 client 下,openid 是不同的,会返回不同的值。 + +oauth2-client 在每次授权时可根据返回的 openid 值来确定用户身份。 + +框架默认的 openid 生成算法为: +``` java +md5(prefix + "_" + clientId + "_" + loginId); +``` + +其中的 prefix 前缀默认值为:`openid_default_digest_prefix`,你可以通过以下方式配置: + + + +``` yaml +# sa-token配置 +sa-token: + oauth2-server: + # 默认 openid 生成算法中使用的摘要前缀 + openid-digest-prefix: xxxxxx +``` + +``` properties +# 默认 openid 生成算法中使用的摘要前缀 +sa-token.oauth2-server.openid-digest-prefix=xxxxxx +``` + + + +你也可以通过实现 `SaOAuth2DataLoader` 接口完全自定义 OpenId 生成算法: + +``` java +/** + * Sa-Token OAuth2:自定义数据加载器 + */ +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + + // 自定义 openid 生成算法 + @Override + public String getOpenid(String clientId, Object loginId) { + // 此种写法代表使用框架默认算法生成 openid,真实环境建议改为从数据库查询 + return SaOAuth2DataLoader.super.getOpenid(clientId, loginId); + } + +} +``` + + +#### openid 算法要求 + + +正常来讲,openid 算法需要保证: + +1. 单个 clientId 下同一 loginId 生成的 `openid` 一致。[必须] +2. 多个 clientId 下同一 loginId 生成的 `openid` 不一致。[非常建议] +3. 客户端无法通过 clientId + loginId 推测 `openid` 值。[建议] +4. 客户端无法通过 clientId + loginId + openid 推测该 loginId 在其它 clientId 下的 `openid` 值。[建议] +5. oauth2-server 自身由 `openid` 可以反查出对应的 clientId 和 loginId。[根据业务需求而定是否满足] + +框架内置的算法,可以满足 1和2,如果自定义了 `sa-token.oauth2-server.openid-digest-prefix` 配置,可以满足3。 + +如果自定义配置的 prefix 长度较短,或比较简单呈现规律性,则有客户端根据 clientId + loginId + openid 穷举爆破出 `prefix` 的风险, +从而获得提前计算彩虹表来推测出其它 clientId、loginId 对应 openid 值的能力。 + +如果自定义的 prefix 前缀比较复杂,让客户端无法爆破,则可以满足4。但依然无法满足5。 + +所以 openid 算法的最优解,应该是 oauth2-server 采用随机字符串作为 openid,然后自建数据库表来维护其映射关系,这样可以同时满足12345。 + +表结构参考如下: + +- id:数据id,主键。 +- client_id:应用id。 +- user_id:用户账号id。 +- openid:对应的 openid 值,随机字符串。 +- create_time:数据创建时间。 +- xxx:其它需要扩展的字段。 + + + +### 2、UnionId + +`UnionId` 的特点与 `OpenId` 几乎一致:同一用户在不同 client 里的 UnionId 值是不同的,除非这些应用属于同一主体。 + +例如:甲公司申请了`应用A`、`应用B`、`应用C`,乙公司申请了`应用D`、`应用F`,那么用户张三: +- 在应用 A、B、C 里的 UnionId 值一致。 +- 在应用 D、F 里的 UnionId 值一致。 +- 在应用 A 和 应用 D 之间,UnionId 值不一致。 + +那么 Sa-Token 框架是如何识别到某两个应用是否为同一主体的呢?这就需要你在注册应用时指定 `subjectId` 属性了: + +``` java +/** + * Sa-Token OAuth2:自定义数据加载器 + */ +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + + // 根据 clientId 获取 Client 信息 + @Override + public SaClientModel getClientModel(String clientId) { + // 此为模拟数据,真实环境需要从数据库查询 + if("1001".equals(clientId)) { + return new SaClientModel() + .setClientId("xxxx") + .setClientSecret("xxxx") + .setSubjectId("1000001") // ⚠️ 关键代码:主体 id (可选) + // .... + ; + } + return null; + } + +} +``` + +`subjectId` 代表此应用的拥有者,相同 `subjectId` 值的应用将被识别为同一主体,在授权中返回的 `unionid` 值也将一致。 + +框架默认的 `unionid` 生成算法为: + +``` java +md5(prefix + "_" + subjectId + "_" + loginId); +``` + +其中的 prefix 前缀默认值为:`unionid_default_digest_prefix`,你可以通过以下方式配置: + + + +``` yaml +# sa-token配置 +sa-token: + oauth2-server: + # 默认 unionid 生成算法中使用的摘要前缀 + unionid-digest-prefix: xxxxxx +``` + +``` properties +# 默认 unionid 生成算法中使用的摘要前缀 +sa-token.oauth2-server.unionid-digest-prefix=xxxxxx +``` + + + +你也可以通过实现 `SaOAuth2DataLoader` 接口完全自定义 UnionId 生成算法: + +``` java +/** + * Sa-Token OAuth2:自定义数据加载器 + */ +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + + // 自定义 unionid 生成算法 + @Override + public String getUnionid(String subjectId, Object loginId) { + // 此种写法代表使用框架默认算法生成 unionid,真实环境建议改为从数据库查询 + return SaOAuth2DataLoader.super.getUnionid(subjectId, loginId); + } + +} +``` + +unionid 算法要求与 openid 基本一致,可参考上述 openid 算法要求介绍,此处暂不赘述。 + + + + +### 3、总结 + +| 类型 | 概念 | +| :-------- | :-------- | +| userid | 在 oauth2-server 端的用户,其唯一标识 | +| clientid | 第三方公司在 oauth2-server 开放平台申请的应用,其唯一标识 | +| openid | 用户在某个应用下的唯一标识 | +| unionid | 用户在某一组应用下的唯一标识 (按照主体id分组) | + + + + + diff --git a/sa-token-doc/oauth2/oauth2-questions.md b/sa-token-doc/oauth2/oauth2-questions.md new file mode 100644 index 000000000..d2d41df3f --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-questions.md @@ -0,0 +1,52 @@ +# Sa-Token-OAuth2整合-常见问题总结 + +OAuth2 集成常见问题整理 + +[[toc]] + +--- + + +### 问:搭建好 oauth2-server 服务后,访问返回:`{"msg": "not handle"}`。 + +返回这个信息,代表你访问的路由有错误,比如说: + +- 统一认证登录地址是:`http://{host}:{port}/oauth2/authorize`。 +- 而你访问的却是:`http://{host}:{port}/oauth2/authorize2`。 + +地址写错了,框架就不会处理这个请求,会直接返回 `{"msg": "not handle"}`,所有开放地址可参考:[OAuth2 开放接口](/oauth2/oauth2-apidoc) + + + +### 问:我参照文档搭建 oauth2-server,一直提示:code 无效,请问怎么回事? +一个 code 码只能使用一次,多次使用就会报这个错。 + + + +### 问:Sa-Token-OAuth2 怎么集成多账号模式? + +在 `configOAuth2Server` 里指定 oauth2 模块使用的 `StpLogic` 对象即可: + +``` java +// Sa-Token OAuth2 定制化配置 +@Autowired +public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + // 其它配置 ... + + // 指定 oauth2 模块使用的 `StpLogic` 对象 + SaOAuth2Manager.setStpLogic(StpUserUtil.stpLogic); +} +``` + + + + +### 问:授权码流程中 state 参数是干吗用的? + +state 参数用于验证授权码流程的发起端和接受端是否为同一个客户端,以防止OAuth-Server账号伪装攻击。 + +授权流程发起端必须保证: +- state 参数必须足够随机,不可被预测。 +- state 参数与授权码流程发起客户端 一 一 对 应,授权流程发起时创建的 state 必须与接受时返回的 state 值一致。 +- 安全起见,一个 state 参数只允许使用一次。 + diff --git a/sa-token-doc/oauth2/oauth2-scope-level.md b/sa-token-doc/oauth2/oauth2-scope-level.md new file mode 100644 index 000000000..dbb94cf47 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-scope-level.md @@ -0,0 +1,137 @@ +# OAuth2 - 为 Scope 划分等级 + + +### 1、划分等级 + +我们可以通过配置文件来为 scope 划分等级 + + + +``` yaml +# sa-token 配置 +sa-token: + # OAuth2.0 配置 + oauth2-server: + # 定义哪些 scope 是高级权限,多个用逗号隔开 + higher-scope: openid,userid + # 定义哪些 scope 是低级权限,多个用逗号隔开 + lower-scope: userinfo +``` + +``` properties +# 定义哪些 scope 是高级权限,多个用逗号隔开 +sa-token.oauth2-server.higher-scope=openid,userid +# 定义哪些 scope 是低级权限,多个用逗号隔开 +sa-token.oauth2-server.lower-scope=userinfo +``` + + +如上所示: +- 通过 `sa-token.oauth2-server.higher-scope` 配置项指定的 `scope` 将变成 **高级权限**。 +- 通过 `sa-token.oauth2-server.lower-scope` 配置项指定的 `scope` 将变成 **低级权限**。 +- 其它未指定的 `scope` 将默认为 **一般权限**。 + +不同的权限等级其差异主要表现在:oauth2-client 授权时是否需要用户手动确认授权。 + +| 权限等级 | 申请授权时表现 | +| :-------- | :-------- | +| 高级权限 | 申请授权时:每次都需要用户手动点击确认授权按钮,才会下放 code 授权码 | +| 一般权限 | 申请授权时:如果申请的 scope 用户近期授权过,则静默授权,如果近期未授权过,则需要手动点击确认授权按钮 | +| 低级权限 | 申请授权时:不需要用户手动点击确认授权,程序自动完成静默授权 | + + +### 2、详细举例 + +1、如下例子,oauth2-client 申请的 `openid` 权限为**高级权限**,每次都需要用户手动点击确认授权按钮,才会下放 code 授权码。 + +``` url +http://{host}:{port}/oauth2/authorize + ?response_type=code + &client_id=1001 + &redirect_uri=http://sa-oauth-client.com:8002/ + &scope=openid +``` + +2、如下例子,oauth2-client 申请的 `userinfo` 权限为**低级权限**,此时不需要用户手动点击确认授权,程序自动完成静默授权。 + +``` url +http://{host}:{port}/oauth2/authorize + ?response_type=code + &client_id=1001 + &redirect_uri=http://sa-oauth-client.com:8002/ + &scope=userinfo +``` + +3、如下例子,oauth2-client 申请的 `fans_list` 权限为**一般权限**,首次申请时,需要用户手动点击确认授权,第二次再申请则是静默授权。 + +``` url +http://{host}:{port}/oauth2/authorize + ?response_type=code + &client_id=1001 + &redirect_uri=http://sa-oauth-client.com:8002/ + &scope=fans_list +``` + +4、如下例子,oauth2-client 申请的 `openid,userid,userinfo,fans_list` 权限同时包括 **高级权限**、**低级权限**、**一般权限**: + +``` url +http://{host}:{port}/oauth2/authorize + ?response_type=code + &client_id=1001 + &redirect_uri=http://sa-oauth-client.com:8002/ + &scope=openid,userid,userinfo,fans_list +``` + +此时是否需要用户手动点击确认授权按钮?具体规则表现为: +- 如果请求的 scope 列表包括高级权限,则必须用户手动点击确认授权。 +- 如果 scope 列表不包括高级权限,则将 scope 列表中的所有低级权限剔除。 +- 剔除后的 list 大小如果为零,则直接静默授权通过。 +- 剔除后的 list 大小如果不为零,则判断剩余的这些 scope 是否全部已近期授权过: + - 如果是,则静默授权。 + - 如果否,则需要用户手动点击确认授权。 + + +### 3、申请高级权限时 `/oauth2/authorize` 无法通过验证 + +由于申请高级权限时,每次都必须用户手动点击确认授权,`/oauth2/authorize` 路由接口是无法完成权限验证操作的。 + +此时需要将构建 `redirect_uri` 的动作提前,在 `/oauth2/doConfirm` 确认授权接口时额外追加 `build_redirect_uri: true` 等参数: +``` url +http://{host}:{port}/oauth2/doConfirm + ?client={value} + &scope={value} + &build_redirect_uri=true + &response_type={value} + &redirect_uri={value} + &state={value} +``` + +返回结果示例: +``` js +{ + code: 200, + msg: 'ok', + data: null, + redirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC' +} +``` + +其中 `redirect_uri` 参数为授权挂载code地址,直接在 ajax 回调函数中使用 `location.href=res.redirect_uri` 跳转即可。 + +自定义确认授权视图修改参考: +``` java +// 授权确认视图 +cfg.confirmView = (clientId, scopes)->{ + String scopeStr = SaFoxUtil.convertListToString(scopes); + String yesCode = + "fetch('/oauth2/doConfirm' + location.search + '&build_redirect_uri=true', {method: 'POST'})" + + ".then(res => res.json())" + + ".then(res => location.href=res.redirect_uri)"; + String res = "

应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?

" + + "

" + + " " + + " " + + "

"; + return res; +}; +``` \ No newline at end of file diff --git a/sa-token-doc/oauth2/oauth2-server.md b/sa-token-doc/oauth2/oauth2-server.md new file mode 100644 index 000000000..efe73451b --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-server.md @@ -0,0 +1,294 @@ +# 搭建OAuth2-Server + +--- + +### 1、准备工作 +首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: +``` url +127.0.0.1 sa-oauth-server.com +127.0.0.1 sa-oauth-client.com +``` + + +### 2、引入依赖 +创建SpringBoot项目 `sa-token-demo-oauth2-server`(不会的同学自行百度或参考仓库示例:[Sa-Token 集成示例大全下载](/more/download-demos) 获取全套 Demo),引入 `pom.xml` 依赖: + + + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + + + + + cn.dev33 + sa-token-oauth2 + ${sa.top.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + +``` + +``` gradle +// Sa-Token 权限认证,在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' + +// Sa-Token OAuth2.0 模块 +implementation 'cn.dev33:sa-token-oauth2:${sa.top.version}' + +// Sa-Token 整合 RedisTemplate (可选) +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' +``` + + +注:Redis 相关依赖是非必须的,如果集成了 redis,可以让你更细致的观察到 sa-token-oauth2 的底层数据格式。 + + +### 3、开放服务 + + +1、新建`SaOAuth2ServerController` +``` java +/** + * Sa-Token OAuth2 Server端 控制器 + */ +@RestController +public class SaOAuth2ServerController { + + // OAuth2-Server 端:处理所有 OAuth2 相关请求 + @RequestMapping("/oauth2/*") + public Object request() { + System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl()); + return SaOAuth2ServerProcessor.instance.dister(); + } + + // Sa-Token OAuth2 定制化配置 + @Autowired + public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + + // 添加 client 信息 + oauth2Server.addClient( + new SaClientModel() + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowRedirectUris("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 + .addAllowGrantTypes( // 所有允许的授权模式 + GrantType.authorization_code, // 授权码式 + GrantType.implicit, // 隐式式 + GrantType.refresh_token, // 刷新令牌 + GrantType.password, // 密码式 + GrantType.client_credentials // 客户端模式 + ) + ); + + // 可以添加更多 client 信息,只要保持 clientId 唯一就行了 + // oauth2Server.addClient(...) + + // 配置:未登录时返回的View + SaOAuth2Strategy.instance.notLoginView = () -> { + // 简化模拟表单 + String doLoginCode = + "fetch(`/oauth2/doLogin?name=${document.querySelector('#name').value}&pwd=${document.querySelector('#pwd').value}`) " + + " .then(res => res.json()) " + + " .then(res => { if(res.code === 200) { location.reload() } else { alert(res.msg) } } )"; + String res = + "

当前客户端在 OAuth-Server 认证中心尚未登录,请先登录

" + + "用户:
" + + "密码:
" + + ""; + return res; + }; + + // 配置:登录处理函数 + SaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> { + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok(); + } + return SaResult.error("账号名或密码错误"); + }; + + // 配置:确认授权时返回的 view + SaOAuth2Strategy.instance.confirmView = (clientId, scopes) -> { + String scopeStr = SaFoxUtil.convertListToString(scopes); + String yesCode = + "fetch('/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scopeStr + "', {method: 'POST'})" + + ".then(res => res.json())" + + ".then(res => location.reload())"; + String res = "

应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?

" + + "

" + + " " + + " " + + "

"; + return res; + }; + } + +} +``` +注意: +- 在 `doLoginHandle` 函数里如果要获取 name, pwd 以外的参数,可通过 `SaHolder.getRequest().getParam("xxx")` 来获取。 +- 你可以在 [框架配置](/use/config?id=SaClientModel属性定义) 了解有关 `SaClientModel` 对象所有属性的详细定义。 + + +2、全局异常处理 +``` java +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } +} +``` + +3、创建启动类: +``` java +/** + * 启动:Sa-OAuth2 Server端 + */ +@SpringBootApplication +public class SaOAuth2ServerApplication { + public static void main(String[] args) { + SpringApplication.run(SaOAuth2ServerApplication.class, args); + System.out.println("\nSa-Token-OAuth2 Server端启动成功,配置如下:"); + System.out.println(SaOAuth2Manager.getServerConfig()); + } +} +``` +启动项目 + + +### 4、访问测试 + +1、由于暂未搭建Client端,我们可以使用 Sa-Token 官网作为重定向URL进行测试: +``` url +http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://sa-token.com&scope=openid +``` + +2、由于首次访问,我们在OAuth-Server端暂未登录,会被转发到登录视图 + +sa-oauth2-server-login-view + +3、输入 `sa/123456` 进行登录之后,会提示我们确认授权 +sa-oauth2-server-scope + +4、点击同意授权之后,我们会被重定向至 redirect_uri 页面,并携带了code参数 + +sa-oauth2-server-code + +4、我们拿着code参数,访问以下地址: +``` url +http://sa-oauth-server.com:8000/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code} +``` + +将得到 `Access-Token`、`Refresh-Token`、`openid`等授权信息: + +``` js +{ + "code": 200, + "msg": "ok", + "data": null, + "token_type": "Bearer", + "access_token": "cAls8jnBLmeo5yuCUMwb8zxaSsQPPzGINXF3NOCjCqFHplr6hagdT6A5HeR2", + "refresh_token": "L2rPbJ3aaOXwaB4Zu0EGWNz5EjVNpw5u2oMP9CS2IEap7rR3Hb76ZqqHS07J", + "expires_in": 7199, + "refresh_expires_in": 2591999, + "client_id": "1001", + "scope": "openid", + "openid": "ded91dc189a437dd1bac2274be167d50" +} +``` + + +测试完毕 + + +### 5、运行官方示例 +以上代码只是简单模拟了一下OAuth2.0的授权流程,现在,我们运行一下官方示例,里面有制作好的UI界面 + +- OAuth2-Server端: `/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/` [源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server)
+- OAuth2-Client端: `/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/` [源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client)
+ +依次启动`OAuth2-Server` 与 `OAuth2-Client`,然后从浏览器访问:[http://sa-oauth-client.com:8002](http://sa-oauth-client.com:8002) + +sa-oauth2-client-index + +如图,可以针对OAuth2.0四种模式进行详细测试 + + + +### 6、OAuth2 前端测试页 + +OAuth2 前端测试页: +`/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/` +[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5)
+ +此示例允许你在前端自由配置 OAuth-Client 端所需的各个参数,方便对 OAuth2 四种模式的测试。 + +sa-oauth2-client-index + +

+ 视频讲解:OAuth2 四种模式 前端测试页 +

+ + diff --git a/sa-token-doc/oauth2/readme.md b/sa-token-doc/oauth2/readme.md new file mode 100644 index 000000000..2c7753475 --- /dev/null +++ b/sa-token-doc/oauth2/readme.md @@ -0,0 +1,60 @@ +# Sa-Token-OAuth2.0 模块 + +--- + +### 什么是 OAuth2.0 ?解决什么问题? + +OAuth2.0 与 SSO 相比,增加了对应用授权范围的控制,减弱了应用之间数据同步的能力。 + +有关 OAuth2.0 的设计思想网上教程较多,此处不再重复赘述,详细可参考博客: +[OAuth2.0 简单解释](https://www.ruanyifeng.com/blog/2019/04/oauth_design.html) + + +如果你还不知道你的项目应该选择 SSO 还是 OAuth2.0,可以参考这篇:[技术选型:[ 单点登录 ] VS [ OAuth2.0 ]](/fun/sso-vs-oauth2) + + + +### OAuth2.0 四种模式 + +基于不同的使用场景,OAuth2.0设计了四种模式: + +1. 授权码(Authorization Code):OAuth2.0 标准授权步骤,Server 端向 Client 端下放 `Code` 码,Client 端再用 `Code` 码换取授权 `Access-Token`。 +2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server 端使用 URL 重定向方式直接将 `Access-Token` 下放到 Client 端页面。 +3. 密码式(Password):Client 端直接拿着用户的账号密码换取授权 `Access-Token`。 +4. 客户端凭证(Client Credentials):Server 端针对 Client 级别的 Token,代表应用自身的资源授权。 + +sa-oauth2-setup.png + +接下来我们将通过简单示例演示如何在 Sa-Token-OAuth2 中完成这四种模式的对接: [搭建OAuth2-Server](/oauth2/oauth2-server) + +若需下载官方仓库内全套可运行 Demo 压缩包,请见:[Sa-Token 集成示例大全下载](/more/download-demos) 。 + + +### OAuth2.0 第三方开放平台完整开发流程参考 + +1. oauth2-server 平台端 + 1. 搭建 oauth2-server 数据后台管理端,也称:后台管理。(后台人员对底层数据增删改查维护的地方)。 + 2. 搭建 oauth2-server 数据前台申请端,也称:开放平台。(给第三方公司提供一个申请注册 client 的地方) + 3. 搭建 oauth2-server 授权端 以及其接口文档,也称:认证中心。(让第三方公司拿到 access_token) + 4. 搭建 oauth2-server 资源端 以及其接口文档,也称:资源中心。(让第三方公司通过 access_token 拿到对应的资源数据) + 5. 以上四端可以是一个项目,也可以是四个独立的项目,也可以是一个后端 + 多个前端的形式。 + +2. oauth2-client 第三方公司端 + 1. 第三方公司登录 oauth-server 数据前台申请端,申请注册应用,拿到 `clientId`、`clientSecret` 等数据。 + 2. 根据自己的业务选择对应的 scope 申请签约,等待平台端审核通过。 + 3. 在自己系统通过 `clientId`、`clientSecret` 等参数对接 oauth2-server 授权端,拿到 `access_token`。 + 4. 通过 `access_token` 调用 oauth2-server 资源端接口,拿到对应资源数据。 + +3. 用户端操作 + 1. 打开第三方公司开发的网站或APP等程序。 + 2. 一般有个“通过xx第三方登录”的按钮,点它。 + 3. 跳转到了 oauth2-server 端的网站,在此网站用 oauth2-server 的账号开始登录。 + 4. 登录完成,继续跳转到授权页,点击确认授权。 + 5. 授权完成,oauth2-server 端生成一个 code 码,重定向回 oauth2-client 的网站,把 code 参数挂到对应的 url 上。 + 6. oauth2-client 从 url 中读取 code 参数,提交到 oauth2-client 的后端。 + 7. oauth2-client 后端拿着 `code`、`clientId`、`clientSecret` 等信息调用 oauth2-server 授权端 的接口,得到 `access_token`。 + 8. 继续拿着 `access_token` 调用 oauth2-server 资源端获取此用户对应的数据。 + 9. 一般最终目的拿到一个 openid 值,oauth2-client 根据 openid 进行登录。生成自己的会话 token ,返回到数据到前端。 + 10. 前端拿到自己 oauth2-client 生成的会话 token ,完成登录。开始进行业务操作。 + + diff --git a/sa-token-doc/plugin/alone-redis.md b/sa-token-doc/plugin/alone-redis.md new file mode 100644 index 000000000..147017493 --- /dev/null +++ b/sa-token-doc/plugin/alone-redis.md @@ -0,0 +1,173 @@ +# Sa-Token-Alone-Redis 独立Redis插件 +--- + +Sa-Token默认的Redis集成方式会把权限数据和业务缓存放在一起,但在部分场景下我们需要将他们彻底分离开来,比如: + +> [!NOTE| label:业务场景] +> 搭建两个Redis服务器,一个专门用来做业务缓存,另一台专门存放Sa-Token权限数据 + + + + + +要将Sa-Token的数据单独抽离出来很简单,你只需要为Sa-Token单独配置一个Redis连接信息即可 + +--- + + +### 1、首先引入Alone-Redis依赖 + + +> [!WARNING| label:Spring Boot 4 用户] +> 若使用 Spring Boot 4.x,请引入 `sa-token-alone-redis-by-spring-boot4` 替代 `sa-token-alone-redis`。 +> 注:当前版本下(v1.45.0),此包尚未发布到 Maven 中央仓库,如需使用请下载源码手动自行打包或直接将源码复制到你的项目中进行使用。 + + + + +``` xml + + + cn.dev33 + sa-token-alone-redis + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 整合 Redis (使用 jackson 序列化方式) +implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' +``` + + + +### 2、然后在application.yml中增加配置 + + + + +``` yaml +# Sa-Token 配置 +sa-token: + # Token名称 + token-name: satoken + # Token有效期 + timeout: 2592000 + # Token风格 + token-style: uuid + + # 配置 Sa-Token 单独使用的 Redis 连接 + alone-redis: + # Redis数据库索引(默认为0) + database: 2 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + +spring: + # 配置业务使用的 Redis 连接 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s +``` + + +``` properties +############## Sa-Token 配置 ############## +# Token名称 +sa-token.token-name=satoken +# Token有效期 +sa-token.timeout=2592000 +# Token风格 +sa-token.token-style=uuid + +############## 配置 Sa-Token 单独使用的 Redis 连接 ############## +# Redis数据库索引(默认为0) +sa-token.alone-redis.database=2 +# Redis服务器地址 +sa-token.alone-redis.host=127.0.0.1 +# Redis服务器连接端口 +sa-token.alone-redis.port=6379 +# Redis服务器连接密码(默认为空) +sa-token.alone-redis.password= +# 连接超时时间 +sa-token.alone-redis.timeout=10s + +############## 配置业务使用的 Redis 连接 ############## +# Redis数据库索引(默认为0) +spring.redis.database=0 +# Redis服务器地址 +spring.redis.host=127.0.0.1 +# Redis服务器连接端口 +spring.redis.port=6379 +# Redis服务器连接密码(默认为空) +spring.redis.password= +# 连接超时时间 +spring.redis.timeout=10s + +``` + + + + +具体可参考示例:[码云:application.yml](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml) + +集群配置说明: alone-redis同样可以配置集群(cluster模式和sentinel模式), 且基础配置参数和spring redis集群配置别无二致 + +集群配置示例可参考demo项目sa-token-demo-alone-redis-cluster + + +### 3、测试 +新建Controller测试一下 +``` java +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + StringRedisTemplate stringRedisTemplate; + + // 测试Sa-Token缓存 + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue="10001") String id) { + System.out.println("--------------- 测试Sa-Token缓存"); + StpUtil.login(id); + return SaResult.ok(); + } + + // 测试业务缓存 + @RequestMapping("test") + public SaResult test() { + System.out.println("--------------- 测试业务缓存"); + stringRedisTemplate.opsForValue().set("hello", "Hello World"); + return SaResult.ok(); + } + +} +``` + +分别访问两个接口,观察Redis中增加的数据 + +alone-redis + +测试完毕! + +### 4、注意点 +目前 Sa-Token-Alone-Redis 仅对以下插件有 Redis 分离效果: +- sa-token-redis-template +- sa-token-redis-template-jdk-serializer + +若使用的是 Redisson 而非 RedisTemplate,请改用 [Alone 独立 Redisson 插件](/plugin/alone-redisson)。 \ No newline at end of file diff --git a/sa-token-doc/plugin/alone-redisson.md b/sa-token-doc/plugin/alone-redisson.md new file mode 100644 index 000000000..5938b7d3a --- /dev/null +++ b/sa-token-doc/plugin/alone-redisson.md @@ -0,0 +1,153 @@ +# Sa-Token-Alone-Redisson 独立 Redisson 插件 +--- + +Sa-Token 默认的 Redisson 集成会把权限数据和业务缓存放在同一个 `RedissonClient` 上。需要彻底分离时,可再为 Sa-Token 单独起一个 Redisson 连接。 + +> [!NOTE| label:业务场景] +> 搭建两个 Redis:一个给业务 RedissonClient,另一台专门存放 Sa-Token 权限数据。 + +--- + +### 1、引入依赖 + +引入 `sa-token-alone-redisson` 即可,**无需再引入** `sa-token-redisson` 或 `sa-token-redisson-spring-boot-starter`(本插件已包含 Dao 实现并完成注册)。 + +业务代码如果也要用 `RedissonClient`: +- Spring Boot 2 / 3:可再引入官方 `redisson-spring-boot-starter`,或自行注册 Bean。 +- Spring Boot 4:当前 Redisson 3.45 的 starter 仍引用已移除的 `RedisAutoConfiguration`,**不要引入** `redisson-spring-boot-starter`,改为引入 `redisson` 并自行注册 Bean。`sa-token-alone-redisson` 本身无需 Boot 4 专用包。 + + + +``` xml + + + cn.dev33 + sa-token-alone-redisson + ${sa.top.version} + + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + +``` + +``` gradle +implementation 'cn.dev33:sa-token-alone-redisson:${sa.top.version}' +// 业务 Redis(按需) +implementation 'org.redisson:redisson-spring-boot-starter:${redisson.version}' +``` + + + +### 2、配置独立连接(Redisson 原生 yaml) + +配置项与官方 `spring.redis.redisson.config` / `file` 相同,单机用 `singleServerConfig`,集群用 `clusterServersConfig`。 + + + + +``` yaml +sa-token: + token-name: satoken + timeout: 2592000 + # Sa-Token 单独使用的 Redisson 连接 + alone-redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 2 + password: null + +spring: + redis: + # 业务使用的 Redisson 连接 + redisson: + config: | + singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 0 + password: null +``` + + +``` yaml +sa-token: + alone-redisson: + config: | + clusterServersConfig: + nodeAddresses: + - "redis://127.0.0.1:3000" + - "redis://127.0.0.1:3001" + - "redis://127.0.0.1:3002" + password: null +``` + + +``` yaml +# application.yml +sa-token: + alone-redisson: + file: classpath:sa-redisson.yml +``` + +`src/main/resources/sa-redisson.yml` 示例(单机): + +``` yaml +singleServerConfig: + address: "redis://127.0.0.1:6379" + database: 2 + password: null +``` + +集群则把内容换成上面「集群」页签里的 `clusterServersConfig` 即可。 + + + +`config` 与 `file` 二选一,同时配置时优先 `config`。 + +完整示例: +- 单机:[sa-token-demo-alone-redisson](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redisson/src/main/resources/application.yml) +- 集群:[sa-token-demo-alone-redisson-cluster](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/resources/application.yml) +- Spring Boot 3:[sa-token-demo-alone-redisson-sb3](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/resources/application.yml) +- Spring Boot 4:[sa-token-demo-alone-redisson-sb4](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/resources/application.yml) + + +### 3、测试 + +``` java +@RestController +@RequestMapping("/test/") +public class TestController { + + @Autowired + RedissonClient redissonClient; + + // 测试 Sa-Token 缓存 --- http://localhost:8084/test/login + @RequestMapping("login") + public SaResult login(@RequestParam(defaultValue="10001") String id) { + StpUtil.login(id); + return SaResult.ok(); + } + + // 测试业务缓存 --- http://localhost:8084/test/test + @RequestMapping("test") + public SaResult test() { + redissonClient.getBucket("hello").set("Hello World"); + return SaResult.ok(); + } +} +``` + +分别访问两个接口,观察两套 Redis 中增加的数据。 + + + +### 4、注意点 +- 引入本插件后,无需再引入 `sa-token-redisson` 或 `sa-token-redisson-spring-boot-starter`。 +- 业务代码注入的 `RedissonClient` 来自官方 `redisson-spring-boot-starter` 或你自己注册的 Bean,与 Sa-Token 使用的独立连接互不影响。Spring Boot 4 请自行注册,不要引入 3.45 的 starter。 +- `SaTokenDaoForRedisson` 默认使用 `StringCodec`,与业务 `RedissonClient` 的全局 codec 无关。 +- Redis Cluster 没有 database 索引,集群配置里不要写 `database`。 +- 哨兵模式同样走 Redisson 原生 yaml(`sentinelServersConfig`),按 [Redisson 配置文档](https://github.com/redisson/redisson/wiki/2.-Configuration) 填写即可。 diff --git a/sa-token-doc/plugin/aop-at.md b/sa-token-doc/plugin/aop-at.md new file mode 100644 index 000000000..c96a1206a --- /dev/null +++ b/sa-token-doc/plugin/aop-at.md @@ -0,0 +1,38 @@ +# AOP注解鉴权 +--- + +在 [注解式鉴权](/use/at-check) 章节,我们非常轻松的实现了注解鉴权, +但是默认的拦截器模式却有一个缺点,那就是无法在`Controller层`以外的代码使用进行校验 + +因此Sa-Token提供AOP插件,你只需在`pom.xml`里添加如下依赖,便可以在任意层级使用注解鉴权 + + + +``` xml + + + cn.dev33 + sa-token-spring-aop + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 整合 SpringAOP 实现注解鉴权 +implementation 'cn.dev33:sa-token-spring-aop:${sa.top.version}' +``` + + + +#### 注意点: +- 使用拦截器模式,只能把注解写在`Controller层`,使用AOP模式,可以将注解写在任意层级
+- **拦截器模式和AOP模式不可同时集成**,否则会在`Controller层`发生一个注解校验两次的bug + + + + + + + + + diff --git a/sa-token-doc/plugin/api-key.md b/sa-token-doc/plugin/api-key.md new file mode 100644 index 000000000..578543833 --- /dev/null +++ b/sa-token-doc/plugin/api-key.md @@ -0,0 +1,283 @@ +# API Key 接口调用秘钥 + +API Key(应用程序编程接口密钥) 是一种用于身份验证和授权的字符串代码,通常由服务提供商生成并分配给开发者或用户。它的主要作用是标识调用 API(应用程序编程接口)的请求来源,确保请求的合法性,并控制访问权限。 + +以上是官话,简单理解:API Key 是一种接口调用密钥,类似于会话 token ,但比会话 token 具有更灵活的权限控制。 + +示例仓库地址:[sa-token-demo-apikey](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-apikey) 🔗 + +sa-api-key + + +### 1、需求场景 + +为了帮助大家更好的理解 API Key 的应用场景,我们假设具有以下业务场景: + +> [!NOTE| label:业务场景] +> 你们公司开发了一款论坛网站,非常火爆。 +> +> 某日,你发现一位用户的头像可以随着日期而变化,Ta 的头像总是显示当前最新日期。 +> +> 这并未引起你的警觉,因为你是一个程序员,在你看来,写一个任务脚本,每天定时调用 API 更新自己的头像是一件非常简单的事情。 +> +> 一个月后,越来越多的账号“具有了此功能”,仿佛发生了人传人,Ta 们的头像都可以随着日期而变化,而且颜色各不相同,DIY 的不亦乐乎。 +> +> 这引起了你的怀疑,如此大批账号的自动化更新行为,显然不是 “某个程序员利用定时脚本更新账号信息” 可以解释的。 +> +> 一番调查之后,你发现了事情的真相,没有灰产公司捣乱,这批账号也不是机器账号,只是有一个公司为你们的网站开发了一款插件。 +> +> 这款插件的作用是:用户把自己的 账号+密码 保存在插件中,插件便可以定时更新该账号的头像、昵称、资料等信息。 +> +> 你觉得插件很有意思,但是插件“要求用户提交账号密码”的行为,让你感到很不爽。 +> +> 总有一些用户为了得到“些许便利”,而出卖自己的账号密码给插件。 +> +> 随着时间推移,越来越多的第三方公司或个人为你的网站开发插件:有的可以自动更新账号资料、有的可以自动发帖,有的检测到新粉丝就发送消息通知... +> +> 最终,不守规矩的插件出现了:一款插件在提供功能的同时,大量收集用户密码等隐私信息,作为不法用途。 +> +> 为了遏制这种现象,你们公司升级了系统,增加了 IP 校验等风控判断,阻断了这些插件的 API 调用。 +> +> 似乎……解决了问题?用户再也不会把账号密码交给第三方插件了。 +> +> 但是插件的需求总是存在的呀,有些用户确实很需要这些插件的能力来提高网站使用体验。 +> +> 俗话说的好,堵不如疏,既然用户有需求,第三方公司愿意免费打工开发插件,我们何不设计一套授权架构, +> 既不需要让用户把账号密码交给第三方插件,又能让插件得到一些权限来调用特定 API 为用户服务。 +> +> API Key 就是为了完成这种“可控式部分授权” 而设计的一种身份凭证。 + + +为了让第三方插件为用户工作,用户必定是要为插件提供一个“凭证”信息的,然后插件利用“凭证”信息,代替用户调用特定 API 完成一些功能。 + +不同的凭证信息将会带来不同的后果: + + +| 提供的凭证 | 后果 | +| :-------- | :-------- | +| 账号密码 | 插件可以得到账号所有权限,安全风险极高 | +| 会话 token | 插件可以调用几乎所有 API,安全风险极高,且容易受到用户退出登录导致 token 失效的影响 | +| API Key | 在可控的范围内进行部分授权,且可以方便的随时取消授权,只要设计得当,不会造成安全问题 | + +API Key 具有以下特点: +- 1、格式类似于会话 token,是一个随机字符串。 +- 2、每个 API Key 都会和具体的用户 id 发生绑定,后端可以查询到此 API Key 的授权人是谁。 +- 3、一个用户可以创建多个 API Key,用作不同的插件中。 +- 4、每个 API Key 都可以赋予不同的 scope 权限,以做到最小化授权。 +- 5、API Key 可以设置有效期,并且随时删除回收,做到灵活控制。 + + + +### 2、引入依赖 +在使用 API Key 模块之前,你必须先引入依赖: +``` xml + + + cn.dev33 + sa-token-apikey + ${sa.top.version} + +``` + + +### 3、创建 API Key + +理解了应用场景后,让我们看看 Sa-Token 为 API Key 提供了哪些方法: + + +``` java +// 为指定用户创建一个新的 API Key +ApiKeyModel akModel = SaApiKeyUtil.createApiKeyModel(10001).setTitle("test"); +System.out.println("API Key 值:" + akModel.getApiKey()); + +// 保存 API Key +SaApiKeyUtil.saveApiKey(akModel); + +// 删除 API Key +SaApiKeyUtil.deleteApiKey(apiKey); +``` + +一个 ApiKeyModel 可设置以下属性: +``` java +ApiKeyModel akModel = new ApiKeyModel(); +akModel.setLoginId(10001); // 设置绑定的用户 id +akModel.setApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp"); // 设置 API Key 值 +akModel.setTitle("commit"); // 设置名称 +akModel.setIntro("提交代码专用"); // 设置描述 +akModel.addScope("commit", "pull"); // 设置权限范围 +akModel.setExpiresTime(System.currentTimeMillis() + 2592000); // 设置失效时间,13位时间戳,-1=永不失效 +akModel.setIsValid(true); // 设置是否有效 +akModel.addExtra("name", "张三"); // 设置扩展信息 +// 保存 +SaApiKeyUtil.saveApiKey(akModel); +``` + +查询: + +``` java +// 获取 API Key 详细信息 +ApiKeyModel akModel = SaApiKeyUtil.getApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp"); + +// 直接获取 ApiKey 所代表的 loginId +Object loginId = SaApiKeyUtil.getLoginIdByApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp"); + +// 获取指定 loginId 的 ApiKey 列表记录 +List apiKeyList = SaApiKeyUtil.getApiKeyList(10001); +``` + + +### 4、校验 API Key + +``` java +// 校验指定 API Key 是否有效,无效会抛出异常 ApiKeyException +SaApiKeyUtil.checkApiKey("AK-XxxXxxXxx"); + +// 校验指定 API Key 是否具有指定 Scope 权限,不具有会抛出异常 ApiKeyScopeException +SaApiKeyUtil.checkApiKeyScope("AK-XxxXxxXxx", "userinfo"); + +// 校验指定 API Key 是否具有指定 Scope 权限,返回 true 或 false +SaApiKeyUtil.hasApiKeyScope("AK-XxxXxxXxx", "userinfo"); + +// 校验指定 API Key 是否属于指定账号 id +SaApiKeyUtil.checkApiKeyLoginId("AK-XxxXxxXxx", 10001); +``` + +注解鉴权示例: +``` java +/** + * API Key 资源 相关接口 + */ +@RestController +public class ApiKeyResourcesController { + + // 必须携带有效的 ApiKey 才能访问 + @SaCheckApiKey + @RequestMapping("/akRes1") + public SaResult akRes1() { + ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); + System.out.println("当前 ApiKey: " + akModel); + return SaResult.ok("调用成功"); + } + + // 必须携带有效的 ApiKey ,且具有 userinfo 权限 + @SaCheckApiKey(scope = "userinfo") + @RequestMapping("/akRes2") + public SaResult akRes2() { + ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); + System.out.println("当前 ApiKey: " + akModel); + return SaResult.ok("调用成功"); + } + + // 必须携带有效的 ApiKey ,且同时具有 userinfo、chat 权限 + @SaCheckApiKey(scope = {"userinfo", "chat"}) + @RequestMapping("/akRes3") + public SaResult akRes3() { + ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); + System.out.println("当前 ApiKey: " + akModel); + return SaResult.ok("调用成功"); + } + + // 必须携带有效的 ApiKey ,且具有 userinfo、chat 其中之一权限 + @SaCheckApiKey(scope = {"userinfo", "chat"}, mode = SaMode.OR) + @RequestMapping("/akRes4") + public SaResult akRes4() { + ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); + System.out.println("当前 ApiKey: " + akModel); + return SaResult.ok("调用成功"); + } + +} +``` + + +### 5、前端如何提交 API Key? +默认情况下,前端可以从任意途径提交 API Key 字符串,只要后端能接受到。 + +但是如果后端是通过 `SaApiKeyUtil.currentApiKey()` 方法获取,或者 `@SaCheckApiKey` 注解校验,则需要前端按照一定的格式来提交了: + +方式一:通过请求参数或请求头,参数名为 `apikey`(全小写) + +``` url +/user/getInfo?apikey=AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp +``` + + +方式二:通过 Basic 参数提交 + +``` url +http://AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp@localhost:8081/user/getInfo +``` + + + + + +### 6、打开数据库模式 + +框架默认将所有 API Key 信息保存在缓存中,这可以称之为“缓存模式”,这种模式下,重启缓存库后,数据将丢失。 + +如果你想改为“数据库模式”,可以通过 `implements SaApiKeyDataLoader` 实现从数据库加载的逻辑。 + +``` java +/** + * API Key 数据加载器实现类 (从数据库查询) + */ +@Component +public class SaApiKeyDataLoaderImpl implements SaApiKeyDataLoader { + + @Autowired + SaApiKeyMapper apiKeyMapper; + + // 指定框架不再维护 API Key 索引信息,而是由我们手动从数据库维护 + @Override + public Boolean getIsRecordIndex() { + return false; + } + + // 根据 apiKey 从数据库获取 ApiKeyModel 信息 (实现此方法无需为数据做缓存处理,框架内部已包含缓存逻辑) + @Override + public ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) { + return apiKeyMapper.getApiKeyModel(apiKey); + } + +} +``` + +参考上述代码实现后,框架内部逻辑将会做出一些改变,请注意以下事项: + +- 1、调用 `SaApiKeyUtil.getApiKey("ApiKey")` 时,会先从缓存中查询,查询不到时调用 `getApiKeyModelFromDatabase` 从数据库加载。 +- 2、框架不再维护 API Key 索引数据,这意味着无法再调用 `SaApiKeyUtil.getApiKeyList(10001)` 来获取一个用户的所有的 API Key 数据,请自行从数据库查询。 +- 3、调用 `SaApiKeyUtil.saveApiKey(akModel)` 保存时,只会把 API Key 数据保存到缓存中,请自行补充额外代码向数据库保存数据。 +- 4、调用 `SaApiKeyUtil.deleteApiKey("ApiKey")` 时,只会删除这个 API Key 在缓存中的数据,不会删除数据库的数据,请自行补充相关代码保证数据双删。 +- 5、其它诸如查询 `SaApiKeyUtil.getApiKey("ApiKey")` 或校验 `SaApiKeyUtil.checkApiKeyScope("ApiKey", "userinfo")` 等方法,依旧可以正常调用。 + + + +### 7、多账号模式使用 + +如果系统有多套账号表,比如 Admin 和 User,只需要指定不同的命名空间即可: + +例如 User 账号的 API Key,我们使用原生 `SaApiKeyUtil` 进行创建与校验。 + +对于 Admin 账号的 API Key,我们则新建一个 `SaApiKeyTemplate` 实例 + +``` java +// 新建 Admin 账号的 apiKeyTemplate 对象,命名空间为 "admin-apikey" +public static SaApiKeyTemplate adminApiKeyTemplate = new SaApiKeyTemplate("admin-apikey"); + +// 创建一个新的 ApiKey,并返回 +@RequestMapping("/createApiKey") +public SaResult createApiKey() { + ApiKeyModel akModel = adminApiKeyTemplate.createApiKeyModel(StpUtil.getLoginId()).setTitle("test"); + adminApiKeyTemplate.saveApiKey(akModel); + return SaResult.data(akModel); +} + +// ...校验、查询等操作,均使用新创建的 adminApiKeyTemplate,而非原生 `SaApiKeyUtil` +``` + + + + + + diff --git a/sa-token-doc/plugin/api-sign.md b/sa-token-doc/plugin/api-sign.md new file mode 100644 index 000000000..61e118db8 --- /dev/null +++ b/sa-token-doc/plugin/api-sign.md @@ -0,0 +1,601 @@ +# API 接口参数签名 + +

+ 观看本节视频讲解(B站:抓蛙师) +

+ +在涉及跨系统接口调用时,我们容易碰到以下安全问题: + +- 请求身份被伪造。 +- 请求参数被篡改。 +- 请求被抓包,然后重放攻击。 + +sa-token-sign 模块将帮你轻松解决以上难题。 + +本篇将根据假设的需求场景,循序渐进讲明白跨系统接口调用时必做的几个步骤,以及为什么要有这些步骤的原因。 + + +### 1、需求场景 + +假设我们有如下业务需求: + +> [!NOTE| label:业务场景] +> 用户在 A 系统参与活动成功后,活动奖励以余额的形式下发到 B 系统。 + + +### 2、初始方案:直接裸奔 + +在不考虑安全问题的情况下,我们很容易完成这个需求: + +1、在 B 系统开放一个接口。 + +``` java +/** + * 为指定用户添加指定余额 + * + * @param userId 用户 id + * @param money 要添加的余额,单位:分 + * @return / + */ +@RequestMapping("addMoney") +public SaResult addMoney(long userId, long money) { + // 处理业务 + // ... + + // 返回 + return SaResult.ok(); +} +``` + +2、在 A 系统使用 http 工具类调用这个接口。 + +``` java +long userId = 10001; +long money = 1000; +String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money); +``` + +上述代码简单的完成了需求,但是很明显它有一个安全问题: + +B 系统开放的接口不仅可以被 A 系统调用,还可以被其它任何人调用,甚至别人可以本地跑一个 for 循环调用这个接口,为自己无限充值金额。 + + +### 3、方案升级:增加 secretKey 校验 + +为防止 B 系统开放的接口被陌生人任意调用,我们增加一个 secretKey 参数 + +``` java +// 为指定用户添加指定余额 +@RequestMapping("addMoney") +public SaResult addMoney(long userId, long money, String secretKey) { + // 1、先校验 secretKey 参数是否正确,如果不正确直接拒绝响应请求 + if( ! check(secretKey) ) { + return SaResult.error("无效 secretKey,无法响应请求"); + } + + // 2、业务代码 + // ... + + // 3、返回 + return SaResult.ok(); +} +``` + +由于 A 系统是我们 “自己人”,所以它可以拿着 `secretKey` 进行合法请求: + +``` java +long userId = 10001; +long money = 1000; +String secretKey = "xxxxxxxxxxxxxxxxxxxx"; +String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "&secretKey=" + secretKey); +``` + +现在,即使 B 系统的接口被暴露了,也不会被陌生人任意调用了,安全性得到了一定的保证,但是仍然存在一些问题: + +- 如果请求被抓包,secretKey 就会泄露,因为每次请求都在 url 中明文传输了 secretKey 参数。 +- 如果请求被抓包,请求的其它参数就可以被任意修改,例如可以将 money 参数修改为 9999999,B系统无法确定参数是否被修改过。 + + + +### 4、方案再升级:使用摘要算法生成参数签名 + +首先,在 A 系统不要直接发起请求,而是先计算一个 sign 参数: + +``` java +// 声明变量 +long userId = 10001; +long money = 1000; +String secretKey = "xxxxxxxxxxxxxxxxxxxx"; + +// 计算 sign 参数 +String sign = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey); + +// 将 sign 拼接在请求地址后面 +String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "&sign=" + sign); +``` + +**注意此处计算签名时,需要将所有参数按照字典顺序依次排列(key除外,挂在最后面)。**以下所有计算签名时同理,不再赘述。 + +然后在 B 系统接收请求时,使用同样的算法、同样的秘钥,生成 sign 字符串,与参数中 sign 值进行比较: + + +``` java +// 为指定用户添加指定余额 +@RequestMapping("addMoney") +public SaResult addMoney(long userId, long money, String sign) { + + // 在 B 系统,使用同样的算法、同样的密钥,计算出 sign2,与传入的 sign 进行比对 + String sign2 = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey); + if( ! sign2.equals(sign)) { + return SaResult.error("无效 sign,无法响应请求"); + } + + // 2、业务代码 + // ... + + // 3、返回 + return SaResult.ok(); +} +``` + +因为 sign 的值是由 userId、money、secretKey 三个参数共同决定的,所以只要有一个参数不一致,就会造成最终生成 sign 也是不一致的,所以,根据比对结果: + +- 如果 sign 一致,说明这是个合法请求。 +- 如果 sign 不一致,说明发起请求的客户端秘钥不正确,或者请求参数被篡改过,是个不合法请求。 + +此方案优点: +- 不在 url 中直接传递 secretKey 参数了,避免了泄露风险。 +- 由于 sign 参数的限制,请求中的参数也不可被篡改,B 系统可放心的使用这些参数。 + +此方案仍然存在以下缺陷: +- 被抓包后,请求可以被无限重放,B 系统无法判断请求是真正来自于 A 系统发出的,还是被抓包后重放的。 + + + +### 5、方案再再升级:追加 nonce 随机字符串 + +首先,在 A 系统发起调用前,追加一个 nonce 参数,一起参与到签名中: + +``` java +// 声明变量 +long userId = 10001; +long money = 1000; +String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串 +String secretKey = "xxxxxxxxxxxxxxxxxxxx"; + +// 计算 sign 参数 +String sign = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); + +// 将 sign 拼接在请求地址后面 +String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "nonce=" + nonce + "&sign=" + sign); +``` + +然后在 B 系统接收请求时,也把 nonce 参数加进去生成 sign 字符串,进行比较: + +``` java +// 为指定用户添加指定余额 +@RequestMapping("addMoney") +public SaResult addMoney(long userId, long money, String nonce, String sign) { + + // 1、检查此 nonce 是否已被使用过了 + if(CacheUtil.get("nonce_" + nonce) != null) { + return SaResult.error("此 nonce 已被使用过了,请求无效"); + } + + // 2、验证签名 + String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); + if( ! sign2.equals(sign)) { + return SaResult.error("无效 sign,无法响应请求"); + } + + // 3、将 nonce 记入缓存,防止重复使用 + CacheUtil.set("nonce_" + nonce, "1"); + + // 4、业务代码 + // ... + + // 5、返回 + return SaResult.ok(); +} +``` + +代码分析: + +- 为方便理解,我们先看第 3 步:此处在校验签名成功后,将 nonce 随机字符串记入缓存中。 +- 再看第 1 步:每次请求进来,先查看一下缓存中是否已经记录了这个随机字符串,如果是,则立即返回:无效请求。 + +这两步的组合,保证了一个 nonce 随机字符串只能被使用一次,如果请求被抓包后重放,是无法通过 nonce 校验的。 + +至此,问题似乎已被解决了 …… 吗? + +别急,我们还有一个问题没有考虑:这个 nonce 在字符串在缓存应该被保存多久呢? + +- 保存 15 分钟?那抓包的人只需要等待 15 分钟,你的 nonce 记录在缓存中消失,请求就可以被重放了。 +- 那保存 24 小时?保存一周?保存半个月?好像无论保存多久,都无法从根本上解决这个问题。 + +你可能会想到,那我永久保存吧。这样确实能解决问题,但显然服务器承载不了这么做,即使再微小的数据量,在时间的累加下,也总一天会超出服务器能够承载的上限。 + + +### 6、方案再再再升级:追加 timestamp 时间戳 + +我们可以再追加一个 timestamp 时间戳参数,将请求的有效性限定在一个有限时间范围内,例如 15分钟。 + +首先,在 A 系统追加 timestamp 参数: + +``` java +// 声明变量 +long userId = 10001; +long money = 1000; +String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串 +long timestamp = System.currentTimeMillis(); // 系统当前时间戳 +String secretKey = "xxxxxxxxxxxxxxxxxxxx"; + +// 计算 sign 参数 +String sign = md5("money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey); + +// 将 sign 拼接在请求地址后面 +String res = HttpUtil.request("http://b.com/api/addMoney" + + "?userId=" + userId + "&money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&sign=" + sign); +``` + +在 B 系统检测这个 timestamp 是否超出了允许的范围 + +``` java +// 为指定用户添加指定余额 +@RequestMapping("addMoney") +public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) { + + // 1、检查 timestamp 是否超出允许的范围(此处假定最大允许15分钟差距) + long timestampDisparity = System.currentTimeMillis() - timestamp; // 实际的时间差 + if(timestampDisparity > 1000 * 60 * 15) { + return SaResult.error("timestamp 时间差超出允许的范围,请求无效"); + } + + // 2、检查此 nonce 是否已被使用过了 + // 代码同上,不再赘述 + + // 3、验证签名 + // 代码同上,不再赘述 + + // 4、将 nonce 记入缓存,ttl 有效期和 allowDisparity 允许时间差一致 + CacheUtil.set("nonce_" + nonce, "1", 1000 * 60 * 15); + + // 5、业务代码 ... + + // 6、返回 + return SaResult.ok(); +} +``` + +至此,抓包者: + +- 如果在 15 分钟内重放攻击,nonce 参数不答应:缓存中可以查出 nonce 值,直接拒绝响应请求。 +- 如果在 15 分钟后重放攻击,timestamp 参数不答应:超出了允许的 timestamp 时间差,直接拒绝响应请求。 + + +### 7、服务器的时钟差异造成安全问题 + +以上的代码,均假设 A 系统服务器与 B 系统服务器的时钟一致,才可以正常完成安全校验,但在实际的开发场景中,有些服务器会存在时钟不准确的问题。 + +假设 A 服务器与 B 服务器的时钟差异为 10 分钟,即:在 A 服务器为 8:00 的时候,B 服务器为 7:50。 + +1. A 系统发起请求,其生成的时间戳也是代表 8:00。 +2. B 系统接受到请求后,完成业务处理,此时 nonce 的 ttl 为 15分钟,到期时间为 7:50 + 15分 = 8:05。 +3. 8.05 后,nonce 缓存消失,抓包者重放请求攻击: + - timestamp 校验通过:因为时间戳差距仅有 8.05 - 8.00 = 5分钟,小于 15 分钟,校验通过。 + - nonce 校验通过:因为此时 nonce 缓存已经消失,可以通过校验。 + - sign 校验通过:因为这本来就是由 A 系统构建的一个合法签名。 + - 攻击完成。 + +要解决上述问题,有两种方案: +- 方案一:修改服务器时钟,使两个服务器时钟保持一致。 +- 方案二:在代码层面兼容时钟不一致的场景。 + +要采用方案一的同学可自行搜索一下同步时钟的方法,在此暂不赘述,此处详细阐述一下方案二。 + +我们只需简单修改一下,B 系统校验参数的代码即可: + +``` java +// 为指定用户添加指定余额 +@RequestMapping("addMoney") +public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) { + + // 1、检查 timestamp 是否超出允许的范围 (⚠️ 重点一:此处需要取绝对值) + long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp); + if(timestampDisparity > 1000 * 60 * 15) { + return SaResult.error("timestamp 时间差超出允许的范围,请求无效"); + } + + // 2、检查此 nonce 是否已被使用过了 + // 代码同上,不再赘述 + + // 3、验证签名 + // 代码同上,不再赘述 + + // 4、将 nonce 记入缓存,防止重复使用(⚠️ 重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) + CacheUtil.set("nonce_" + nonce, "1", (1000 * 60 * 15) * 2); + + // 5、业务代码 ... + + // 6、返回 + return SaResult.ok(); +} +``` + + +### 8、最终版方案 + +此处再贴一下完整的代码。 + +A 系统(发起请求端): + +``` java +// 声明变量 +long userId = 10001; +long money = 1000; +String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串 +long timestamp = System.currentTimeMillis(); // 当前时间戳 +String secretKey = "xxxxxxxxxxxxxxxxxxxx"; + +// 计算 sign 参数 +String sign = md5("money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey); + +// 将 sign 拼接在请求地址后面 +String res = HttpUtil.request("http://b.com/api/addMoney" + + "?userId=" + userId + "&money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&sign=" + sign); +``` + +B 系统(接收请求端): + +``` java +// 为指定用户添加指定余额 +@RequestMapping("addMoney") +public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) { + + // 1、检查 timestamp 是否超出允许的范围 + long allowDisparity = 1000 * 60 * 15; // 允许的时间差:15分钟 + long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp); // 实际的时间差 + if(timestampDisparity > allowDisparity) { + return SaResult.error("timestamp 时间差超出允许的范围,请求无效"); + } + + // 2、检查此 nonce 是否已被使用过了 + if(CacheUtil.get("nonce_" + nonce) != null) { + return SaResult.error("此 nonce 已被使用过了,请求无效"); + } + + // 3、验证签名 + String sign2 = md5("money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey); + if( ! sign2.equals(sign)) { + return SaResult.error("无效 sign,无法响应请求"); + } + + // 4、将 nonce 记入缓存,防止重复使用,注意此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 + CacheUtil.set("nonce_" + nonce, "1", allowDisparity * 2); + + // 5、业务代码 ... + + // 6、返回 + return SaResult.ok(); +} +``` + + +### 9、使用 Sa-Token 框架完成 API 参数签名 + +接下来步入正题,使用 sa-token-sign 模块,方便的完成 API 签名创建、校验等步骤: +- 不限制请求的参数数量,方便组织业务需求代码。 +- 自动补全 nonce、timestamp 参数,省时省力。 +- 自动构建签名,并序列化参数为字符串。 +- 一句代码完成 nonce、timestamp、sign 的校验,防伪造请求调用、防参数篡改、防重放攻击。 + + +#### 9.1、引入依赖 +请求发起端和接收端都需要引入: + + + + +``` xml + + + cn.dev33 + sa-token-sign + ${sa.top.version} + +``` + + + +``` gradle +// Sa-Token 整合 API 参数签名校验 +implementation 'cn.dev33:sa-token-sign:${sa.top.version}' +``` + + + +#### 9.2、配置秘钥 +请求发起端和接收端需要配置一个相同的秘钥,在 `application.yml` 中配置: + +``` yml +sa-token: + sign: + # API 接口签名秘钥 (随便乱摁几个字母即可) + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +#### 9.3、请求发起端构建签名 + +``` java +// 请求地址 +String url = "http://b.com/api/addMoney"; + +// 请求参数 +Map paramMap = new LinkedHashMap<>(); +paramMap.put("userId", 10001); +paramMap.put("money", 1000); +// 更多参数,不限制数量... + +// 补全 timestamp、nonce、sign 参数,并序列化为 kv 字符串 +String paramStr = SaSignUtil.addSignParamsAndJoin(paramMap); + +// 将参数字符串拼接在请求地址后面 +url += "?" + paramStr; + +// 发送请求 +String res = HttpUtil.request(url); + +// 根据返回值做后续处理 +System.out.println("server 端返回信息:" + res); +``` + + +#### 9.4、请求接受端校验签名 +``` java +// 为指定用户添加指定余额 +@RequestMapping("addMoney") +public SaResult addMoney(long userId, long money) { + + // 1、校验请求中的签名 + SaSignUtil.checkRequest(SaHolder.getRequest()); + + // 2、校验通过,处理业务 + System.out.println("userId=" + userId); + System.out.println("money=" + money); + + // 3、返回 + return SaResult.ok(); +} +``` + +如上代码便可简单方便的完成 API 接口参数签名校验,当请求端的秘钥不对,或者请求参数被篡改、请求被重放时,均无法通过 `SaSignUtil.checkRequest` 校验。 + +``` js +{ + "code": 500, + "msg": "无效签名:9c3e3e98c7d543fb599766c9d3f3b5ff", + "data": null +} +``` + + +### 10、使用注解校验签名 + +`@SaCheckSign` 注解用于为一个接口提供签名校验,用于替代 `SaSignUtil.checkRequest(SaHolder.getRequest())`,示例如下: + +``` java +// 校验全部参数:效果等同于 SaSignUtil.checkRequest(SaHolder.getRequest()) +@SaCheckSign +@RequestMapping("test1") +public SaResult test1() { + // code ... + return SaResult.ok(); +} + +// 指定参与签名的参数有哪些:效果等同于 SaSignUtil.checkRequest(SaHolder.getRequest(), "id", "name"); +@SaCheckSign(verifyParams = {"id", "name"}) +@RequestMapping("test2") +public SaResult test2() { + // code ... + return SaResult.ok(); +} + +// 指定: 在多应用模式下,使用的 appid,详情见下 +@SaCheckSign(appid = "xm-shop") +@RequestMapping("test3") +public SaResult test3() { + // code ... + return SaResult.ok(); +} +``` + +> 注:上述注解与 `SaSignUtil.checkRequest` 默认从 **Query / Form** 参数中取参验签。若业务使用 **JSON Body** 传参,请参考下文第 12 节。 + + +### 11、多应用模式 + +有时候我们可能需要同时与多个应用对接,每个应用都需要使用不同的秘钥: + +首先在配置文件配置多个应用信息: + + +``` yaml +sa-token: + # API 签名配置 多应用模式 + sign-many: + # 应用1 + xm-shop: + secret-key: 0123456789abcdefg + digest-algo: md5 + # 应用2 + xm-forum: + secret-key: 0123456789hijklmnopq + digest-algo: sha256 + # 应用3 + xm-video: + secret-key: 12341234aaaaccccdddd + digest-algo: sha512 +``` + +``` properties +# API 签名配置 多应用模式 +# 应用1 +sa-token.sign-many.xm-shop.secret-key=0123456789abcdefg +sa-token.sign-many.xm-shop.digest-algo=md5 +# 应用2 +sa-token.sign-many.xm-forum.secret-key=0123456789hijklmnopq +sa-token.sign-many.xm-forum.digest-algo=sha256 +# 应用3 +sa-token.sign-many.xm-video.secret-key=12341234aaaaccccdddd +sa-token.sign-many.xm-video.digest-algo=sha512 +``` + +``` java +@Autowired +public void configSaToken(SaTokenConfig config) { + // API 签名配置 多应用模式 + // 应用1 + config.getSignMany().put("xm-shop", new SaSignConfig() + .setSecretKey("0123456789abcdefg") // 秘钥 + .setDigestAlgo("md5") // 签名算法 + ); + // 应用2 + config.getSignMany().put("xm-forum", new SaSignConfig() + .setSecretKey("0123456789hijklmnopq") + .setDigestAlgo("sha256") + ); + // 应用3 + config.getSignMany().put("xm-video", new SaSignConfig() + .setSecretKey("12341234aaaaccccdddd") + // 自定义签名算法示例 + .setDigestMethod(fullStr -> { + return SaSecureUtil.sha384(fullStr); + }) + ); +} +``` + + + +然后在签名时通过指定 appid 的方式获取对应的 SignTemplate 进行操作: + +``` java +// 创建签名示例 +String paramStr = SaSignMany.getSignTemplate("xm-shop").addSignParamsAndJoin(paramMap); + +// 校验签名示例 +SaSignMany.getSignTemplate("xm-shop").checkRequest(SaHolder.getRequest()); +``` + + +### 12、JSON Body 验签参考(社区示例) + +官方默认的签名校验基于请求参数(Query / Form),无法直接读取 `Content-Type: application/json` 的请求体字段。 + +若你的接口使用 JSON Body 传参,可参考社区示例自行扩展(自定义 `@SaCheckSign` 处理器、缓存可重复读取的 Request Body 等): + +> [!TIP| label:社区示例 | style:callout] +> 仓库:[https://github.com/ly-chn/sa-token-check-sign-from-body](https://github.com/ly-chn/sa-token-check-sign-from-body) + + + diff --git a/sa-token-doc/plugin/custom-serializer.md b/sa-token-doc/plugin/custom-serializer.md new file mode 100644 index 000000000..697c5ed6d --- /dev/null +++ b/sa-token-doc/plugin/custom-serializer.md @@ -0,0 +1,105 @@ +# 序列化插件扩展包 +--- + +引入此插件可以为 Sa-Token 提供一些有意思的序列化方案。(娱乐向,不建议上生产) + + + +``` xml + + + cn.dev33 + sa-token-serializer-features + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 自定义 String 序列化方案合集 +implementation 'cn.dev33:sa-token-serializer-features:${sa.top.version}' +``` + + + +#### 1、SaSerializerForBase64UseTianGan +base64 编码,采用 十大天干、十二地支 等64个中文字符作为元字符集 + +``` java +// 设置序列化方案: base64 编码,采用 十大天干、十二地支 等64个中文字符作为元字符集 +@PostConstruct +public void rewriteComponent() { + SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan()); +} +``` + +效果图: + +sa-custom-serializer-tiangan.png + + +#### 2、SaSerializerForBase64UsePeriodicTable +base64 编码,采用 元素周期表 前六十四位作为元字符集 + +``` java +// 设置序列化方案: base64 编码,采用 元素周期表 前六十四位作为元字符集 +@PostConstruct +public void rewriteComponent() { + SaManager.setSaSerializerTemplate(new SaSerializerForBase64UsePeriodicTable()); +} +``` + +效果图: + +sa-custom-serializer-yszqb.png + + + +#### 3、SaSerializerForBase64UseSpecialSymbols +base64 编码,采用64个特殊符号作为元字符集 + +``` java +// 设置序列化方案: base64 编码,采用64个特殊符号作为元字符集 +@PostConstruct +public void rewriteComponent() { + SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseSpecialSymbols()); +} +``` + +效果图: + +sa-custom-serializer-tsfh.png + + +#### 4、SaSerializerForBase64UseEmoji +base64 编码,采用 64 个 Emoji 小黄脸作为元字符集,无填充字符 + +``` java +// 设置序列化方案: base64 编码,采用 64 个 Emoji 小黄脸作为元字符集,无填充字符 +@PostConstruct +public void rewriteComponent() { + SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseEmoji()); +} +``` + +效果图: + +sa-custom-serializer-emoji.png + +sa-custom-serializer-emoji2.png + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/plugin/dao-extend.md b/sa-token-doc/plugin/dao-extend.md new file mode 100644 index 000000000..d902c249e --- /dev/null +++ b/sa-token-doc/plugin/dao-extend.md @@ -0,0 +1,33 @@ +# 缓存层扩展 +--- + +对于权限框架来讲,最容易碰到的扩展点便是数据存储方式,为了方便对接不同的缓存中间件,Sa-Token将所有数据持久化操作抽象到SaTokenDao接口, +开发者要对接不同的平台只需要实现此接口即可,接口签名:[SaTokenDao.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java) + +框架已提供的集成包包括: + +- 默认方式:储存在内存中,位于core核心包。 +- sa-token-redis-template:Redis Template 集成包。 +- sa-token-redis-template-jdk-serializer:Redis 集成包,使用 jdk 默认序列化方式。 +- sa-token-hutool-timed-cache:集成 hutool 框架的 Timed-Cache 缓存方案(基于内存)。 +- sa-token-caffeine:集成 Caffeine 缓存方案(基于内存)。 +- sa-token-redisson:集成 Redisson 客户端。 +- sa-token-redisson-spring-boot-starter:集成 Redisson 客户端 - SpringBoot 自动配置包 。 +- sa-token-alone-redisson:独立 Redisson 连接,权限缓存与业务缓存分离。 +- sa-token-redisx:Redisx 集成包。 + +`SaTokenDaoForRedisson` 默认使用 `StringCodec`。从旧版本升级时,请先清空 Redis 中的 Sa-Token 缓存,或构造时传入 `Kryo5Codec` 以兼容旧数据,详见:[集成 Redis - 集成 Redisson](/up/integ-redis?id=_5集成-redisson)。 + + +有关 Redis 集成,详细参考:[集成Redis](/up/integ-redis),更多存储方式欢迎提交PR + + +**扩展:集成 MongoDB** + +- [集成 MongoDB 参考一](/up/integ-spring-mongod-1) +- [集成 MongoDB 参考二](/up/integ-spring-mongod-2) + + + + + diff --git a/sa-token-doc/plugin/dubbo-extend.md b/sa-token-doc/plugin/dubbo-extend.md new file mode 100644 index 000000000..b3709075a --- /dev/null +++ b/sa-token-doc/plugin/dubbo-extend.md @@ -0,0 +1,178 @@ +# 和 Dubbo 集成 + +本插件的作用是让 Sa-Token 和 Dubbo 做一个整合。 + +--- + +### 先说说要解决的问题 + +在 Dubbo 的整个调用链中,代码被分为 Consumer 端和 Provider 端,为方便理解我们可以称其为 `[调用端]` 和 `[被调用端]`。 + +RPC 模式的调用,可以让我们像调用本地方法一样完成服务通信,然而这种便利下却隐藏着两个问题: + +- 上下文环境的丢失。 +- 上下文参数的丢失。 + +这种问题作用在 Sa-Token 框架上就是,在 [ 被调用端 ] 调用 Sa-Token 相关API会抛出异常: **`无效上下文`** 。 + +所以本插件的目的也就是解决上述两个问题: + +- 在 [ 被调用端 ] 提供以 Dubbo 为基础的上下文环境 +- 在 RPC 调用时将 Token 传递至 [ 被调用端 ],同时在调用结束时将 Token 回传至 [ 调用端 ]。 + + +### 引入插件 + +在项目已经引入 Dubbo 的基础上,继续添加依赖(Consumer 端和 Provider 端都需要引入): + + + +``` xml + + + cn.dev33 + sa-token-dubbo + ${sa.top.version} + +``` +注:如果使用的是 dubbo3,只需要将 `sa-token-dubbo` 修改为 `sa-token-dubbo3` 即可。 + + +``` gradle +// Sa-Token 整合 Dubbo +implementation 'cn.dev33:sa-token-dubbo:${sa.top.version}' +``` +注:如果使用的是 dubbo3,只需要将 `sa-token-dubbo` 修改为 `sa-token-dubbo3` 即可。 + + + + + +然后我们就可以愉快的做到以下事情: + +1. 在 [ 被调用端 ] 安全的调用 Sa-Token 相关 API。 +2. 在 [ 调用端 ] 登录的会话,其登录状态可以自动传递到 [ 被调用端 ] 。 +3. 在 [ 被调用端 ] 登录的会话,其登录状态也会自动回传到 [ 调用端 ] 。 + +但是我们仍具有以下限制: + +1. [ 调用端 ] 与 [ 被调用端 ] 的 `SaStorage` 数据无法互通。 +2. [ 被调用端 ] 执行的 `SaResponse.setHeader()`、`setStatus()` 等代码无效。 + +应该合理避开以上 API 的使用。 + + +### RPC调用鉴权 + +在之前的 [Same-Token](/micro/same-token) 章节,我们演示了基于 Feign 的 RPC 调用鉴权,下面我们演示一下在 Dubbo 中如何集成 Same-Token 模块。 + +其实思路和 Feign 模式一致,在 [ 调用端 ] 追加 Same-Token 参数,在 [ 被调用端 ] 校验这个 Same-Token 参数: + +- 校验通过:调用成功。 +- 校验不通过:调用失败,抛出异常。 + +我们有两种方式完成整合。 + +##### 方式一、使用配置(推荐) + +直接在 `application.yml` 配置即可: + + + +``` yaml +sa-token: + # 打开 RPC 调用鉴权 + check-same-token: true +``` + +``` properties +# 打开 RPC 调用鉴权 +sa-token.check-same-token=true +``` + + + +##### 方式二、自建 Dubbo 过滤器校验 +此方式略显繁琐,好处是除了Same-Token,我们还可以添加其它自定义参数 (attachment)。 + +1、在 [ 调用端 ] 的 `\resources\META-INF\dubbo\` 目录新建 `org.apache.dubbo.rpc.Filter` 文件 +``` html +dubboConsumerFilter=com.pj.DubboConsumerFilter +``` + +新建 `DubboConsumerFilter.java` 过滤器 + +``` java +package com.pj; + +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; + +import cn.dev33.satoken.same.SaSameUtil; + +/** + * Sa-Token 整合 Dubbo Consumer端过滤器 + */ +@Activate(group = {CommonConstants.CONSUMER}, order = -10000) +public class DubboConsumerFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + + // 追加 Same-Token 参数 + RpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()); + + // 如果有其他自定义附加数据,如租户 + // RpcContext.getContext().setAttachment("tenantContext", tenantContext); + + // 开始调用 + return invoker.invoke(invocation); + } + +} +``` + + +2、在 [ 被调用端 ] 的 `\resources\META-INF\dubbo\` 目录新建 `org.apache.dubbo.rpc.Filter` 文件 +``` html +dubboProviderFilter=com.pj.DubboProviderFilter +``` + +新建 `DubboProviderFilter.java` 过滤器 + +``` java +package com.pj; + +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; + +import cn.dev33.satoken.same.SaSameUtil; + +/** + * Sa-Token 整合 Dubbo Provider端过滤器 + */ +@Activate(group = {CommonConstants.PROVIDER}, order = -10000) +public class DubboProviderFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + + // 取出 Same-Token 进行校验 + String sameToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN); + SaSameUtil.checkToken(sameToken); + + // 取出其他自定义附加数据 + // TenantContext tenantContext = invocation.getAttachment("tenantContext"); + + // 开始调用 + return invoker.invoke(invocation); + } + +} +``` + + +然后我们就可以进行安全的 RPC 调用了,不带有 Same-Token 参数的调用都会抛出异常,无法调用成功。 + diff --git a/sa-token-doc/plugin/freemarker-extend.md b/sa-token-doc/plugin/freemarker-extend.md new file mode 100644 index 000000000..a4b3e4767 --- /dev/null +++ b/sa-token-doc/plugin/freemarker-extend.md @@ -0,0 +1,130 @@ +# Freemarker 自定义标签 + +本插件的作用是让我们可以在 Freemarker 页面中使用 Sa-Token 自定义标签以及相关API。 + +--- + +### 1、引入依赖 +首先我们确保项目已经引入 Freemarker 依赖,然后在此基础上继续添加: + + + +``` xml + + + cn.dev33 + sa-token-freemarker + ${sa.top.version} + +``` + +``` gradle +// 在 Freemarker 页面中使用 Sa-Token 自定义标签 +implementation 'cn.dev33:sa-token-freemarker:${sa.top.version}' +``` + + + + +### 2、注入 Sa-Token Freemarker 标签模板模型 对象 +在 SaTokenConfigure 配置类中增加配置 +``` java +@Configuration +public class SaTokenConfigure { + + @Autowired + FreeMarkerConfigurer configurer; + + /** + * 注入 Sa-Token Freemarker 标签模板模型 对象 + */ + @PostConstruct + public void setSaTokenTemplateModel() throws TemplateModelException { + + // 注入 Sa-Token Freemarker 标签模板模型,使之可以在 xxx.ftl 文件中使用 sa 标签, + // 例如:<#if sa.login()>... + configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel()); + + // 注入 Sa-Token Freemarker 全局对象,使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法, + // 例如:${stp.getSession().get('name')} + configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic); + } + +} +``` + + +### 3、使用自定义标签 +然后我们就可以愉快的使用在 Freemarker 页面中使用 Sa-Token 自定义标签了 + +##### 3.1、登录判断 +``` html +

标签方言测试页面

+

+ 登录之后才能显示: + <@sa.login>value +

+

+ 不登录才能显示: + <@sa.notLogin>value +

+``` + +##### 3.2、角色判断 +``` html +

+ 具有角色 admin 才能显示: + <@sa.hasRole value="admin">value +

+

+ 同时具备多个角色才能显示: + <@sa.hasRoleAnd value="admin, ceo, cto">value +

+

+ 只要具有其中一个角色就能显示: + <@sa.hasRoleOr value="admin, ceo, cto">value +

+

+ 不具有角色 admin 才能显示: + <@sa.notRole value="admin">value +

+``` + +##### 3.3、权限判断 +``` html +

+ 具有权限 user-add 才能显示: + <@sa.hasPermission value="user-add">value +

+

+ 同时具备多个权限才能显示: + <@sa.hasPermissionAnd value="user-add, user-delete, user-get">value +

+

+ 只要具有其中一个权限就能显示: + <@sa.hasPermissionOr value="user-add, user-delete, user-get">value +

+

+ 不具有权限 user-add 才能显示: + <@sa.notPermission value="user-add">value +

+``` + + +### 4、调用 Sa-Token 相关API + +以上的自定义标签,可以满足我们大多数场景下的权限判断,然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API : + + +``` html +

+ 从SaSession中取值: + <#if stp.isLogin()> + ${stp.getSession().get('name')} + +

+``` + + + + diff --git a/sa-token-doc/plugin/grpc-extend.md b/sa-token-doc/plugin/grpc-extend.md new file mode 100644 index 000000000..eaaa39927 --- /dev/null +++ b/sa-token-doc/plugin/grpc-extend.md @@ -0,0 +1,56 @@ +# 和 grpc 集成 + +本插件的作用是让 Sa-Token 和 grpc 做一个整合。 + +--- + +和dubbo插件一样,解决了以下问题 + +1. 在 [ 被调用端 ] 安全的调用 Sa-Token 相关 API。 +2. 在 [ 调用端 ] 登录的会话,其登录状态可以自动传递到 [ 被调用端 ] ;在 [ 被调用端 ] 登录的会话,其登录状态可以自动回传到 [ 调用端 ] +3. Same-Token 安全校验 + +--- +和dubbo插件一样,具有以下限制: + +1. [ 调用端 ] 与 [ 被调用端 ] 的 `SaStorage` 数据无法互通。 +2. [ 被调用端 ] 执行的 `SaResponse.setHeader()`、`setStatus()` 等代码无效。 + +### 引入插件 +需要springboot环境,添加依赖(调用端和被调用端都需要引入): + + + +``` xml + + + cn.dev33 + sa-token-grpc + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 整合 grpc +implementation 'cn.dev33:sa-token-grpc:${sa.top.version}' +``` + + + +--- +### 开启 Same-Token 校验: +直接在 `application.yml` 配置即可: + + + +``` yaml +sa-token: + # 打开 RPC 调用鉴权 + check-same-token: true +``` + +``` properties +# 打开 RPC 调用鉴权 +sa-token.check-same-token=true +``` + \ No newline at end of file diff --git a/sa-token-doc/plugin/json-extend.md b/sa-token-doc/plugin/json-extend.md new file mode 100644 index 000000000..ade2b6dd5 --- /dev/null +++ b/sa-token-doc/plugin/json-extend.md @@ -0,0 +1,91 @@ +# JSON 序列化扩展 + +--- + +Sa-Token 在 Session 存储、Redis 缓存等场景下需要对对象进行 JSON 序列化与反序列化。框架将 JSON 转换逻辑抽象到 `SaJsonTemplate` 接口, +开发者只需引入对应的 JSON 插件依赖,框架会通过 SPI 机制自动完成注入,接口签名:[SaJsonTemplate.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java) + +框架已提供的 JSON 序列化插件包括: + +- **sa-token-jackson**:集成 Jackson(com.fasterxml.jackson),适用于 SpringBoot2/3 等环境。 +- **sa-token-jackson3**:集成 Jackson 3(tools.jackson.core),适用于 SpringBoot4、Java 17+ 等环境。 +- **sa-token-fastjson**:集成 Fastjson。 +- **sa-token-fastjson2**:集成 Fastjson2。 +- **sa-token-snack3**:集成 Snack3。 +- **sa-token-snack4**:集成 Snack4。 + +> 若使用 `sa-token-spring-boot-starter` 集成包(含 SpringBoot3),框架会自动引入 Jackson 作为默认 JSON 方案,一般无需额外配置。如需更换为其它 JSON 框架,引入对应插件依赖即可。 + + + +``` xml + + + cn.dev33 + sa-token-jackson + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-jackson:${sa.top.version}'` + + +``` xml + + + cn.dev33 + sa-token-jackson3 + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-jackson3:${sa.top.version}'` + + +``` xml + + + cn.dev33 + sa-token-fastjson + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-fastjson:${sa.top.version}'` + + +``` xml + + + cn.dev33 + sa-token-fastjson2 + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-fastjson2:${sa.top.version}'` + + +``` xml + + + cn.dev33 + sa-token-snack3 + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-snack3:${sa.top.version}'` + + +``` xml + + + cn.dev33 + sa-token-snack4 + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-snack4:${sa.top.version}'` + + + + +有关 Redis 集成与序列化配置,详细参考:[集成 Redis](/up/integ-redis) + +更多自定义序列化方案(如 Base64、天干地支等),可参考:[序列化插件扩展包](/plugin/custom-serializer) diff --git a/sa-token-doc/plugin/jwt-extend.md b/sa-token-doc/plugin/jwt-extend.md new file mode 100644 index 000000000..181367dd7 --- /dev/null +++ b/sa-token-doc/plugin/jwt-extend.md @@ -0,0 +1,247 @@ +# 和 jwt 集成 + +本插件的作用是让 Sa-Token 和 jwt 做一个整合。 + +--- + +### 1、引入依赖 +首先在项目已经引入 Sa-Token 的基础上,继续添加: + + + +``` xml + + + cn.dev33 + sa-token-jwt + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 整合 jwt +implementation 'cn.dev33:sa-token-jwt:${sa.top.version}' +``` + + + +> [!WARNING| label:版本兼容性] +> 1. 注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本,保险起见:你的项目中要么不引入 hutool,要么引入版本 >= 5.7.14 的 hutool 版本。 +> 2. hutool 5.8.13 和 5.8.14 版本下会出现类型转换问题,[关联issue](https://gitee.com/dromara/sa-token/issues/I6L429)。 + + +### 2、配置秘钥 +在 `application.yml` 配置文件中配置 jwt 生成秘钥: + + + +``` yaml +sa-token: + # jwt秘钥 + jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk +``` + +``` properties +# jwt秘钥 +sa-token.jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk +``` + + +注:为了安全起见请不要直接复制官网示例这个字符串(随便按几个字符就好了) + + +### 3、注入jwt实现 +根据不同的整合规则,插件提供了三种不同的模式,你需要 **选择其中一种** 注入到你的项目中 + + + + +Simple 模式:Token 风格替换 +``` java +@Configuration +public class SaTokenConfigure { + // Sa-Token 整合 jwt (Simple 简单模式) + @Bean + public StpLogic getStpLogicJwt() { + return new StpLogicJwtForSimple(); + } +} +``` + + +Mixin 模式:混入部分逻辑 +``` java +@Configuration +public class SaTokenConfigure { + // Sa-Token 整合 jwt (Mixin 混入模式) + @Bean + public StpLogic getStpLogicJwt() { + return new StpLogicJwtForMixin(); + } +} +``` + + +Stateless 模式:服务器完全无状态 +``` java +@Configuration +public class SaTokenConfigure { + // Sa-Token 整合 jwt (Stateless 无状态模式) + @Bean + public StpLogic getStpLogicJwt() { + return new StpLogicJwtForStateless(); + } +} +``` + + + +### 4、开始使用 +然后我们就可以像之前一样使用 Sa-Token 了 +``` java +/** + * 登录测试 + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 + @RequestMapping("login") + public SaResult login() { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + + // 查询登录状态 + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 测试注销 + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} +``` + +访问上述接口,观察Token生成的样式 +``` java +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbklkIjoiMTAwMDEiLCJybiI6IjZYYzgySzBHVWV3Uk5NTTl1dFdjbnpFZFZHTVNYd3JOIn0.F_7fbHsFsDZmckHlGDaBuwDotZwAjZ0HB14DRujQfOQ +``` + + +### 5、不同模式策略对比 + +注入不同模式会让框架具有不同的行为策略,以下是三种模式的差异点(为方便叙述,以下比较以同时引入 jwt 与 Redis 作为前提): + +| 功能点 | Simple 简单模式 | Mixin 混入模式 | Stateless 无状态模式 | +| :-------- | :-------- | :-------- | :-------- | +| Token风格 | jwt风格 | jwt风格 | jwt风格 | +| 登录数据存储 | Redis中存储 | Token中存储 | Token中存储 | +| Session存储 | Redis中存储 | Redis中存储 | 无Session | +| 注销下线 | 前后端双清数据 | 前后端双清数据 | 前端清除数据 | +| 踢人下线API | 支持 | 不支持 | 不支持 | +| 顶人下线API | 支持 | 不支持 | 不支持 | +| 登录认证 | 支持 | 支持 | 支持 | +| 角色认证 | 支持 | 支持 | 支持 | +| 权限认证 | 支持 | 支持 | 支持 | +| timeout 有效期 | 支持 | 支持 | 支持 | +| active-timeout 有效期 | 支持 | 支持 | 不支持 | +| id反查Token | 支持 | 支持 | 不支持 | +| 会话管理 | 支持 | 部分支持 | 不支持 | +| 注解鉴权 | 支持 | 支持 | 支持 | +| 路由拦截鉴权 | 支持 | 支持 | 支持 | +| 账号封禁 | 支持 | 支持 | 不支持 | +| 身份切换 | 支持 | 支持 | 支持 | +| 二级认证 | 支持 | 支持 | 支持 | +| 模式总结 | Token风格替换 | jwt 与 Redis 逻辑混合 | 完全舍弃Redis,只用jwt | + + + +### 6、扩展参数 +你可以通过以下方式在登录时注入扩展参数: + +``` java +// 登录10001账号,并为生成的 Token 追加扩展参数name +StpUtil.login(10001, new SaLoginParameter().setExtra("name", "zhangsan")); + +// 连缀写法追加多个 +StpUtil.login(10001, new SaLoginParameter() + .setExtra("name", "zhangsan") + .setExtra("age", 18) + .setExtra("role", "超级管理员")); + +// 获取扩展参数 +String name = StpUtil.getExtra("name"); + +// 获取任意 Token 的扩展参数 +String name = StpUtil.getExtra("tokenValue", "name"); +``` + + + +### 7、在多账户模式中集成 jwt +sa-token-jwt 插件默认只为 `StpUtil` 注入 `StpLogicJwtFoxXxx` 实现,自定义的 `StpUserUtil` 是不会自动注入的,我们需要帮其手动注入: + +``` java +/** + * 为 StpUserUtil 注入 StpLogicJwt 实现 + */ +@PostConstruct +public void setUserStpLogic() { + StpUserUtil.setStpLogic(new StpLogicJwtForSimple(StpUserUtil.TYPE)); +} +``` + + + +### 8、自定义 SaJwtUtil 生成 token 的算法 + +如果需要自定义生成 token 的算法(例如更换sign方式),直接重写 SaJwtTemplate 对象即可: + +``` java +/** + * 自定义 SaJwtUtil 生成 token 的算法 + */ +@PostConstruct +public void setSaJwtTemplate() { + SaJwtUtil.setSaJwtTemplate(new SaJwtTemplate() { + @Override + public String generateToken(JWT jwt, String keyt) { + System.out.println("------ 自定义了 token 生成算法"); + return super.generateToken(jwt, keyt); + } + }); +} +``` + + +### 9、注意点 + +##### 1、使用 jwt-simple 模式后,is-share=false 恒等于 false。 + +`is-share=true` 的意思是每次登录都产生一样的 token,这种策略和 [ 为每个 token 单独设定 setExtra 数据 ] 不兼容的, +为保证正确设定 Extra 数据,当使用 `jwt-simple` 模式后,`is-share` 配置项 恒等于 `false`。 + + +##### 2、使用 jwt-mixin 模式后,is-concurrent 必须为 true。 + +`is-concurrent=false` 代表每次登录都把旧登录顶下线,但是 jwt-mixin 模式登录的 token 并不会记录在持久库数据中, +技术上来讲无法将其踢下线,所以此时顶人下线和踢人下线等 API 都属于不可用状态,所以此时 `is-concurrent` 配置项必须配置为 `true`。 + + +##### 3、使用 jwt-mixin 模式后,max-try-times 恒等于 -1。 + +为防止框架错误判断 token 唯一性,当使用 jwt-mixin 模式后,`max-try-times` 恒等于 -1。 + + + + + + + diff --git a/sa-token-doc/plugin/plugin-dev.md b/sa-token-doc/plugin/plugin-dev.md new file mode 100644 index 000000000..84ff063df --- /dev/null +++ b/sa-token-doc/plugin/plugin-dev.md @@ -0,0 +1,9 @@ +# Sa-Token 插件开发指南 + +本插件的作用是让 Sa-Token 和 Dubbo 做一个整合。 + +--- + + + + diff --git a/sa-token-doc/doc/plugin/quick-login.md b/sa-token-doc/plugin/quick-login.md similarity index 75% rename from sa-token-doc/doc/plugin/quick-login.md rename to sa-token-doc/plugin/quick-login.md index 0997e8142..11ef50da3 100644 --- a/sa-token-doc/doc/plugin/quick-login.md +++ b/sa-token-doc/plugin/quick-login.md @@ -44,14 +44,33 @@ Sa-Token-Quick-Login的定位是这样的场景:你的项目需要一个登录 首先我们需要创建一个SpringBoot的demo项目,比如:`sa-token-demo-quick-login` ##### 1、添加pom依赖 + + + ``` xml - + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + + cn.dev33 sa-token-quick-login ${sa.top.version} ``` + +``` gradle +// Sa-Token 启动依赖 +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' +// Sa-Token-Quick-Login 插件 +implementation 'cn.dev33:sa-token-quick-login:${sa.top.version}' +``` + + + ##### 2、启动类 ``` java @@ -89,16 +108,26 @@ public class TestController { ### 测试访问 启动项目,使用浏览器访问:`http://localhost:8081`,首次访问时,由于处于未登录状态,会被强制进入登录页面 -![登录](https://oss.dev33.cn/sa-token/doc/sa-quick-login.png 's-w') +登录 使用默认账号:`sa / 123456`进行登录,会看到资源页面 -![登录](https://oss.dev33.cn/sa-token/doc/sa-quick-login-index.png 's-w') +登录 + +也可以通过 Http Basic 的方式直接进行认证 (一般需要在专门的 API 测试工具下才能正常测试,浏览器会自动忽略@之前的信息) + +``` url +http://sa:123456@localhost:8081/ +``` ### 可配置信息 你可以在yml中添加如下配置 (所有配置都是可选的) -``` java + + + + +``` yaml # Sa-Token-Quick-Login 配置 sa: # 登录账号 @@ -116,12 +145,36 @@ sa: # 指定拦截路径 # include: /** # 指定排除路径 - # exclude: /sss,/fff + # exclude: /1.jpg +``` + + +``` properties +####### Sa-Token-Quick-Login 配置 ####### +# 登录账号 +sa.name=sa +# 登录密码 +sa.pwd=123456 +# 是否自动随机生成账号密码 (此项为true时, name与pwd失效) +sa.auto=false +# 是否开启全局认证(关闭后将不再强行拦截) +sa.auth=true +# 登录页标题 +sa.title=Sa-Token 登录 +# 是否显示底部版权信息 +sa.copr=true +# 指定拦截路径 +# sa.include=/** +# 指定排除路径 +# sa.exclude=/1.jpg ``` + + +
-**注:**示例源码在`/sa-token-demo/sa-token-demo-quick-login`目录下,可结合源码查看学习 +**注:**示例源码在`/sa-token-demo/sa-token-demo-quick-login`目录下,可结合源码查看学习。[Sa-Token 集成示例大全下载](/more/download-demos) 。 @@ -133,7 +186,9 @@ sa: ##### 打包步骤 -首先放上懒人链接:[sa-quick-dist.jar](https://gitee.com/dromara/sa-token/attach_files/695353/download) ,不想手动操作的同学可以直接点此链接下载打包后的jar文件 + + +首先放上懒人链接:[sa-quick-dist.jar](https://pan.quark.cn/s/04fd34a24928),不想手动操作的同学可以直接点此链接下载打包后的jar文件 1、首先将 `sa-token-demo-quick-login` 模块添加到顶级父模块的``节点中 @@ -167,7 +222,7 @@ java -jar sa-quick-dist.jar 5、测试访问,根据控制台输出提示,我们使用浏览器访问测试: `http://localhost:8080` -![sa-quick-start](https://oss.dev33.cn/sa-token/doc/sa-quick-start.png 's-w') +sa-quick-start 如果可以进入登录界面,则代表打包运行成功
当然仅仅运行成功还不够,下面我们演示一下如何使用这个jar包进行静态网站部署 @@ -217,7 +272,7 @@ SpringBoot默认开放了一些路径作为资源目录,比如`classpath:/stat 同时,我们还可以在jar包同目录创建yml配置文件,来覆盖jar包内的yml配置,如下图所示: -![sa-quick-case.png](https://oss.dev33.cn/sa-token/doc/sa-quick-case.png 's-w') +sa-quick-case.png 例如如上目录中`/static`中有一个`1.jpg`文件,我们启动jar包后访问`http://localhost:8080/1.jpg`即可查看到此文件,这是Springboot自带的功能,在此不再赘述 diff --git a/sa-token-doc/plugin/spel-at.md b/sa-token-doc/plugin/spel-at.md new file mode 100644 index 000000000..743cb2d6f --- /dev/null +++ b/sa-token-doc/plugin/spel-at.md @@ -0,0 +1,148 @@ +# SpEL 表达式注解鉴权 + +Sa-Token 提供一个 `@SaCheckEL` 鉴权注解,该注解允许你使用 SpEL 表达式进行鉴权。 + + +### 1、引入插件 + +由于该注解的工作底层需要依赖 SpringAOP 切面编程,因此你需要单独引入插件包 `sa-token-spring-el` 才可以使用此注解。 + + + +``` xml + + + cn.dev33 + sa-token-spring-el + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 注解鉴权使用 EL 表达式 +implementation 'cn.dev33:sa-token-spring-el:${sa.top.version}' +``` + + + +### 2、简单示例 + +以下是一些使用示例: +``` java +@RestController +@RequestMapping("/check-el/") +public class SaCheckELController { + + // 登录校验 + @SaCheckEL("stp.checkLogin()") + @RequestMapping("test1") + public SaResult test1() { + return SaResult.ok(); + } + + // 权限校验 + @SaCheckEL("stp.checkPermission('user:edit')") + @RequestMapping("test3") + public SaResult test3() { + return SaResult.ok(); + } + + // 参数长度校验 + @SaCheckEL("NEED( #name.length() > 3 )") + @RequestMapping("test5") + public SaResult test5(@RequestParam(defaultValue = "") String name) { + return SaResult.ok().set("name", name); + } + + // SaSession 里取值校验 + @SaCheckEL("NEED( stp.getSession().get('name') == 'zhangsan' )") + @RequestMapping("test8") + public SaResult test8() { + return SaResult.ok(); + } + +} +``` + + +### 3、多账号体系鉴权 + +要在 EL 表达式中使用多账号体系鉴权模式,你需要在配置类中重写 `SaCheckELRootMap 扩展函数`,增加 EL 表达式可使用的根对象: + +``` java +@Configuration +public class SaTokenConfigure { + + /** + * 重写 Sa-Token 框架内部算法策略 + */ + @PostConstruct + public void rewriteSaStrategy() { + // 重写 SaCheckELRootMap 扩展函数,增加注解鉴权 EL 表达式可使用的根对象 + SaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> { + System.out.println("--------- 执行 SaCheckELRootMap 增强,目前已包含的的跟对象包括:" + rootMap.keySet()); + // 新增 stpUser 根对象,使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权 + rootMap.put("stpUser", StpUserUtil.getStpLogic()); + }; + } + +} +``` + +然后就可以使用多账号体系鉴权模式了 + +``` java +// 多账号体系鉴权测试 +@SaCheckEL("stpUser.checkLogin()") +@RequestMapping("test9") +public SaResult test9() { + return SaResult.ok(); +} +``` + + +### 4、调用本类成员变量 +``` java +// 本模块需要鉴权的权限码 +public String permissionCode = "article:add"; + +// 调用本类的成员变量 +@SaCheckEL("stp.checkPermission( this.permissionCode )") +@RequestMapping("test10") +public SaResult test10() { + return SaResult.ok(); +} +``` + + +### 5、忽略鉴权 +配合 `@SaIgnore` 注解做到忽略某接口的鉴权 +``` java +// 忽略鉴权测试 +@SaIgnore +@SaCheckEL("stp.checkPermission( 'abc' )") +@RequestMapping("test11") +public SaResult test11() { + return SaResult.ok(); +} +``` + + +### 6、代码提示 + +如果在书写 SpEL 表达式时需要代码提示: + +sa-check-el-code-tips.png + +可以在 idea 中安装 **SpEL Assistant** 插件,该插件由 `@ly-chn` 提供,允许为自定义注解书写 SpEL 表达式时增加代码提示功能, +开源地址:[https://github.com/ly-chn/SpEL-Assistant](https://github.com/ly-chn/SpEL-Assistant) + +安装方式:直接在 idea 插件商店中搜索 “**SpEL Assistant**” 即可 + +sa-check-el-code-tips.png + + + + 本章代码示例:Sa-Token SpEL表达式注解鉴权 —— [ SaCheckELController.java ] + \ No newline at end of file diff --git a/sa-token-doc/plugin/temp-token.md b/sa-token-doc/plugin/temp-token.md new file mode 100644 index 000000000..4b43ee778 --- /dev/null +++ b/sa-token-doc/plugin/temp-token.md @@ -0,0 +1,120 @@ +# 临时 Token 令牌认证 + +--- + +### 1、适用场景 + +在部分业务场景,我们需要一种临时授权的能力,即:一个token的有效期并不需要像登录有效期那样需要[七天、三十天],而是仅仅需要 [五分钟、半小时]。 + +举个比较明显的例子:超链接邀请机制。 + +> [!NOTE| label:业务场景] +> +> 你在一个游戏中创建一个公会 `(id=10014)`,现在你想邀请你的好朋友加入这个公会,在你点击 **`[邀请]`** 按钮时,系统为你生成一个连接: +> +> ``` xml +> http://xxx.com/apply?id=10014 +> ``` +> +> 接着,你的好朋友点击这个链接,加入了你的工会。 +> +> 那么,系统是如何识别这个链接对应的工会是10014呢?很明显,我们可以观察出,这个链接的尾部有个id参数值为10014,这便是系统识别的关键。 +> +> 此时你可能眉头一紧,就这么简单?那我如果手动更改一下尾部的参数改成10015,然后我再一点,岂不是就可以偷偷加入别人的工会了? +> +> 你想的没错,如果这个游戏的架构设计者采用上述方案完成功能的话,这个邀请机制就轻松的被你攻破了。 +> +> 但是很明显,正常的商业项目一般不会拉跨到这种地步,比较常见的方案是,对这个公会id做一个token映射,最终你看到链接一般是这样的: +> +> ``` xml +> http://xxx.com/apply?token=oEwQBnglXDoGraSJdGaLooPZnGrk +> ``` +> +> 后面那一串字母是乱打出来的,目的是为了突出它的随机性,即:使用一个随机的token来代替明文显示真正的数据。 +> +> 在用户点击这个链接之后,服务器便可根据这个token解析出真正公会id (10014) ,至于伪造?全是随机的你怎么伪造?你又不知道10015会随机出一个什么样的Token 。 +> +> 而且为了安全性,这个token的有效期一般不会太长,给你预留五分钟、半小时的时间足够你点击它即可。 + + +### 2、创建临时 token + +**[sa-token-temp 临时 token 认证模块]** 已内嵌到核心包,无需引入其它依赖即可使用: + +``` java +// 根据 value 创建一个 token +String token = SaTempUtil.createToken("10014", 200); + +// 解析 token 获取 value,并转换为指定类型 +String value = SaTempUtil.parseToken(token, String.class); + +// 获取指定 token 的剩余有效期,单位:秒 +SaTempUtil.getTimeout(token); + +// 删除指定 token +SaTempUtil.deleteToken(token); +``` + + +### 3、前缀拼接与裁剪 + +``` java +// 如果由多条业务线都需要生成临时 token,可以加个前缀进行区分 +String token = SaTempUtil.createToken("shop_1001", 1200); +``` + +在获取时可以自行裁剪前缀,也可以调用: +``` java +// 解析 token 获取 value,并裁剪指定前缀,然后转换为指定类型 +SaTempUtil.parseToken(token, "shop_", Long.class) +``` + +如果指定了错误的前缀,即使 token 正确,上述方法也将返回 null + + +### 4、根据 value 反查 token + +在创建 token 时,框架默认只会保存 `token -> value` 的映射,而不会记录 `value -> token` 的索引信息。 + +如果想要做到反查 token,则必须在创建 token 指定框架记录 token 索引信息: + +``` java +// 在创建 token 时,指定第三个参数 true,即可让框架在保存 token 时同时记录 token 索引信息 +String token1 = SaTempUtil.createToken(10004, 1200, true); +String token2 = SaTempUtil.createToken(10004, 1300, true); +String token3 = SaTempUtil.createToken(10004, -1, true); + +// 获取 10004 对应的所有 token +List list = SaTempUtil.getTempTokenList(10004); +System.out.println(list); +``` + + + +### 5、集成jwt +提到 [临时Token认证],你是不是想到一个专门干这件事的框架?对,就是JWT! + +**[sa-token-temp]** 模块允许以JWT作为逻辑内核完成工作,你只需要引入以下依赖,所有上层API保持不变 + + + +``` xml + + cn.dev33 + sa-token-temp-jwt + ${sa.top.version} + +``` + +``` gradle +implementation 'cn.dev33:sa-token-temp-jwt:${sa.top.version}' +``` + + + +并在配置文件中配置上jwt秘钥 **`(必填!)`** +``` yml +sa-token: + # sa-token-temp-jwt 模块的秘钥 (随便乱摁几个字母就行了) + jwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk +``` diff --git a/sa-token-doc/plugin/thymeleaf-extend.md b/sa-token-doc/plugin/thymeleaf-extend.md new file mode 100644 index 000000000..a6de17e35 --- /dev/null +++ b/sa-token-doc/plugin/thymeleaf-extend.md @@ -0,0 +1,155 @@ +# Thymeleaf 标签方言 + +本插件的作用是让我们可以在 Thymeleaf 页面中使用 Sa-Token 相关API,俗称 —— 标签方言。 + +--- + +### 1、引入依赖 +首先我们确保项目已经引入 Thymeleaf 依赖,然后在此基础上继续添加: + + + +``` xml + + + cn.dev33 + sa-token-thymeleaf + ${sa.top.version} + +``` + +``` gradle +// 在 thymeleaf 标签中使用 Sa-Token +implementation 'cn.dev33:sa-token-thymeleaf:${sa.top.version}' +``` + + + +### 2、注册标签方言对象 +在 SaTokenConfigure 配置类中注册 Bean +``` java +@Configuration +public class SaTokenConfigure { + // Sa-Token 标签方言 (Thymeleaf版) + @Bean + public SaTokenDialect getSaTokenDialect() { + return new SaTokenDialect(); + } +} +``` + + +### 3、使用标签方言 +然后我们就可以愉快的使用在 Thymeleaf 页面中使用标签方言了 + +##### 3.1、登录判断 +``` html +

标签方言测试页面

+

+ 登录之后才能显示: + value +

+

+ 不登录才能显示: + value +

+``` + +##### 3.2、角色判断 +``` html +

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

+

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

+

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

+

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

+``` + +##### 3.3、权限判断 +``` html +

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

+

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

+

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

+

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

+``` + + +### 4、调用 Sa-Token 相关API + +以上的标签方言,可以满足我们大多数场景下的权限判断,然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API + +首先在 SaTokenConfigure 配置类中为 Thymeleaf 配置全局对象: + +``` java +@Configuration +public class SaTokenConfigure{ + // ... 其它代码 + + // 为 Thymeleaf 注入全局变量,以便在页面中调用 Sa-Token 的方法 + @Autowired + private void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) { + viewResolver.addStaticVariable("stp", StpUtil.stpLogic); + } +} +``` + +> [!WARNING| label:注意] +> 如果`SaTokenConfigure`继承了`WebMvcConfigurer`等类,可能会造成循环依赖,如果遇到,请新建一个其他配置类完成此项配置 + + +然后我们就可以在页面上调用 StpLogic 的 API 了,例如: + +``` html +

调用 StpLogic 方法调用测试

+

+ 从SaSession中取值: + +

+``` + + +### 5、代码提示 + +如果想在写标签属性时增加代码提示: + +thymeleaf-code-tips.png + +只需在头部声明增加上对应的命名空间即可: + +``` html + + + + + + + + + +``` + + + + diff --git a/sa-token-doc/pro/st_doc_top.md b/sa-token-doc/pro/st_doc_top.md new file mode 100644 index 000000000..77b1306f5 --- /dev/null +++ b/sa-token-doc/pro/st_doc_top.md @@ -0,0 +1,55 @@ +# Sa-Max 统一认证商业版 + +### 项目介绍 + +根据 SSO / OAuth2 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个 SSO / OAuth2 模式的认证 Demo 示例。 + +然而,要真正开发一个商业级项目的统一认证中心系统,绝非一朝一夕可以搭建完毕,为此我们特意准备了项目: +[[ Sa-Max 统一认证商业版 ]](https://sa-max.cn?way=st_doc_top)。 + +项目基于 Sa-Token 搭建,集成了统一认证常见技术点,可大大缩短您的项目接入统一授权认证的开发周期: +- 支持:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 +- 支持:API Key、OAuth2.0 统一认证能力, 并提供开放平台,供第三方公司申请应用对接平台能力。 + +全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。 + + + + + +### 释疑 + +##### 1、Sa-Max 是收费项目吗?与 Sa-Token 有什么不同? + +`Sa-Max` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 + +`Sa-Max` 与 `Sa-Token` 的区别,简单来讲: +- `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 +- `Sa-Max` 是一个完整项目,下载源码后可直接启动 + + +##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? +首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 + +其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, +比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 + +Sa-Max 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 + +另外:即使您没有购买 `Sa-Max`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 + + + +##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! + +答案是不会。 + +再次强调一下:`Sa-Token` 与 `Sa-Max` 是两个独立的项目,两者互不影响。 +付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 + +实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 + + + + + diff --git a/sa-token-doc/pro/st_index_top.md b/sa-token-doc/pro/st_index_top.md new file mode 100644 index 000000000..33f2f0b5b --- /dev/null +++ b/sa-token-doc/pro/st_index_top.md @@ -0,0 +1,55 @@ +# Sa-Max 统一认证商业版 + +### 项目介绍 + +根据 SSO / OAuth2 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个 SSO / OAuth2 模式的认证 Demo 示例。 + +然而,要真正开发一个商业级项目的统一认证中心系统,绝非一朝一夕可以搭建完毕,为此我们特意准备了项目: +[[ Sa-Max 统一认证商业版 ]](https://sa-max.cn?way=st_index_top)。 + +项目基于 Sa-Token 搭建,集成了统一认证常见技术点,可大大缩短您的项目接入统一授权认证的开发周期: +- 支持:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 +- 支持:API Key、OAuth2.0 统一认证能力, 并提供开放平台,供第三方公司申请应用对接平台能力。 + +全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。 + + + + + +### 释疑 + +##### 1、Sa-Max 是收费项目吗?与 Sa-Token 有什么不同? + +`Sa-Max` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 + +`Sa-Max` 与 `Sa-Token` 的区别,简单来讲: +- `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 +- `Sa-Max` 是一个完整项目,下载源码后可直接启动 + + +##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? +首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 + +其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, +比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 + +Sa-Max 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 + +另外:即使您没有购买 `Sa-Max`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 + + + +##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! + +答案是不会。 + +再次强调一下:`Sa-Token` 与 `Sa-Max` 是两个独立的项目,两者互不影响。 +付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 + +实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 + + + + + diff --git a/sa-token-doc/pro/st_oauth2.md b/sa-token-doc/pro/st_oauth2.md new file mode 100644 index 000000000..ad7df71e5 --- /dev/null +++ b/sa-token-doc/pro/st_oauth2.md @@ -0,0 +1,55 @@ +# Sa-Max 统一认证商业版 + +### 项目介绍 + +根据 OAuth2 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个 OAuth2 模式的认证 Demo 示例。 + +然而,要真正开发一个商业级项目的统一认证中心系统,绝非一朝一夕可以搭建完毕,为此我们特意准备了项目: +[[ Sa-Max 统一认证商业版 ]](https://sa-max.cn?way=st_oauth2)。 + +项目基于 Sa-Token 搭建,集成了统一认证常见技术点,可大大缩短您的项目接入统一授权认证的开发周期: +- 支持:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 +- 支持:API Key、OAuth2.0 统一认证能力, 并提供开放平台,供第三方公司申请应用对接平台能力。 + +全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。 + + + + + +### 释疑 + +##### 1、Sa-Max 是收费项目吗?与 Sa-Token 有什么不同? + +`Sa-Max` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 + +`Sa-Max` 与 `Sa-Token` 的区别,简单来讲: +- `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 +- `Sa-Max` 是一个完整项目,下载源码后可直接启动 + + +##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? +首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 + +其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, +比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 + +Sa-Max 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 + +另外:即使您没有购买 `Sa-Max`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 + + + +##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! + +答案是不会。 + +再次强调一下:`Sa-Token` 与 `Sa-Max` 是两个独立的项目,两者互不影响。 +付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 + +实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 + + + + + diff --git a/sa-token-doc/pro/st_sso.md b/sa-token-doc/pro/st_sso.md new file mode 100644 index 000000000..5ac37fd56 --- /dev/null +++ b/sa-token-doc/pro/st_sso.md @@ -0,0 +1,55 @@ +# Sa-Pro 单点登录商业版 + +### 项目介绍 + +根据 SSO 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个SSO模式的认证 Demo 示例。 + +然而,要真正开发一个商业级项目的统一认证中心系统,绝非一朝一夕可以搭建完毕,为此,我们特意准备了项目: +[[ Sa-Pro 单点登录商业版 ]](https://sa-max.cn?way=st_sso)。 + +项目基于 Sa-Token 搭建,集成了单点登录常见技术点,可解决: +同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 +可大大缩短您的项目接入单点登录的开发周期。 + +全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。 + + + + + +### 释疑 + +##### 1、Sa-Pro 是收费项目吗?与 Sa-Token 有什么不同? + +`Sa-Pro` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 + +`Sa-Pro` 与 `Sa-Token` 的区别,简单来讲: +- `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 +- `Sa-Pro` 是一个完整项目,下载源码后可直接启动 + + +##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? +首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 + +其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, +比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 + +Sa-Pro 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 + +另外:即使您没有购买 `Sa-Pro`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 + + + +##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Pro 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! + +答案是不会。 + +再次强调一下:`Sa-Token` 与 `Sa-Pro` 是两个独立的项目,两者互不影响。 +付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 + +实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 + + + + + diff --git a/sa-token-doc/robots.txt b/sa-token-doc/robots.txt new file mode 100644 index 000000000..833be1030 --- /dev/null +++ b/sa-token-doc/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://sa-token.com/sitemap.xml diff --git a/sa-token-doc/sitemap-pages.xml b/sa-token-doc/sitemap-pages.xml new file mode 100644 index 000000000..71d5bc68b --- /dev/null +++ b/sa-token-doc/sitemap-pages.xml @@ -0,0 +1,15 @@ + + + + https://sa-token.com/ + 2026-08-12 + + + https://sa-token.com/doc.html + 2026-08-12 + + + https://sa-token.com/blog/index.html + 2026-08-12 + + diff --git a/sa-token-doc/sitemap.xml b/sa-token-doc/sitemap.xml new file mode 100644 index 000000000..d4cd8e587 --- /dev/null +++ b/sa-token-doc/sitemap.xml @@ -0,0 +1,11 @@ + + + + https://sa-token.com/sitemap-pages.xml + 2026-08-12 + + + https://sa-token.com/blog/sitemap.xml + 2026-08-12 + + diff --git a/sa-token-doc/sso/anon-client.md b/sa-token-doc/sso/anon-client.md new file mode 100644 index 000000000..1212717d9 --- /dev/null +++ b/sa-token-doc/sso/anon-client.md @@ -0,0 +1,108 @@ +# 匿名 Client 接入 + +匿名 Client 就是指在客户端没有配置 `sso-client` 的应用,没有一个明确的 “Client” 标识名称。 + +匿名 Client 在一些关键步骤中不会构建 `client` 参数,如:“重定向至认证中心授权地址”、“校验 ticket”、“单点注销” 等。 + +要想匿名 client 接入,你需要做一些特殊配置。 + + +### 1、在 sso-server 端开启匿名 client 接入 + +开启方式一,通过配置项方式: + + + +``` yaml +# Sa-Token 配置 +sa-token: + # SSO-Server 配置 + sso-server: + # 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) + allow-anon-client: true + # 所有允许的授权回调地址 (匿名 client 使用) + allow-url: "*" + # API 接口调用秘钥 (全局默认 + 匿名 client 使用) + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +``` properties +# SSO-Server 配置 +# 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) +sa-token.sso-server.allow-anon-client=true +# 所有允许的授权回调地址 (匿名 client 使用) +sa-token.sso-server.allow-url=* +# API 接口调用秘钥 (全局默认 + 匿名 client 使用) +sa-token.sso-server.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + + +开启方式二,通过代码重写方式: + +``` java +/** + * 重写 SaSsoServerTemplate 部分方法,增强功能 + */ +@Component +public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { + + /** + * 获取配置项:是否允许匿名 client 接入 + */ + @Override + public boolean getConfigOfAllowAnonClient() { + return true; + } + + /** + * 获取匿名 client 配置信息 + */ + @Override + public SaSsoClientModel getAnonClient() { + SaSsoClientModel scm = new SaSsoClientModel(); + scm.setAllowUrl("*"); // 允许的授权地址 + scm.setIsSlo(true); // 是否允许单点注销 + scm.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); // 客户端密钥 + return scm; + } +} +``` + + +### 2、在 sso-client 端不要配置 client 字段 + +然后在对应的应用端不要配置 client 字段,例如: + +``` yml +# sa-token配置 +sa-token: + # 配置一个不同的 token-name,以避免在和模式三 demo 一起测试时发生数据覆盖 + token-name: satoken-client-anon + # sso-client 相关配置 + sso-client: + # client 标识 匿名应用就是指不配置 client 标识的应用 + # client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 使用 Http 请求校验ticket (模式三) + is-http: true + # 是否在登录时注册单点登录回调接口 (匿名应用想要参与单点注销必须打开这个) + reg-logout-call: true + # API 接口调用秘钥 + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + +> [!TIP| label:demo] +> 匿名 Client 接入的 Demo 示例地址:[sa-token-demo-sso3-client-anon](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon) + +这里有个值得注意的配置项:`reg-logout-call: true`,是干嘛的? + +简单来讲,就是匿名应用不包含 client 字段信息,因此 sso-server 端也无法配置此 client 的消息推送地址,所以此 client 无法接受到消息推送,也就无法参与到单点注销的环路中来。 + +因此,新增一个配置项 `reg-logout-call: true`,代表在登录的同时把当前项目的单点注销回调地址 `/sso/logoutCall` 发送到 sso-server 端, +这样 sso-server 端有了备案,也就可以成功通知此应用发起单点注销掉了。 + +如果当前应用不需要单点注销可以不配置此字段。 + diff --git a/sa-token-doc/sso/message-push.md b/sa-token-doc/sso/message-push.md new file mode 100644 index 000000000..0ad8dbbf8 --- /dev/null +++ b/sa-token-doc/sso/message-push.md @@ -0,0 +1,209 @@ +# 消息推送机制 + +消息推送机制简单来讲就是:sso-client 端按照特点格式构建一个 http 请求,调用 sso-server 端的 `/sso/pushS` 接口,sso-server 接收到消息后做出处理回应 sso-client 端。 + +消息推送是相互的,sso-server 端也可以构建 http 请求,调用 sso-client 端的 `/sso/pushC` 接口。 + +消息推送机制是应用与认证中心相互沟通的桥梁,ticket 校验、单点注销等行为都是依赖消息推送机制来实现的。 + +本篇将介绍在 Sa-Token SSO 模块中,sso-server 端和 sso-client 端分别内置了哪些消息模块,以及如何自定义消息处理器。 + + +### 1、sso-server 端内置消息处理器 + +#### 1.1、checkTicket(ticket 校验) + +作用:在 SSO 模式三下为 sso-client 提供 ticket 校验能力,返回 loginId 等数据 + +``` url +http://{sso-server主机地址}/sso/pushS +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| msgType | 是 | 消息类型,此处填 `checkTicket` | +| ticket | 是 | ticket 码 | +| client | 否 | 客户端标识,可不填,代表是一个匿名应用 | +| ssoLogoutCall | 否 | Client 端单点注销时 - 回调 URL 参数名称 (匿名 Client 时使用) | +| timestamp | 是 | 当前时间戳,13位 | +| nonce | 是 | 随机字符串 | +| sign | 是 | 签名,生成算法示例:`md5( client={client值}&msgType={checkTicket}&nonce={随机字符串}&ticket={ticket码}×tamp={13位时间戳}&key={secretkey秘钥} )` | + +**签名算法规则:将所有参数按照字典顺序依次排列(key除外,挂在最后面),然后进行 md5 摘要。以下不再赘述。** + +返回值示例: +``` js +{ + "code": 200, // 返回 200=成功,500=失败 + "msg": "ok", + "data": "10001", + "loginId": "10001", // 此 ticket 对应的认证中心 loginId + "tokenValue": "5db12b02-9c8e-4e36-8ed9-bf295caed80e", // 对应的认证中心会话 token + "deviceId": "MxOTCLWi5NXGqFQZBFdsH66Ni5YTJ8q0", // 对应的认证中心登录设备 id + "remainTokenTimeout": 2591999, // token 剩余有效期 + "remainSessionTimeout": 2591999 // Access-Session 会话剩余有效期 +} +``` + + +#### 1.2、signout(单点注销) + +作用:为 sso-client 提供单点注销能力 + +``` url +http://{sso-server主机地址}/sso/pushS +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| msgType | 是 | 消息类型,此处填 `signout` | +| loginId | 是 | 账号id | +| client | 否 | 客户端标识,可不填,代表是一个匿名应用 | +| deviceId | 否 | 客户端设备 id | +| timestamp | 是 | 当前时间戳,13位 | +| nonce | 是 | 随机字符串 | +| sign | 是 | 签名,生成算法示例:`md5( client={client值}&deviceId={设备id}&msgType={signout}&nonce={随机字符串}&loginId={loginId}×tamp={13位时间戳}&key={secretkey秘钥} )` | + +返回值示例: +``` js +{ + "code": 200, // 返回 200=成功,500=失败 + "msg": "ok", + "data": null +} +``` + + +### 2、sso-client 端内置消息处理器 + +#### 2.1、logoutCall(单点注销回调) + +作用:接收来自 sso-server 的单点注销回调通知 + +``` url +http://{sso-client主机地址}/sso/pushC +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| msgType | 是 | 消息类型,此处填 `logoutCall` | +| loginId | 是 | 账号id | +| deviceId | 否 | 客户端设备 id | +| timestamp | 是 | 当前时间戳,13位 | +| nonce | 是 | 随机字符串 | +| sign | 是 | 签名,生成算法示例:`md5( deviceId={设备id}&msgType={logoutCall}&nonce={随机字符串}&loginId={loginId}×tamp={13位时间戳}&key={secretkey秘钥} )` | + +返回值示例: +``` js +{ + "code": 200, // 返回 200=成功,500=失败 + "msg": "单点注销回调成功", + "data": null +} +``` + + +### 3、认证中心自定义消息处理器 + +当然你也可以通过自定义消息处理器的方式,来扩展消息推送能力,这将非常有助于你完成一些应用与认证中心的自定义数据交互。 + +假设我们现在有如下需求:在 sso-client 获取 sso-server 端指定账号 id 的昵称、头像等信息,即:用户资料的拉取。 + +首先,我们需要在 sso-server 实现一个消息处理器: + +``` java +@RestController +public class SsoServerController { + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoServerTemplate ssoServerTemplate) { + + // 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口) + ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> { + System.out.println("收到消息:" + message); + + // 自定义返回结果(模拟) + return SaResult.ok() + .set("id", message.get("loginId")) + .set("name", "LinXiaoYu") + .set("sex", "女") + .set("age", 18); + }); + + } + +} +``` + + +### 4、应用端调用消息推送接口获取数据 + +首先保证在配置文件里要配置上消息推送的具体地址 + + + +``` yaml +# sa-token配置 +sa-token: + # sso-client 相关配置 + sso-client: + # 应用标识 + client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # sso-server 端推送消息地址 + # 配置 server-url 后,框架可自动计算对应的 push-url 地址,也可以单独配置 push-url 地址,两者选其一即可 + # push-url: http://sa-sso-server.com:9000/sso/pushS + # API 接口调用秘钥 + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +``` properties +# sso-client 相关配置 +# 应用标识 +sa-token.sso-client.client=sso-client3 +# sso-server 端主机地址 +sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# sso-server 端推送消息地址 +# 配置 server-url 后,框架可自动计算对应的 push-url 地址,也可以单独配置 push-url 地址,两者选其一即可 +sa-token.sso-client.push-url=http://sa-sso-server.com:9000/sso/pushS +# API 接口调用秘钥 +sa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + + +然后在需要拉取资料的地方: + +``` java +// 查询我的账号信息:sso-client 前端 -> sso-client 后端 -> sso-server 后端 +@RequestMapping("/sso/myInfo") +public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 获取本地 loginId + Object loginId = StpUtil.getLoginId(); + + // 构建消息对象 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", loginId); + + // 推送至 sso-server,并接收响应数据 + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; +} +``` + diff --git a/sa-token-doc/sso/readme.md b/sa-token-doc/sso/readme.md new file mode 100644 index 000000000..bc9f59dd4 --- /dev/null +++ b/sa-token-doc/sso/readme.md @@ -0,0 +1,54 @@ +# Sa-Token-SSO 单点登录模块 + +

+ 观看 SSO 模块视频讲解(B站:王清江唷) +

+ +凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录——便是我们搭建统一认证中心的关键。 + +--- + +### 什么是单点登录?解决什么问题? + +举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, +为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统。 + +单点登录——就是为了解决这个问题而生! + +简而言之,单点登录可以做到: **`在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。`** + + +### 架构选型 +Sa-Token-SSO 由简入难划分为三种模式,解决不同架构下的 SSO 接入问题: + +| 系统架构 | 采用模式 | 简介 | 文档链接 | +| :-------- | :-------- | :-------- | :-------- | +| 前端同域 + 后端同 Redis | 模式一 | 共享 Cookie 同步会话 | [文档](/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client) | +| 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [文档](/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client) | +| 前端不同域 + 后端不同 Redis | 模式三 | Http请求获取会话 | [文档](/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client) | + +集成示例还可下载全套 Demo 压缩包:[Sa-Token 集成示例大全下载](/more/download-demos) 。 + +1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com`。 +2. 后端同Redis:就是指多个系统可以连接同一个Redis。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案,详情戳: Alone独立Redis插件。 +3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成)。 + +sa-token-jss + + +### Sa-Token-SSO 特性 +1. API 简单易用,文档介绍详细,且提供直接可用的集成示例。 +2. 支持三种模式,不论是否跨域、是否共享Redis、是否前后端分离,都可以完美解决。 +3. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝`Ticket劫持`、`Token窃取`等常见攻击手段(文档讲述攻击原理和防御手段)。 +4. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:`http://a.com?id=1&name=2`,登录成功之后就变成了:`http://a.com?id=1`,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面。 +5. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合…… +6. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发。 + + +### 学习注意点 +1. sa-token-sso 虽然是个单独的插件,但其本质仍是对 Sa-Token 本身各个功能的组合使用,所以先熟练掌握 Sa-Token 可有效降低 SSO 章节的学习压力。 +2. 相比单体系统的会话管理,SSO 登录与注销的整体链路较长,出现 bug 时调试步骤也更复杂,因此建议先通过 demo 打通各个技术细节,再正式集成到项目中。 +3. 文档对 跨Redis、跨域、前后端分离 等常见场景提供直接可用的示例,但真实项目往往是多种特殊场景交叉组合存在,每个项目各不相同。 +所以文档无法依次列出所有技术点交叉组合的 demo 示例,文档会更注重解释清楚每一种特殊场景的特殊点所在,以及其解决原理, +所以推荐大家细心阅读相关段落,以便在真实项目中可以做到灵活组合、举一反三。 + diff --git a/sa-token-doc/sso/signout.md b/sa-token-doc/sso/signout.md new file mode 100644 index 000000000..fb9a1b108 --- /dev/null +++ b/sa-token-doc/sso/signout.md @@ -0,0 +1,158 @@ +# 单点注销 + +Sa-Token SSO 提供多种注销模式: + +从注销范围上可以分为: + +- 单端注销:会话只在当前应用注销,其它应用和认证中心不受影响。 +- 全端注销:一处注销,全端下线。也即:单点注销。 +- 单浏览器注销:该账号的只在当前浏览器登录的应用注销,其它浏览器/设备不受影响。 + +从注销方式上可以分为: +- ajax 无刷单点注销:调用指定的 RestAPI 接口完成注销。 +- 跳页面注销:跳转到指定接口进行注销,注销完成后原路返回或跳转到指定页面。 + +--- + +### 1、单端注销 + +在后端添加接口: +``` java +// 当前应用独自注销 (不退出其它应用) +@RequestMapping("/sso/logoutByAlone") +public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); +} +``` + +在前端或跳转或 ajax 异步调用此接口即可。 + +如果是跳转可指定 back 参数,代表注销成功后跳转的地址,例如:`http://sso-client.com/sso/logoutByAlone?back=https://sa-token.com` + + +### 2、全端注销 + +此处先简单看一下 Sa-Token SSO 的单点注销链路过程: + +1. sso-client 的前端向 sso-client 的后端发起单点注销请求。(调用 `http://{sso-client}/sso/logout`) +2. sso-client 的后端向 sso-server 的后端发送单点注销请求。(调用 `http://{sso-server}/sso/pushS?msgType=signout`) +3. sso-server 端遍历 client 列表,逐个推送消息通知 sso-client 端下线。(`http://{sso-client}/sso/pushC?msgType=logoutCall`) +4. sso-server 端注销下线。 +5. sso-server 后端响应 sso-client 后端:注销完成。 +6. sso-client 后端响应 sso-client 前端:注销完成。 +7. 整体完成。 + + + + + +这些逻辑 Sa-Token 内部已经封装完毕,你只需按照文档步骤集成即可。以模式三 demo 为例: + +#### 2.1、更改注销方案 + +单点注销是 Sa-Token SSO 内部已封装的接口,无需手动再添加,只需要在前端调用即可。 + +``` java +// SSO-Client端:首页 +@RequestMapping("/") +public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "全端注销 " + + "

"; + return str; +} +``` + +重点在第 9 行。 + +#### 2.2、启动测试 +重启项目,依次登录三个 client: +- [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/) +- [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/) +- [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/) + +sso-type3-client-index.png + +在任意一个 client 里,点击 **`[注销]`** 按钮,即可单点注销成功(打开另外两个client,刷新一下页面,登录态丢失)。 + + + +sso-type3-slo-index.png + +PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用 Ajax 调用接口即可做到无刷单点登录退出。 + +例如,我们使用 [Apifox 接口测试工具](https://www.apifox.cn/) 可以做到同样的效果: + +sso-slo-apifox.png + + + +### 3、单浏览器注销 + +单浏览器注销的前提是在登录时按照 `deviceId` 设备ID 参数为登录进行分组,这样在发起注销时即可格局设备ID参数做到单浏览器注销功能。 + +#### 3.1、sso-server 端加上设备ID参数登录 + +首先在 sso-server 的登录方法内,加上 deviceId 参数,例如: + +``` java +@RestController +public class SsoServerController { + + // 其它代码,非重点,省略展示... + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoServerTemplate ssoServerTemplate) { + // 配置:登录处理函数 + ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { + // 此处仅做模拟登录,真实环境应该查询数据库进行登录 + if("sa".equals(name) && "123456".equals(pwd)) { + String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32)); + StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId)); + return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); + } + return SaResult.error("登录失败!"); + }; + + } +} +``` + +如上代码,在登录时获取前端提交的 deviceId 参数,如果前端没有提交则随机生成一个。 + + +#### 3.2、sso-client 端发起注销时指定单设备注销参数 + +然后在 sso-client 发起单点注销时,加上 `singleDeviceIdLogout=true` 参数,代表按照设备 id 进行分组注销,非本设备id的会话不参与注销行为: + +``` java +// SSO-Client端:首页 +@RequestMapping("/") +public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "单浏览器注销 - " + + "全端注销 " + + "

"; + return str; +} +``` + +重点在第 9 行。 + + +> [!WARNING| label:测试注意点] +> 在进行测试时,同时将一个浏览器双击打开两次,是不算 “不同浏览器” 的,虽然你打开了两个浏览器窗口,但是这两个浏览器的会话数据是互通的。 +> +> 必须打开两个不同的浏览器来测试,或者按快捷键 `ctrl + shift + N` 打开隐私模式,才可以做到会话相互隔离。 + + diff --git a/sa-token-doc/sso/sso-apidoc.md b/sa-token-doc/sso/sso-apidoc.md new file mode 100644 index 000000000..a6274c8bd --- /dev/null +++ b/sa-token-doc/sso/sso-apidoc.md @@ -0,0 +1,273 @@ +# SSO-Server 认证中心开放接口 + +--- + +## 一、对接方式说明 + +在前一章节,我们成功搭建了 SSO-Server 端。SSO-Server 可以为各个子系统提供中央认证服务。 + +在默认代码架构下,SSO-Server 将提供一套标准 HTTP 接口对外开放。要对接 SSO-Server,有两种方式: +- SDK 方式:在你的 SSO-Client 端,也引入 Sa-Token SSO 框架,通过框架提供的 API 方法完成对接。 + +- NoSDK 方式:在你的 SSO-Client 端,不引入 Sa-Token SSO 框架,通过工具库调用 Http 接口的方式完成对接。 + + +如果你的 SSO-Client 端是 java 项目,且支持引入 sa-token-sso 框架,我们强烈推荐你使用 SDK 方式对接。**你可以直接跳过本章**,开始 [SSO模式一 共享Cookie同步会话](/sso/sso-type1) 的学习。 + +如果你的 SSO-Client 端是 非java 项目,或不支持引入 sa-token-sso 框架,那么你可以使用 NoSDK 方式进行对接。 +在之后的 [SSO整合 - NoSdk 模式对接](/sso/sso-nosdk) 章节我们会详细介绍对接步骤,下面的 API 文档将给你的对接步骤做一份参考。 + + +## 二、STS 协议 + +Sa-Token SSO 模块的开放接口标准为 STS 协议。STS 协议的设计充分参考了 CAS、Spring Authorization Server、KeyClock、OAuth2 协议等经典框架/ 协议的授权流程。 +定义了应用进行 单点登录、单点注销、消息推送 等动作的标准动作。 +可解决:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、Sa-Token 项目、非 Sa-Token 项目、java 项目、非 java 项目 等架构下的 SSO 认证需求。 + +下面两节将介绍 STS 协议在 SSO-Server 端和 SSO-Client 端开放的接口标准。 + + + +## 三、SSO-Server 认证中心接口 + + +### 1、单点登录授权地址 +``` url +http://{host}:{port}/sso/auth +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| redirect | 否 | 登录成功后的重定向地址,一般填写 location.href(从哪来回哪去),如不填,则跳转至 home-route | +| mode | 否 | 授权模式,取值 [simple, ticket],simple=登录后直接重定向,ticket=带着ticket参数重定向,默认值为ticket | +| client | 否 | 客户端标识,可不填,代表是一个匿名应用,若填写了,则校验 ticket 时也必须是这个 client 才可以校验成功 | + +访问接口后有两种情况: +- 情况一:当前会话在 SSO 认证中心未登录,会进入登录页开始登录。 +- 情况二:当前会话在 SSO 认证中心已登录,会被重定向至 `redirect` 地址,并携带 `ticket` 参数。 + +Ticket 码具有以下特点: +1. 每次授权产生的 `ticket` 码都不一样。 +2. `ticket` 码用完即废,不能二次使用。 +3. 一个 `ticket` 的有效期默认为五分钟,超时自动作废。 +4. 每次授权产生新 `ticket` 码,会导致旧 `ticket` 码立即作废,即使旧 `ticket` 码尚未使用。 + + +### 2、RestAPI 登录接口 +``` url +http://{host}:{port}/sso/doLogin +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| name | 是 | 用户名 | +| pwd | 是 | 密码 | + +此接口属于 RestAPI (使用ajax访问),会进入后端配置的 `ssoServerTemplate.strategy.doLoginHandle` 函数中,此函数的返回值即是此接口的响应值。 + +另外需要注意:此接口并非只能携带 name、pwd 参数,因为你可以在方法里通过 `SaHolder.getRequest().getParam("xxx")` 来获取前端提交的其它参数。 + + +### 3、单点注销接口 +``` url +http://{host}:{port}/sso/signout +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| back | 否 | 注销成功后的重定向地址,一般填写 location.href(从哪来回哪去),也可以填写 self 字符串,含义同上 | + + +### 4、消息推送接口 + +``` url +http://{host}:{port}/sso/pushS +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| client | 否 | 客户端标识,可不填,代表是一个匿名应用 | +| timestamp | 是 | 当前时间戳,13位 | +| msgType | 是 | 消息类型 | +| nonce | 是 | 随机字符串 | +| sign | 是 | 签名,生成算法:`md5( client={client值}&msgType={消息类型}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | + +此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。 + +返回值示例: + +- 推送成功时: + +``` js +{ + "code": 200, + "msg": "ok", + "data": "10001", // 返回的数据 +} +``` + +- 推送失败时: + +``` js +{ + "code": 500, // 200表示请求成功,非200标识请求失败 + "msg": "签名无效:xxx", // 失败原因 + "data": null +} +``` + +- 也有可能消息推送成功了,但是处理消息失败,例如校验 ticket 时: + +``` js +{ + "code": 500, + "msg": "无效ticket:vESj0MtqrtSoucz4DDHJnsqU3u7AKFzbj0KH57EfJvuhkX1uAH23DuNrMYSjTnEq", + "data": null +} +``` + +详细可参考:[消息推送机制](/sso/message-push) + + + +
+ + + + + +--- + +## 四、SSO-Client 应用端开放接口 + +### 1、登录地址 +``` url +http://{host}:{port}/sso/login +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| back | 是 | 登录成功后的重定向地址,一般填写 location.href(从哪来回哪去) | +| ticket | 否 | 授权 ticket 码 | + +此接口有两种访问方式: +- 方式一:我们需要登录操作,所以带着 back 参数主动访问此接口,框架会拼接好参数后再次将用户重定向至认证中心。 +- 方式二:用户在认证中心登录成功后,带着 ticket 参数重定向而来,此为框架自动处理的逻辑,开发者无需关心。 + + +### 2、注销地址 +``` url +http://{host}:{port}/sso/logout +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| back | 否 | 注销成功后的重定向地址,一般填写 location.href(从哪来回哪去),也可以填写 self 字符串,含义同上 | + +此接口有两种访问方式: +- 方式一:直接 `location.href` 网页跳转,此时可携带 back 参数。 +- 方式二:使用 Ajax 异步调用(此方式不可携带 back 参数,但是需要提交会话 Token ),注销成功将返回以下内容: + +``` js +{ + "code": 200, // 200表示请求成功,非200标识请求失败 + "msg": "单点注销成功", + "data": null +} +``` + + +### 3、单点注销回调接口 +此接口仅配置 `(reg-logout-call=true)` 时打开,且为框架回调,开发者无需关心 + +``` url +http://{host}:{port}/sso/logoutCall +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| loginId | 是 | 要注销的账号 id | +| timestamp | 是 | 当前时间戳,13位 | +| nonce | 是 | 随机字符串 | +| sign | 是 | 签名,生成算法:`md5( loginId={账号id}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | +| client | 否 | 客户端标识,如果你在登录时向 sso-server 端传递了 client 值,那么在此处 sso-server 也会给你回传过来,否则此参数无值。如果此参数有值,则此参数也要参与签名,放在 loginId 参数前面(字典顺序) | +| autoLogout | 否 | 是否为“登录client超过最大数量”引起的自动注销(true=超限系统自动注销,false=用户主动发起注销)。如果此参数有值,则此参数也要参与签名,放在 client 参数前面(字典顺序) | + + +返回数据: + +``` js +{ + "code": 200, // 200表示请求成功,非200标识请求失败 + "msg": "单点注销回调成功", + "data": null +} +``` + + + +### 4、消息推送接口 + +``` url +http://{host}:{port}/sso/pushC +``` + +接收参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| timestamp | 是 | 当前时间戳,13位 | +| msgType | 是 | 消息类型 | +| nonce | 是 | 随机字符串 | +| sign | 是 | 签名,生成算法:`md5( msgType={消息类型}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | + +此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。 + +返回值示例: + +- 推送成功时: + +``` js +{ + "code": 200, + "msg": "ok", + "data": "10001", // 返回的数据 +} +``` + +- 推送失败时: + +``` js +{ + "code": 500, // 200表示请求成功,非200标识请求失败 + "msg": "签名无效:xxx", // 失败原因 + "data": null +} +``` + +详细可参考:[消息推送机制](/sso/message-push) + + + + + + + + + + + diff --git a/sa-token-doc/sso/sso-check-domain.md b/sa-token-doc/sso/sso-check-domain.md new file mode 100644 index 000000000..bccef6b29 --- /dev/null +++ b/sa-token-doc/sso/sso-check-domain.md @@ -0,0 +1,61 @@ +# SSO整合-配置域名校验 + +--- + +### 1、Ticket劫持攻击 +在前面章节的 SSO-Server 示例中,配置项 `sa-token.sso-server.clients.sso-client3.allow-url=*` 意为该 client 所有允许的授权地址,不在此配置项中的 URL 将无法单点登录成功。 + +为了方便测试,上述代码将其配置为`*`,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 Ticket 劫持的风险。 + +假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL: + +> [http://sa-sso-server.com:9000/sso/auth?client=sso-client3&redirect=https://www.baidu.com/](http://sa-sso-server.com:9000/sso/auth?client=sso-client3&redirect=https://www.baidu.com/) + +当不知情的小红被诱导访问了这个URL时,它将被重定向至百度首页: + +sso-ticket-jc + +可以看到,代表着用户身份的 Ticket 码也显现到了 URL 之中,借此漏洞,攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器,伪造小红身份登录网站 + +### 2、防范方法 + +造成此漏洞的直接原因就是SSO-Server认证中心没有对 `redirect地址` 进行任何的限制,防范的方法也很简单,就是对`redirect参数`进行校验,如果其不在指定的URL列表中时,拒绝下放ticket + +我们将其配置为一个具体的URL: + + +``` yaml +sa-token: + sso-server: + clients: + sso-client3: + # 配置允许单点登录的 url + allow-url: http://sa-sso-client1.com:9003/sso/login +``` + +``` properties +# 配置允许单点登录的 url +sa-token.sso-server.clients.so-client3.allow-url=http://sa-sso-client1.com:9003/sso/login +``` + + +再次访问上述链接: + +sso-feifa-rf + +域名没有通过校验,拒绝授权! + +### 3、配置安全性参考表 + +| 配置方式 | 举例 | 安全性 | 建议 | +| :-------- | :-------- | :-------- | :-------- | +| 配置为* | `*` | | **禁止在生产环境下使用** | +| 配置到域名 | `http://sa-sso-client1.com/*` | | 不建议在生产环境下使用 | +| 配置到详细地址| `http://sa-sso-client1.com:9001/sso/login` | | 可以在生产环境下使用 | + + +### 4、疑问:为什么不直接回传 Token,而是先回传 Ticket,再用 Ticket 去查询对应的账号id? +Token 作为长时间有效的会话凭证,在任何时候都不应该直接暴露在 URL 之中(虽然 Token 直接的暴露本身不会造成安全漏洞,但会为很多漏洞提供可乘之机) + +为了不让系统安全处于亚健康状态,Sa-Token-SSO 选择先回传 Ticket,再由 Ticket 获取账号id,且 Ticket 一次性用完即废,提高安全性。 + diff --git a/sa-token-doc/sso/sso-custom-api.md b/sa-token-doc/sso/sso-custom-api.md new file mode 100644 index 000000000..97b82c0fc --- /dev/null +++ b/sa-token-doc/sso/sso-custom-api.md @@ -0,0 +1,127 @@ +# SSO整合-自定义 API 路由 + +--- + +### 方式一:修改全局变量 + +在之前的章节中,我们演示了如何搭建一个SSO认证中心: +``` java +/** + * Sa-Token-SSO Server端 Controller + */ +@RestController +public class SsoServerController { + + // SSO-Server端:处理所有SSO相关请求 + @RequestMapping("/sso/*") + public Object ssoRequest() { + return SaSsoServerProcessor.instance.dister(); + } + + // ... 其它代码 + +} +``` + +这种写法集成简单但却不够灵活。例如认证中心地址只能是:`http://{host}:{port}/sso/auth`,如果我们想要自定义其API地址,应该怎么做呢? + +打开SSO模块相关源码,有关 API 的设计都定义在: +[ApiName.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ApiName.java) +中,我们可以对其进行二次修改。 + +例如,我们可以在 Main 方法启动类或者 SSO 配置方法中修改变量值: +``` java +// 配置SSO相关参数 +@Autowired +private void configSso(SaSsoServerTemplate ssoServerTemplate) { + // 自定义API地址 + SaSsoServerProcessor.instance.ssoServerTemplate.apiName.ssoAuth = "/sso/auth2"; + // ... + +} +``` + +启动项目,统一认证地址就被我们修改成了:`http://{host}:{port}/sso/auth2` + + +### 方式二:拆分路由入口 +根据上述路由入口:`@RequestMapping("/sso/*")`,我们给它起一个合适的名字 —— 聚合式路由。 + +与之对应的,我们可以将其修改为拆分式路由: + +``` java +/** + * Sa-Token-SSO Server端 Controller + */ +@RestController +public class SsoServerController { + + // SSO-Server:统一认证地址 + @RequestMapping("/sso/auth") + public Object ssoAuth() { + return SaSsoServerProcessor.instance.ssoAuth(); + } + + // SSO-Server:RestAPI 登录接口 + @RequestMapping("/sso/doLogin") + public Object ssoDoLogin() { + return SaSsoServerProcessor.instance.ssoDoLogin(); + } + + // SSO-Server:接收推送消息地址 + @RequestMapping("/sso/pushS") + public Object ssoPushS() { + return SaSsoServerProcessor.instance.ssoPushS(); + } + + // SSO-Server:单点注销 + @RequestMapping("/sso/signout") + public Object ssoSignout() { + return SaSsoServerProcessor.instance.ssoSignout(); + } + + // ... 其它方法 + +} +``` + +拆分式路由 与 聚合式路由 在功能上完全等价,且提供了更为细致的路由管控。 + + +### SSO-Client 端拆分路由入口示例 + +``` java +/** + * Sa-Token-SSO Client端 Controller + */ +@RestController +public class SsoClientController { + + // SSO-Client:登录地址 + @RequestMapping("/sso/login") + public Object ssoLogin() { + return SaSsoClientProcessor.instance.ssoLogin(); + } + + // SSO-Client:单点注销地址 + @RequestMapping("/sso/logout") + public Object ssoLogout() { + return SaSsoClientProcessor.instance.ssoLogout(); + } + + // SSO-Client:单点注销回调 + @RequestMapping("/sso/logoutCall") + public Object ssoLogoutCall() { + return SaSsoClientProcessor.instance.ssoLogoutCall(); + } + + // SSO-Client:接收消息推送地址 + @RequestMapping("/sso/pushC") + public Object ssoPushC() { + return SaSsoClientProcessor.instance.ssoPushC(); + } + + // ... 其它方法 + +} +``` \ No newline at end of file diff --git a/sa-token-doc/sso/sso-custom-login.md b/sa-token-doc/sso/sso-custom-login.md new file mode 100644 index 000000000..dc5dcc0d1 --- /dev/null +++ b/sa-token-doc/sso/sso-custom-login.md @@ -0,0 +1,170 @@ +# SSO整合-定制化登录页面 + +--- + +### 1、何时引导用户去登录? + +#### 方案一:前端按钮跳转 +前端页面准备一个 **`[登录]`** 按钮,当用户点击按钮时,跳转到登录接口 +``` html +登录 +``` + +#### 方案二:后端拦截重定向 +在后端注册全局过滤器(或拦截器、或全局异常处理),拦截需要登录后才能访问的页面资源,将未登录的访问重定向至登录接口 +``` java +/** + * Sa-Token 配置类 + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + /** 注册 [Sa-Token全局过滤器] */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + .addInclude("/**") + .addExclude("/sso/*", "/favicon.ico") + .setAuth(obj -> { + if(StpUtil.isLogin() == false) { + String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString()); + SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back)); + SaRouter.back(); + } + }) + ; + } +} +``` + +#### 方案三:后端拦截 + 前端跳转 +首先,后端仍需要提供拦截,但是不直接引导用户重定向,而是返回未登录的提示信息 +``` java +/** + * Sa-Token 配置类 + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + /** 注册 [Sa-Token全局过滤器] */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + .addInclude("/**") + .addExclude("/sso/*", "/favicon.ico") + .setAuth(obj -> { + if(StpUtil.isLogin() == false) { + // 与前端约定好,code=401时代表会话未登录 + SaRouter.back(SaResult.ok().setCode(401)); + } + }) + ; + } +} +``` + +前端接受到返回结果 `code=401` 时,开始跳转至登录接口 +``` js +if(res.code == 401) { + location.href = '/sso/login?back=' + encodeURIComponent(location.href); +} +``` + +这种方案比较适合以 Ajax 访问的 RestAPI 接口重定向 + + + + +### 2、如何自定义登录视图? + +#### 方式一:在demo示例中直接更改 login.html 页面代码即可 + +#### 方式二:在配置中配置登录视图地址 + +``` java +// 配置:未登录时返回的View +ssoServerTemplate.strategy.notLoginView = () -> { + return new ModelAndView("xxx.html"); +} +``` + + +### 3、如何自定义登录API的接口地址? +根据需求点选择解决方案: + +#### 3.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数? +``` java +// 在任意代码处获取前端提交的参数 +String xxx = SaHolder.getRequest().getParam("xxx"); +``` + +#### 3.2、想完全自定义一个接口来接受前端登录请求? +``` java +// 直接定义一个拦截路由为 `/sso/doLogin` 的接口即可 +@RequestMapping("/sso/doLogin") +public SaResult ss(String name, String pwd) { + System.out.println("------ 请求进入了自定义的API接口 ---------- "); + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功!"); + } + return SaResult.error("登录失败!"); +} +``` + +#### 3.3、不想使用`/sso/doLogin`这个接口,想自定义一个API地址? + +答:直接在前端更改点击按钮时 Ajax 的请求地址即可 + + +### 4、不同 client 不同登录页 + +如果你的不同应用覆盖的用户群体差异极大,此时你可能想针对不同的应用跳转到不同的登录页,让每个应用的用户在登录时能够看到当前应用的专属信息,怎么做呢? + +首先,保证每个 sso-client 端都配置了不同的 client 标识: + + + + +``` yaml +sa-token: + sso-client: + # 当前 client 标识 + client: sso-client-shop +``` + +``` properties +# 当前 client 标识 +sa-token.sso-client.client=sso-client-shop +``` + + + +然后在 `sso-server` 里为每个系统开发不同的登录页,并在 `configSso` 方法里 `notLoginView` 函数中根据 client 值,返回不同的登录视图: + +``` java +// 配置SSO相关参数 +@Autowired +private void configSso(SaSsoServerTemplate ssoServerTemplate) { + + // 配置:未登录时返回的View + ssoServerTemplate.strategy.notLoginView = () -> { + + String client = SaHolder.getRequest().getParam("client"); + if("sso-client-shop".equals(client)) { + return new ModelAndView("sa-shop-login.html"); + } + if("sso-client-video".equals(client)) { + return new ModelAndView("sa-video-login.html"); + } + // 更多 ... + + // 都不匹配,返回一个默认的 + return new ModelAndView("sa-login.html"); + }; + + // ... + +} +``` + + + diff --git a/sa-token-doc/sso/sso-dev.md b/sa-token-doc/sso/sso-dev.md new file mode 100644 index 000000000..deaba1941 --- /dev/null +++ b/sa-token-doc/sso/sso-dev.md @@ -0,0 +1,204 @@ +# Sa-Token SSO Server端 二次开发用到的所有函数说明 + +本篇展示一下 SSO 模块常用的工具类、方法 + +## Sso-Server 工具类 + +### Ticket 操作 +``` java +// 增删改 + +// 删除 Ticket +SaSsoServerUtil.deleteTicket(String ticket); + +// 根据参数创建一个 ticket 码,并保存 +SaSsoServerUtil.createTicketAndSave(String client, Object loginId, String tokenValue); + +// 查 + +// 查询 ticket ,如果 ticket 无效则返回 null +SaSsoServerUtil.getTicket(String ticket); + +// 查询 ticket 指向的 loginId,如果 ticket 码无效则返回 null +SaSsoServerUtil.getLoginId(String ticket); + +// 查询 ticket 指向的 loginId,并转换为指定类型 +SaSsoServerUtil.getLoginId(String ticket, Class cs); + +// 校验 + +// 校验 Ticket,无效 ticket 会抛出异常 +SaSsoServerUtil.checkTicket(String ticket); + +// 校验 Ticket 码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 +SaSsoServerUtil.checkTicketParamAndDelete(String ticket); + +// 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 +SaSsoServerUtil.checkTicketParamAndDelete(String ticket, String client); + +// ticket 索引 + +// 查询 指定 client、loginId 其所属的 ticket 值 +SaSsoServerUtil.getTicketValue(String client, Object loginId); +``` + + + +### Client 信息获取 +``` java +// 获取所有 Client +SaSsoServerUtil.getClients(); + +// 获取应用信息,无效 client 返回 null +SaSsoServerUtil.getClient(String client); + +// 获取应用信息,无效 client 则抛出异常 +SaSsoServerUtil.getClientNotNull(String client); + +// 获取匿名 client 信息 +SaSsoServerUtil.getAnonClient(); + +// 获取所有需要接收消息推送的 Client +SaSsoServerUtil.getNeedPushClients(); +``` + + +### 重定向 URL 构建与校验 +``` java +// 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 +SaSsoServerUtil.buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue); + +// 校验重定向 url 合法性 +SaSsoServerUtil.checkRedirectUrl(String client, String url); +``` + + +### 单点注销 +``` java +// 指定账号单点注销 +SaSsoServerUtil.ssoLogout(Object loginId); + +// 指定账号单点注销 +SaSsoServerUtil.ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient); +``` + + + + + +### 消息推送 +``` java +// 向指定 Client 推送消息 +SaSsoServerUtil.pushMessage(SaSsoClientModel clientModel, SaSsoMessage message); + +// 向指定 client 推送消息,并将返回值转为 SaResult +SaSsoServerUtil.pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message); + +// 向指定 Client 推送消息 +SaSsoServerUtil.pushMessage(String client, SaSsoMessage message); + +// 向指定 client 推送消息,并将返回值转为 SaResult +SaSsoServerUtil.pushMessageAsSaResult(String client, SaSsoMessage message); + +// 向所有 Client 推送消息 +SaSsoServerUtil.pushToAllClient(SaSsoMessage message); + +// 向所有 Client 推送消息,并忽略掉某个 client +SaSsoServerUtil.pushToAllClient(SaSsoMessage message, String ignoreClient); +``` + + +详情请参考源码:[码云:SaSsoServerUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java) + + + +## Sso-Client 工具类 + + +### 构建交互地址 +``` java +// 构建URL:Server端 单点登录授权地址 +SaSsoClientUtil.buildServerAuthUrl(String clientLoginUrl, String back); +``` + + +### 消息推送 + +``` java +// 向 sso-server 推送消息 +SaSsoClientUtil.pushMessage(SaSsoMessage message); + +// 向 sso-server 推送消息,并将返回值转为 SaResult +SaSsoClientUtil.pushMessageAsSaResult(SaSsoMessage message); + +// 构建消息:校验 ticket +SaSsoClientUtil.buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl); + +// 构建消息:单点注销 +SaSsoClientUtil.buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter); +``` + +详情请参考源码:[码云:SaSsoClientUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java) + + + + +## Sso-Server 所有可重写策略 + +``` java +// 发送 Http 请求的处理函数 +SaSsoServerProcessor.instance.ssoServerTemplate.strategy.sendRequest = url -> { + // ... +} + +// 使用异步模式执行一个任务 +SaSsoServerProcessor.instance.ssoServerTemplate.strategy.asyncRun = fun -> { + // ... +} + +// 未登录时返回的 View +SaSsoServerProcessor.instance.ssoServerTemplate.strategy.notLoginView = () -> { + // ... +} + +// SSO-Server端:登录函数 +SaSsoServerProcessor.instance.ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { + // ... +} + +//SSO-Server端:在授权重定向之前的通知 +SaSsoServerProcessor.instance.ssoServerTemplate.strategy.jumpToRedirectUrlNotice = (redirectUrl) -> { + // ... +} + +// SSO-Server端:在校验 ticket 后,给 sso-client 端追加返回信息的函数 +SaSsoServerProcessor.instance.ssoServerTemplate.strategy.checkTicketAppendData = (loginId, result) -> { + // ... +} +``` + + + +## Sso-Client 所有可重写策略 + +``` java +// 发送 Http 请求的处理函数 +SaSsoClientProcessor.instance.ssoClientTemplate.strategy.sendRequest = url -> { + // ... +} + +// 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) +SaSsoClientProcessor.instance.ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { + // ... +} + +// 转换:认证中心 centerId > 本地 loginId +SaSsoClientProcessor.instance.ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> { + // ... +} + +// 转换:本地 loginId > 认证中心 centerId +SaSsoClientProcessor.instance.ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> { + // ... +} +``` diff --git a/sa-token-doc/sso/sso-diff-key.md b/sa-token-doc/sso/sso-diff-key.md new file mode 100644 index 000000000..f93fda278 --- /dev/null +++ b/sa-token-doc/sso/sso-diff-key.md @@ -0,0 +1,158 @@ +# 不同 SSO Client 配置不同秘钥 + +在校验 ticket、单点注销等操作发起的 http 调用时,需要配置秘钥参数,像这样: + + + +``` yaml +sa-token: + sign: + # API 接口调用秘钥 + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +``` properties +# 接口调用秘钥 +sa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + + +如果 SSO Client 端和 SSO Server 端配置的秘钥不同,则无法调通请求,显示无效签名: +``` js +{ + "code": 500, + "msg": "无效签名:9f1b453817bfeac56d2f772a66c01eb2", + "data": null +} +``` + +如果你有多个 SSO Client,你可能想让每个应用配置不同的秘钥,让它们彼此之间不能互相“冒充”,怎么做呢? + +### 1、首先在 SSO Client 端,你需要配置上不同的 Client 标识参数: + +例如在 client1 我们配置上: + + + +``` yaml +sa-token: + sso-client: + # 当前 client 标识 + client: sso-client1 + # ... + sign: + # sso-client1 使用的秘钥 + secret-key: secret-key-xxxx-1 +``` + +``` properties +# 当前 client 标识 +sa-token.sso-client.client=sso-client1 + +# sso-client1 使用的秘钥 +sa-token.sign.secret-key=secret-key-xxxx-1 +``` + + + +在 client2 我们配置上: + + + +``` yaml +sa-token: + sso-client: + # 当前 client 标识 + client: sso-client2 + # ... + sign: + # sso-client2 使用的秘钥 + secret-key: secret-key-xxxx-2 +``` + +``` properties +# 当前 client 标识 +sa-token.sso-client.client=sso-client2 + +# sso-client2 使用的秘钥 +sa-token.sign.secret-key=secret-key-xxxx-2 +``` + + + +### 2、然后在 SSO Server 端,重写获取秘钥的函数 + +在 SSO Server 端新建 `CustomSaSsoServerTemplate.java`,继承 `SaSsoServerTemplate`,重写其 `getSignTemplate` 函数: + +``` java +/** + * 自定义 SaSsoServerTemplate 子类 + */ +@Component +public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { + + // 存储所有 client 的秘钥 + static Map signMap = new HashMap<>(); + static { + signMap.put("sso-client1", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-1"))); + signMap.put("sso-client2", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-2"))); + signMap.put("sso-client3", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-3"))); + // ... + } + + @Override + public SaSignTemplate getSignTemplate(String client) { + // 先从自定义的 signMap 中获取 + SaSignTemplate saSignTemplate = signMap.get(client); + if (saSignTemplate != null) { + return saSignTemplate; + } + // 找不到就返回全局默认的 SaSignTemplate + return SaManager.getSaSignTemplate(); + } +} +``` + +至此完成。 + + +### 3、其它注意点 + +有同学反馈,集成 “不同 SSO Client 配置不同秘钥” 模式后,客户端发起调用 `/sso/getData` 调用时会报如下错误: + +``` +无效签名:5a7fc42836deba12d96527d43c1301ea +``` + +或者: +``` +参与参数签名的秘钥不可为空 +``` + +这大概率是因为在 sso-server 端的 `/sso/getData` 接口在校验签名时忘了加 client 参数导致的,修改为如下代码即可: + +``` java +// 示例:获取数据接口(用于在模式三下,为 client 端开放拉取数据的接口) +@RequestMapping("/sso/getData") +public SaResult getData(String apiType, String loginId) { + System.out.println("---------------- 获取数据 ----------------"); + System.out.println("apiType=" + apiType); + System.out.println("loginId=" + loginId); + + // ↓↓↓ ⚠️ 重点代码 ↓↓↓ + // 校验签名:只有拥有正确秘钥发起的请求才能通过校验 + String client = SaHolder.getRequest().getHeader("client"); + SaSsoServerProcessor.instance.ssoServerTemplate.getSignTemplate(client).checkRequest(SaHolder.getRequest()); + // ↑↑↑ ⚠️ 重点代码 ↑↑↑ + + // 自定义返回结果(模拟) + return SaResult.ok() + .set("id", loginId) + .set("name", "LinXiaoYu") + .set("sex", "女") + .set("age", 18); +} +``` + + diff --git a/sa-token-doc/sso/sso-h5.md b/sa-token-doc/sso/sso-h5.md new file mode 100644 index 000000000..6965b7d3c --- /dev/null +++ b/sa-token-doc/sso/sso-h5.md @@ -0,0 +1,322 @@ +# SSO整合-前后端分离架构下的整合方案 + +--- + +## SSO-Client 前后端分离 + +要在前后端分离的环境中接入 SSO,思路不难,主要的工作是把后端 `/sso/login` 接口的路由中转工作拿到前端来,以`sa-token-demo-sso3-client`为例: + +### 1、在 sso-client 后端新建`H5Controller`,开放接口: + +``` java +/** + * 前后台分离架构下集成 SSO 所需的代码 (SSO-Client端) + */ +@RestController +public class H5Controller { + + // 判断当前是否登录 + @RequestMapping("/sso/isLogin") + public Object isLogin() { + return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); + } + + // 返回SSO认证中心登录地址 + @RequestMapping("/sso/getSsoAuthUrl") + public SaResult getSsoAuthUrl(String clientLoginUrl) { + String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); + return SaResult.data(serverAuthUrl); + } + + // 根据 ticket 进行登录 + @RequestMapping("/sso/doLoginByTicket") + public SaResult doLoginByTicket(String ticket) { + SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); + StpUtil.login(ctr.loginId, new SaLoginParameter() + .setTimeout(ctr.remainTokenTimeout) + .setDeviceId(ctr.deviceId) + ); + return SaResult.data(StpUtil.getTokenValue()); + } + +} +``` + + +### 2、增加跨域处理策略 + +``` java +/** + * [Sa-Token 权限认证] 配置类 + */ +@Configuration +public class SaTokenConfigure { + + /** + * CORS 跨域处理策略 + */ + @Bean + public SaCorsHandleFunction corsHandle() { + return (req, res, sto) -> { + res. + // 允许指定域访问跨域资源 + setHeader("Access-Control-Allow-Origin", "*") + // 允许所有请求方式 + .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") + // 有效时间 + .setHeader("Access-Control-Max-Age", "3600") + // 允许的header参数 + .setHeader("Access-Control-Allow-Headers", "*"); + + // 如果是预检请求,则立即返回到前端 + SaRouter.match(SaHttpMethod.OPTIONS) + .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) + .back(); + }; + } + +} +``` + +详细参考:[解决跨域问题](/fun/cors-filter) + + +### 3、新建前端项目 +任意文件夹新建前端项目:`sa-token-demo-sso-client-h5`,在根目录添加测试文件:`index.html` +``` html + + + + + Sa-Token-SSO-Client端-测试页(前后端分离版-原生h5) + + +

Sa-Token SSO-Client 应用端(前后端分离版-原生h5)

+

当前是否登录:

+

+ 登录 - + 单应用注销 - + 单浏览器注销 - + 全端注销 - + 账号资料 +

+ + + + +``` + +### 4、添加单点登录登录中转页 + +在根目录创建文件:`sso-login.html` + +``` html + + + + + Sa-Token-SSO-Client端-登录中转页页 + + + + + + + + +``` + + +### 5、添加公共 js文件 +新建 `sso-common.js`: + +``` js +// 服务器接口主机地址 +// var baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 +var baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 + +// 封装一下Ajax +function ajax(path, data, successFn, errorFn) { + console.log('发起请求:', baseUrl + path, JSON.stringify(data)); + fetch(baseUrl + path, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + 'satoken': localStorage.getItem('satoken') + }, + body: serializeToQueryString(data), + }) + .then(response => response.json()) + .then(res => { + console.log('返回数据:', res); + if(res.code === 500) { + return alert(res.msg); + } + successFn(res); + }) + .catch(error => { + console.error('请求失败:', error); + return alert("异常:" + JSON.stringify(error)); + }); +} + +// ------------ 工具方法 --------------- + +// 从url中查询到指定名称的参数值 +function getParam(name, defaultValue) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + if (pair[0] == name) { + return pair[1]; + } + } + return (defaultValue == undefined ? null : defaultValue); +} + +// 将 json 对象序列化为kv字符串,形如:name=Joh&age=30&active=true +function serializeToQueryString(obj) { + return Object.entries(obj) + .filter(([_, value]) => value != null) // 过滤 null 和 undefined + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); +} + +// 向指定标签里 set 内容 +function setHtml(select, html) { + const dom = document.querySelector('.is-login'); + if(dom) { + dom.innerHTML = html; + } +} +``` + + +### 6、测试运行 +先启动 Server 服务端与 Client 服务端,再随便找个能预览html的工具打开前端项目(比如[HBuilderX](https://www.dcloud.io/hbuilderx.html)),测试流程与一体版一致,暂不赘述。 + + + + +> [!TIP| label:另附其它技术栈的前后端分离 demo 示例:] +> - sso-client 前后端分离 - 原生h5:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5) +> - sso-client 前后端分离 - vue2:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2) +> - sso-client 前后端分离 - vue3:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3) + + +## SSO-Server 前后端分离 + +解决思路与 SSO-Client 一样,我们需要把原本在 “后端处理的授权重定向逻辑” 拿到前端来实现。 + +由于集成代码与 Client 端类似,这里暂不贴详细代码,我们可以下载官方仓库,里面有搭建好的demo。 + +使用前端 ide 导入项目 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5`,浏览器访问 `sso-auth.html` 页面: + +sso-type2-server-h5-auth.png + +复制上述地址,将其配置到 Client 端的配置项 `sa-token.sso-client.auth-url` ,例如: + + + +``` yaml +sa-token: + sso-client: + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) + auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html +``` + +``` properties +# SSO-Server端 统一认证地址 +sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) +sa-token.sso-client.auth-url=http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html +``` + + + +然后我们启动项目 sso-server 与 sso-client ,按照之前的测试步骤访问: +[http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/),即可以前后端分离模式完成 SSO-Server 端的授权登录。 + diff --git a/sa-token-doc/sso/sso-home-jump.md b/sa-token-doc/sso/sso-home-jump.md new file mode 100644 index 000000000..9ecc8a729 --- /dev/null +++ b/sa-token-doc/sso/sso-home-jump.md @@ -0,0 +1,95 @@ +# SSO 平台中心跳转模式,点连接跳入子系统 + +--- + +有的时候,我们需要把 sso-server 搭建成一个平台中心,效果图大致如下: + +sso-home-jump.png + +如图所示,用户先从 sso-server 登录进入平台首页,在首页上有各个子系统的进入链接,用户点击链接进入子系统(免登录)。 + +怎么做到如上效果呢?当然,加个超链接跳到子系统并不难,难点在于跳转的同时我们需要让用户自动登录上子系统,从而达到:平台中心一处登录,所有子系统无障碍通行的效果。 + +怎么做到跳转的时候自动登录呢?直接跳转肯定是不会自动登录的,我们需要对链接改造一下: + +假设子系统的地址是: + +``` url +http://sa-sso-client1.com:9003/ +``` + +那么我们改造后的地址就是: + +``` url +/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/ +``` + +格式形如:`/sso/auth?client={client标识}&redirect=${子系统首页}/sso/login?back=${子系统首页}` + +--- + +### 完整代码示例: + +1、在 sso-server 中配置 `home-route` 字段: + + + +``` yaml +# Sa-Token 配置 +sa-token: + # SSO-Server 配置 + sso-server: + # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 + home-route: /home +``` + +``` properties +# 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 +sa-token.sso-server.home-route: /home +``` + + + + +2、在 sso-server 中添加 `HomeController`,作为平台中心首页: + +``` java +/** + * SSO 平台中心模式示例,跳连接进入子系统 + */ +@RestController +public class HomeController { + // 平台化首页 + @RequestMapping({"/", "/home"}) + public Object index() { + // 如果未登录,则先去登录 + if(!StpUtil.isLogin()) { + return SaHolder.getResponse().redirect("/sso/auth"); + } + + // 拼接各个子系统的地址,格式形如:/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页} + String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/"; + String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/"; + String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/"; + + // 组织网页结构返回到前端 + String title = "

SSO 平台首页 (平台中心模式)

"; + String client1 = "

进入Client1系统

"; + String client2 = "

进入Client2系统

"; + String client3 = "

进入Client3系统

"; + + return title + client1 + client2 + client3; + } +} +``` + + +### 测试访问 + +启动项目,访问:[http://sa-sso-server.com:9000](http://sa-sso-server.com:9000) + +首次访问,因为我们没有登录,所以会被重定向到 `/sso/auth` 登录页,我们登录上之后,便会跳转到平台中心首页: + +sso-home-jump-do.png + +依次点击三个链接,便可在跳转的同时自动登录上子系统。 diff --git a/sa-token-doc/sso/sso-nosdk.md b/sa-token-doc/sso/sso-nosdk.md new file mode 100644 index 000000000..89d703c8e --- /dev/null +++ b/sa-token-doc/sso/sso-nosdk.md @@ -0,0 +1,46 @@ +# SSO整合 - NoSdk、ReSdk 模式与非 java 项目 + +--- + +经常有小伙伴提问:客户端不使用 Sa-Token,能否接入 SSO 认证中心?当然是可以的。 + +SSO-Server 所有接口都是通过 http 协议开放的,这意味着原则上只要一个语言支持 http 请求调用就可以对接 SSO-Server,参考: [SSO 认证中心开放接口](/sso/sso-apidoc) + +### NoSdk 模式 + +NoSdk 模式(不使用SDK):通过 http 工具类调用接口的方式来对接 SSO-Server。 + +参考 demo:[sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk) + +该 demo 假设应用端没有使用任何“权限认证框架”,使用最基础的 ServletAPI 进行会话管理,模拟了 `/sso/login`、 `/sso/logout`、 `/sso/pushC` 三个接口的处理逻辑。 + +> [!INFO| label:NoSdk 模式优缺点] +> - 1、支持客户端使用任意技术栈。 +> - 2、代码简单易懂,流程直观清晰。 +> - 3、用 http 工具类模拟 Sa-Token SSO 内部实现,样版代码较多,略显冗余。 + + + +### ReSdk 模式 + +ReSdk 模式(重写SDK部分方法):通过重写框架关键步骤点,来对接 SSO-Server。 + +参考 demo:[sa-token-demo-sso3-client-resdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk) + +> [!INFO| label:ReSdk 模式优缺点] +> - 1、支持客户端使用任意技术栈。 +> - 2、仅重写少量部分关键代码,即可完成对接。几乎可以得到 Sa-Token SSO 模块全量能力。 +> - 3、此模式需要对 Sa-Token SSO 内部实现较为熟悉,才可以驾驭。 + + + + +### 非 java 语言项目 + +sso-server 的所有接口均以 http 协议对外开放,因此原则上支持任何语言对接,只要这个语言支持 http 请求调用。 + +例如 PHP、.NET、Node.js 等语言的项目,无法集成 Sa-Token,同上,也可以通过 http 工具类调用接口的方式来对接 SSO-Server。 + +建议各位同学先搞懂 NoSdk 模式的对接流程,再参照 [SSO 认证中心开放接口](/sso/sso-apidoc) 章节进行对接。 + + diff --git a/sa-token-doc/sso/sso-pro.md b/sa-token-doc/sso/sso-pro.md new file mode 100644 index 000000000..a0bf55332 --- /dev/null +++ b/sa-token-doc/sso/sso-pro.md @@ -0,0 +1,52 @@ +# Sa-Sso-Pro 单点登录商业版 + +### 项目介绍 + +根据 Sa-Token SSO 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个SSO模式的认证Demo。 + + + +然而,要真正开发一个商业级项目的认证中心系统,绝非一朝一夕可以搭建完毕,其必不可少的一些功能, +比如:用户账号增删改查维护、登录日志统计、新增用户数据报表、新增 Client 应用接入域名配置……等等, +仍需要大量的开发时间。 + +为此,我们特意准备了项目:[[ Sa-Sso-Pro 单点登录商业版 ]](https://sa-max.cn?way=st_md), +项目集成了单点登录常见技术点, 绝大多数功能无需二次开发,直接可用,可大大缩短您的项目接入单点登录的开发周期。 + + +### 释疑 + +##### 1、Sa-Sso-Pro 是收费项目吗?与 Sa-Token 有什么不同? + +`Sa-Sso-Pro` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 + +`Sa-Sso-Pro` 与 `Sa-Token` 的区别,简单来讲: +- `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 +- `Sa-Sso-Pro` 是一个完整项目,下载源码后可直接启动 + + +##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? +首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 + +其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, +比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 + +Sa-Sso-Pro 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 + +另外:即使您没有购买 `Sa-Sso-Pro`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 + + + +##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Sso-Pro 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! + +答案是不会。 + +再次强调一下:`Sa-Token` 与 `Sa-Sso-Pro` 是两个独立的项目,两者互不影响。 +付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 + +实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 + + + + + diff --git a/sa-token-doc/sso/sso-questions.md b/sa-token-doc/sso/sso-questions.md new file mode 100644 index 000000000..ac915075e --- /dev/null +++ b/sa-token-doc/sso/sso-questions.md @@ -0,0 +1,398 @@ +# Sa-Token-SSO整合-常见问题总结 + +SSO 集成常见问题整理 + +[[toc]] + +--- + + +### 问:在模式一与模式二中,Client端 必须通过 Alone-Redis 插件来访问 Redis 吗? +答:不必须,只是推荐,权限缓存与业务缓存分离后会减少 `SSO-Redis` 的访问压力,且可以避免多个 `Client端` 的缓存读写冲突。 + + +### 问:搭建好 sso-server 或 sso-client 服务后,访问返回:`{"msg": "not handle"}`。 + +返回这个信息,代表你访问的路由有错误,比如说: + +- 统一认证登录地址是:`http://{host}:{port}/sso/auth`。 +- 而你访问的却是:`http://{host}:{port}/sso/auth2`。 + +地址写错了,框架就不会处理这个请求,会直接返回 `{"msg": "not handle"}`,所有开放地址可参考:[SSO 开放接口](/sso/sso-apidoc) + +如果仔细检查地址后没有写错,却依然返回了这个信息,那有可能是对应的接口没有打开,比如说: + +- sso-server 端的单点注销地址:`http://{host}:{port}/sso/signout`; +- sso-client 端的注销地址:`http://{host}:{port}/sso/logout`; + +都需要在配置文件配置:`sa-token.sso-server.is-slo=true`(client端为 `sa-token.sso-client.is-slo=true` )后,才会打开。 + + +### 问:我参照文档搭建SSO-Client,一直提示:Ticket无效,请问怎么回事? +如果使用的是模式二,出现此异常概率最大的原因是因为 `Client` 与 `Server` 没有连接同一个Redis,SSO模式二中两者必须连接同一个 Redis 才可以登录成功。 + +你可能会问:我看配置文件明明是同一个啊? + +我的建议是:排查时不要仅凭肉眼判断,分别在你的 `Client` 与 `Server` 启动后调用 `SaManager.getSaTokenDao().set("name", "value", 100000);` +随便写入一个值,看看能不能根据你的预期写进同一个Redis里,如果能的话才能证明 `Client` 与 `Server` 连接的Reids 是同一个,再进行下一步排查。 + +``` java +@SpringBootApplication +public class SaSsoServerApplication { + public static void main(String[] args) { + SpringApplication.run(SaSsoServerApplication.class, args); + System.out.println("\n------ Sa-Token-SSO 统一认证中心启动成功 "); + // 分别在 Client 与 Server 启动后调用 set 数据代码,看看能否根据预期写入同一个 reids + SaManager.getSaTokenDao().set("name", "value", 100000); + } +} +``` + +如果使用的是模式三,则排查是否有重复校验 ticket 的代码,一个 ticket 码只能使用一次,多次重复使用就会提示这个。 + + +### 问:模式一或者模式二报错:Could not write JSON: No serializer found for class com.pj.sso.SysUser and no properties discovered to create BeanSerializer + +一般是因为在 sso-server 端往 session 上写入了某个实体类(比如 User),而在 sso-client 端没有这个实体类,导致反序列化失败。 + +解决方案:在 sso-client 也新建上这个类,而且包名需要与 sso-server 端的一致(直接从 sso-server 把实体类复制过来就好了) + + + +### 在测试模式一时,出现一些难以理解的现象 + +测试模式一时,三种异常现象: + +1、在 sso-client 端点击登录,可以成功跳转到 sso-server 端,登录后可以跳回 sso-client 端,但显示 sso-client 端未登录 +- 原因:sso-server 后端没有配置 sa-token.cookie.domain 值。 + +2、在 sso-client 端点击登录,可以成功跳转到 sso-server 端,登录页面刷新一下,并没有跳转回 sso-client 端,依然提示让你登录 +- 可能1:sso-server 后端配置了错误的 sa-token.cookie.domain 值。 +- 可能2:sso-server 后端没有打开 sa-token.is-read-cookie 值。(测试模式二三时发生这种现象,有时候也是因为这个) + +3、在 sso-client 端点击登录,页面只是闪了一下,肉眼没有观察到页面跳转,页面也没有显示登录上。 + +原因:sso-server 的页面存储了不带 . 的有效 Cookie,为什么会这样:常常是因为测试模式二三后,没有清除redis记录或者浏览器记录,直接再开始测试模式一, +模式二三登录成功后遗留的有效cookie,影响了模式一的行为逻辑。 + +解决方案: +- 1、手动清空 redis里的所有数据, +- 2、或者手动清空 sso-server 域名下的所有 Cookie +- 3、换一个新的干净浏览器来测试。 + + + + + +### 问:模式三配置一堆 xxx-url ,有办法简化一下吗? +可以使用 `sa-token.sso-client.server-url` 来简化: + +配置含义:配置 Server 端主机总地址,拼接在 authUrl、getDataUrl、signoutUrl、pushUrl 属性前面,用以简化各种 url 配置。 + +在开发 SSO 模块时,我们需要在 sso-client 配置认证中心的各种地址,特别是在模式三下,一般代码会变成这样: + +``` yaml +sa-token: + sso-client: + # SSO-Server端 统一认证地址 + auth-url: http://sa-sso-server.com:9000/sso/auth + # 单点注销地址 + slo-url: http://sa-sso-server.com:9000/sso/signout + # SSO-Server端 查询数据地址 + get-data-url: http://sa-sso-server.com:9000/sso/getData +``` + +一堆 xxx-url 配置比较繁琐,且含有大量重复字符,现在我们可以将其简化为: +``` yaml +sa-token: + sso-client: + server-url: http://sa-sso-server.com:9000 +``` + +只要你配置了 `server-url` 地址,Sa-Token 就可以自动拼接出其它四个地址: + +**例1,使用 server-url 简化:** +- 你配置的 server-url 值是:`http://sa-sso-server.com:9000`。 +- 框架拼接出的 auth-url 值就是:`http://sa-sso-server.com:9000/sso/auth`,其它三个 url 配置项同理。 + +**例2,使用 server-url + auth-url 简化:** +- 你配置的 server-url 值是:`http://sa-sso-server.com:9000`,auth-url 是:`/sso/auth2`。 +- 框架拼接出的 auth-url 值就是:`http://sa-sso-server.com:9000/sso/auth2`,其它三个 url 配置项同理。 + +**例3,auth-url 地址以 http 字符开头:** +- 你配置的 server-url 值是:`http://sa-sso-server.com:9000`,auth-url 是:`http://my-site.com/sso/auth2`。 +- 此时框架只以 auth-url 值为准,得到的 auth-url 值是:`http://my-site.com/sso/auth2`,其它三个 url 配置项同理。 + + +### 问:我接手了一个项目,里面集成了 Sa-Token SSO ,请问怎么快速分辨它用的模式几? + +**方法一:看代码注释。** + +如果开发这个项目的人没有写清楚注释那就 gg 了。 + +**方法二,根据配置项来分析,例如:** + +- 先看配置项 `sa-token.cookie.domain`,如果此配置项有值,一般是在使用模式一开发,否则就是模式二或者模式三。 +- 再看配置项 `sa-token.sso-client.is-http` ,如果有值且为 true,一般是在使用模式三,否则就是模式二。 + +**方法三,根据配置项 `sa-token.sso-client.mode` 的提示来判断** + +`sa-token.sso-client.mode` 是框架预留的约定型配置项,此配置项不对代码逻辑产生任何影响,只为系统做一个标记,标注此系统用到了SSO的哪个模式。 + +例如你可以将其配置为 `sa-token.sso-client.mode=client-2`,代表当前系统为 sso-client 端,使用 SSO 模式二来对接。 + +需要注意,这个配置项不是必须的,你不写也不会对代码造成任何影响,只有在你需要为系统做一个明确的标记时才需要去配置它,方便后人阅读代码时快速分析使用的模式。 + +例如我们可以使用以下约定: + +- `sa-token.sso-client.mode=client-2`:代表当前系统为 sso-client 端,使用 SSO 模式二来对接。 +- `sa-token.sso-client.mode=client-2,h5`:代表当前系统为 sso-client 端,使用 SSO 模式二来对接,并且是前后端分离模式。 +- `sa-token.sso-server.mode=server-123`:代表当前系统为 sso-server 端,同时开放了 SSO 模式一、模式二、模式三。 +- `sa-token.sso-server.mode=server-2,client-2`:代表当前系统既是 sso-server 端,又是 sso-clent 端,使用模式二来对接。 +- 等等等等... + +此配置项可以是任意字符串,你也可以自己整理一套合适的表达规则。 + + + + + + +### 问:SSO模式二或模式三,第一个 client 登录成功之后再访问其它两个 client 不会自动登录,需要点一下登录按钮才会登录上? +答:这是正常现象,系统 1 登录成功之后,系统 2 与系统 3 需要点击登录按钮,才会登录成功。 + +> 第一个系统,需要:点击 [登录] 按钮 -> 跳转到登录页 -> 输账号密码 -> 登录成功
+> 第二个系统,需要:点击 [登录] 按钮 -> 登录成功
+> 第三个系统,需要:点击 [登录] 按钮 -> 登录成功 +> +> (系统二、三 免去重复跳转登录页输入账号密码的步骤) + + +### 追问:那我是否可以设计成不需要点登录按钮的,只要访问页面,它就能登录成功? +可以的。 + +其实思路很简单,我们只需要给 client 项目加个过滤器,拦截所有请求,只要检测到未登录就将其重定向至登录页面: + +``` java +/** + * Sa-Token 配置类 + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + /** 注册 [Sa-Token全局过滤器] */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + .addInclude("/**") + .addExclude("/sso/*", "/favicon.ico") // 这里需要注意排除掉 /sso/* 相关请求不拦截,否则就会触发无限重定向 + .setAuth(obj -> { + /* + * 这里会被分为两种情况: + * 情况1:这个请求在当前 client 已经登录,此时会顺利进入网站 + * 情况2:这个请求在当前 client 尚未登录,此时会被拦截,重定向至当前系统的 /sso/login?back=当前地址 + * + * 情况2会带领着用户继续重定向至 sso-server 认证中心,此时又分为两种情况: + * 情况2.1:此用户在 sso-server 尚未登录,此时会停留在登录页面,开始输入账号密码进行登录 + * 情况2.2:此用户在 sso-server 已经登录(这证明此用户已经在其它至少一个 sso-client 处完成了登录) + * 此时用户会继续重定向回当前 client,并携带 ticket 参数,完成登录。 + */ + if(StpUtil.isLogin() == false) { + String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString()); + SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back)); + SaRouter.back(); + } + }) + ; + } +} +``` + + +更多登录姿势可以参考 [[何时引导用户去登录]](/sso/sso-custom-login) 给出的建议进行设计。 + + + +### 问:Client 信息可以做成从数据库读取的吗? +可以,自定义 `SaSsoServerTemplate` 实现类,重写 `getClient` 与 `getClients` 方法即可: +``` java +/** + * 重写 SaSsoServerTemplate 部分方法,增强功能 + */ +@Component +public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { + + // 获取指定 client 的配置信息 + @Override + public SaSsoClientModel getClient(String client) { + if("sso-client1".equals(client)) { + SaSsoClientModel scm = new SaSsoClientModel(); + scm.setAllowUrl("sso-client1"); + scm.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); + return scm; + } + + // ... + + return null; + } + + // 返回所有 client 信息 + @Override + public List getClients() { + // 模拟示例代码,真实项目可改为从数据查询 + + SaSsoClientModel scm1 = new SaSsoClientModel(); + scm1.setAllowUrl("sso-client1"); + scm1.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); + + SaSsoClientModel scm2 = new SaSsoClientModel(); + scm2.setAllowUrl("sso-client2"); + scm2.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); + + // ... + + return Arrays.asList(scm1, scm2); + } + +} +``` + + +### 问:如果 sso-client 端我没有集成 sa-token-sso,如何对接? +需要手动调用 http 请求来对接 sso-server 开放的接口,参考示例: +[sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk) + + +### 问:如果 sso-client 端不是 java语言,可以对接吗? +可以,只不过有点麻烦,基本思路和上个问题一致,需要手动调用 http 请求来对接 sso-server 开放的接口,参考: +[SSO-Server 认证中心开放接口](/sso/sso-apidoc) + + +### 问:将旧有系统改造为单点登录时,应该注意哪些? +答:建议不要把其中一个系统改造为SSO服务端,而是新起一个项目作为 SSO-Server 端,所有旧有项目全部作为 Client 端与此对接。 + + +### 问:怎么在一个项目里同时搭建 sso-server 和 sso-client? + +难点在于解决两边的路由冲突,示例代码: + +``` java +// Sa-Token SSO Controller +@RestController +public class SsoController { + + // 处理 SSO-Server 端所有请求 + @RequestMapping({"/sso/auth", "/sso/doLogin", "/sso/signout", "/sso/pushS"}) + public Object ssoServerRequest() { + return SaSsoServerProcessor.instance.dister(); + } + + // 处理 SSO-Client 端所有请求 + @RequestMapping({"/sso/login", "/sso/logout", "/sso/logoutCall", "/sso/pushC"}) + public Object ssoClientRequest() { + return SaSsoClientProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Autowired + private void configSsoServer(SaSsoServerTemplate ssoServerTemplate) { + // SSO Server 配置代码,参考文档前几章 ... + } + @Autowired + private void configSsoClient(SaSsoClientTemplate ssoClientTemplate) { + // SSO Client 配置代码,参考文档前几章 ... + } + +} +``` + + +### 问:我一个项目里有两套账号体系,都需要单点登录,怎么在一个项目里同时搭建两个 sso-server 服务? + +首先推荐你不要在一个项目里同时搭建两个 sso-server,建议创建两个项目,分别搭建各自的 sso-server 服务。 + +如果一定要在一个项目中搭建两套 sso-server 服务,参考方案如下: + +第一套,还是用前面几章文档给出的示例代码。 + +第二套,修改一些参数属性,使之与第一套不产生冲突,参考代码如下: + +``` java +/** + * Sa-Token-SSO 第二套 SSO-Server端 Controller + */ +@RestController +public class SsoUserServerController { + + /** + * 新建一个 SaSsoServerProcessor 请求处理器 + */ + public static SaSsoServerProcessor ssoUserServerProcessor = new SaSsoServerProcessor(); + static { + + // 自定义一个 getServerConfig + SaSsoServerConfig serverConfig = new SaSsoServerConfig(); + serverConfig.setSecretKey("xxx"); + // 更多配置 ... + + // 自定义一个 SaSsoServerTemplate 对象 + SaSsoServerTemplate ssoUserTemplate = new SaSsoServerTemplate() { + @Override + public SaSsoServerConfig getServerConfig() { + return serverConfig; + } + }; + + // 使用自定义的 StpLogic 会话对象 + ssoUserTemplate.setStpLogic(StpUserUtil.stpLogic); + + // 让这个SSO请求处理器,使用的路由前缀是 /sso-user,而不是原先的 /sso + ssoUserTemplate.apiName.replacePrefix("/sso-user"); + + // 给这个 SSO 请求处理器使用自定义的 SaSsoTemplate 对象 + ssoUserServerProcessor.ssoServerTemplate = ssoUserTemplate; + } + + /* + * 第二套 sso-server 服务:处理所有SSO相关请求 + * http://{host}:{port}/sso-user/auth -- 单点登录授权地址 + * http://{host}:{port}/sso-user/doLogin -- 账号密码登录接口 + * http://{host}:{port}/sso-user/signout -- 单点注销地址(isSlo=true时打开) + */ + @RequestMapping("/sso-user/*") + public Object ssoUserRequest() { + return ssoUserServerProcessor.dister(); + } + + // 自定义 doLogin 方法 */ + // 注意点: + // 1、第2套 sso-server 对应的 RestApi 登录接口也应该更换为 /sso-user/doLogin,而不是原先的 /sso/doLogin + // 2、在这里,登录函数要使用自定义的 StpUserUtil.login(),而不是原先的 StpUtil.login() + @RequestMapping("/sso-user/doLogin") + public Object ssoUserRequest(String name, String pwd) { + if("sa".equals(name) && "123456".equals(pwd)) { + StpUserUtil.login(10001); + return SaResult.ok("登录成功!").setData(StpUserUtil.getTokenValue()); + } + return SaResult.error("登录失败!"); + } + +} +``` + + + +
+ +--- + +
+还有其它问题? +

+可以加群反馈一下,比较典型的问题我们解决之后都会提交到此页面方便大家快速排查 +

+
+ + + + diff --git a/sa-token-doc/sso/sso-server.md b/sa-token-doc/sso/sso-server.md new file mode 100644 index 000000000..703064289 --- /dev/null +++ b/sa-token-doc/sso/sso-server.md @@ -0,0 +1,317 @@ +# 搭建统一认证中心 SSO-Server + +在开始SSO三种模式的对接之前,我们必须先搭建一个 SSO-Server 认证中心 + +> [!TIP| label:demo] +> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`,如遇到难点可结合源码进行测试学习。[Sa-Token 集成示例大全下载](/more/download-demos) ,demo里有制作好的登录页面 + +--- + +### 1、添加依赖 +创建 SpringBoot 项目 `sa-token-demo-sso-server`,在引入 SpringBoot 依赖的基础上,继续引入: + + + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + + + + + cn.dev33 + sa-token-sso + ${sa.top.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + cn.dev33 + sa-token-forest + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 权限认证,在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' + +// Sa-Token 插件:整合SSO +implementation 'cn.dev33:sa-token-sso:${sa.top.version}' + +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' + +// 视图引擎(在前后端不分离模式下提供视图支持) +implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + +// Sa-Token 插件:整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) +implementation 'cn.dev33:sa-token-forest:${sa.top.version}' +``` + + + +> [!NOTE| label:引包简化] +> 除了 `sa-token-spring-boot-starter` 和 `sa-token-sso` 以外,其它包都是可选的: +> +> - 在 SSO 模式三时 Redis 相关包是可选的。 +> - 在前后端分离模式下可以删除 thymeleaf 相关包。 +> - 在不需要 SSO 模式三单点注销的情况下可以删除 http 工具包。 +> +> 建议先完整测试三种模式之后再对 pom 依赖进行酌情删减。 + + +### 2、开放认证接口 +新建 `SsoServerController`,用于对外开放接口: + +``` java +/** + * Sa-Token-SSO Server端 Controller + */ +@RestController +public class SsoServerController { + + /** + * SSO-Server端:处理所有SSO相关请求 + * http://{host}:{port}/sso/auth -- 单点登录授权地址 + * http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd + * http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开) + */ + @RequestMapping("/sso/*") + public Object ssoRequest() { + return SaSsoServerProcessor.instance.dister(); + } + + /** + * 配置SSO相关参数 + */ + @Autowired + private void configSso(SaSsoServerTemplate ssoServerTemplate) { + // 配置:未登录时返回的View + ssoServerTemplate.strategy.notLoginView = () -> { + // 简化模拟表单 + String doLoginCode = + "fetch(`/sso/doLogin?name=${document.querySelector('#name').value}&pwd=${document.querySelector('#pwd').value}`) " + + " .then(res => res.json()) " + + " .then(res => { if(res.code === 200) { location.reload() } else { alert(res.msg) } } )"; + String res = + "

当前客户端在 SSO-Server 认证中心尚未登录,请先登录

" + + "用户:
" + + "密码:
" + + ""; + return res; + }; + + // 配置:登录处理函数 + ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { + // 此处仅做模拟登录,真实环境应该查询数据库进行登录 + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); + } + return SaResult.error("登录失败!"); + }; + } + +} +``` + +注意:在`doLoginHandle`函数里如果要获取 name, pwd 以外的参数,可通过`SaHolder.getRequest().getParam("xxx")`来获取。 + + +全局异常处理: +``` java +@RestControllerAdvice +public class GlobalExceptionHandler { + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } +} +``` + + +### 3、application.yml配置 + + + + +``` yml +# 端口 +server: + port: 9000 + +# Sa-Token 配置 +sa-token: + # 打印操作日志 + is-log: true + + # SSO-模式一相关配置 (非模式一不需要配置) + # cookie: + # 配置 Cookie 作用域 + # domain: stp.com + + # SSO-Server 配置 + sso-server: + # Ticket有效期 (单位: 秒),默认五分钟 + ticket-timeout: 300 + # 应用列表:配置接入的应用信息 + clients: + # 应用 sso-client1:采用模式一对接 (同域、同Redis) + sso-client1: + client: sso-client1 + allow-url: "*" + # 应用 sso-client2:采用模式二对接 (跨域、同Redis) + sso-client2: + client: sso-client2 + allow-url: "*" + secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用 sso-client3:采用模式三对接 (跨域、跨Redis) + sso-client3: + # 应用名称 + client: sso-client3 + # 允许授权地址 + allow-url: "*" + # 是否接收消息推送 + is-push: true + # 消息推送地址 + push-url: http://sa-sso-client1.com:9003/sso/pushC + # 接口调用秘钥 (如果不配置则使用全局默认秘钥) + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +spring: + # Redis配置 (SSO模式一和模式二使用Redis来同步会话) + redis: + # Redis数据库索引(默认为0) + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: +``` + +``` properties +# 端口 +server.port=9000 + +################## Sa-Token 配置 ################## +# 打印操作日志 +sa-token.is-log=true + +# SSO-模式一相关配置 (非模式一不需要配置) +# 配置 Cookie 作用域 +# sa-token.cookie.domain=stp.com + +# SSO-Server 配置 +# Ticket有效期 (单位: 秒),默认五分钟 +sa-token.sso-server.ticket-timeout=300 + +# 应用列表:配置接入的应用信息 +# 应用 sso-client1:采用模式一对接 (同域、同Redis) +sa-token.sso-server.clients.sso-client1.client=sso-client1 +sa-token.sso-server.clients.sso-client1.allow-url=* + +# 应用 sso-client2:采用模式二对接 (跨域、同Redis) +sa-token.sso-server.clients.sso-client2.client=sso-client2 +sa-token.sso-server.clients.sso-client2.allow-url=* +sa-token.sso-server.clients.sso-client2.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# 应用 sso-client3:采用模式三对接 (跨域、跨Redis) +# 应用名称 +sa-token.sso-server.clients.sso-client3.client=sso-client3 +# 允许授权地址 +sa-token.sso-server.clients.sso-client3.allow-url=* +# 是否接收消息推送 +sa-token.sso-server.clients.sso-client3.is-push=true +# 消息推送地址 +sa-token.sso-server.clients.sso-client3.push-url=http://sa-sso-client1.com:9003/sso/pushC +# 接口调用秘钥 (如果不配置则使用全局默认秘钥) +sa-token.sso-server.clients.sso-client3.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# Redis配置 (SSO模式一和模式二使用Redis来同步会话) +# Redis数据库索引(默认为0) +spring.redis.database=1 +# Redis服务器地址 +spring.redis.host=127.0.0.1 +# Redis服务器连接端口 +spring.redis.port=6379 +# Redis服务器连接密码(默认为空) +spring.redis.password= +``` + + + +注意点:`sa-token.sso-server.clients.xxx.allow-url`为了方便测试配置为`*`,线上生产环境一定要配置为详细 URL 地址 (之后的章节我们会详细阐述此配置项) + + +### 4、创建启动类 +``` java +@SpringBootApplication +public class SaSsoServerApplication { + public static void main(String[] args) { + SpringApplication.run(SaSsoServerApplication.class, args); + + System.out.println(); + System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------"); + System.out.println("配置信息:" + SaSsoManager.getServerConfig()); + System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth"); + System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); + System.out.println(); + } +} +``` + +启动项目,不出意外的情况下我们将看到如下输出: + +sso-server-start + +访问统一授权地址(仅测试 SSO-Server 是否部署成功,暂时还不需要点击登录): +- [http://localhost:9000/sso/auth](http://localhost:9000/sso/auth) + +sso-server-init-login.png + +可以看到这个页面目前非常简陋,这是因为我们以上的代码示例,主要目标是为了带大家从零搭建一个可用的SSO认证服务端,所以就对一些不太必要的步骤做了简化。 + +大家可以下载运行一下官方仓库里的示例`/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`,里面有制作好的登录页面: + +sso-server-init-login2.png + +默认账号密码为:`sa / 123456`,先别着急点击登录,因为我们还没有搭建对应的 Client 端项目, +真实项目中我们一般不会直接从浏览器访问 `/sso/auth` 授权地址的,我们需要在 Client 端点击登录按钮重定向而来。 + + +--- + +现在我们先来看看除了 `/sso/auth` 统一授权地址,这个 SSO-Server 认证中心还开放了哪些API:[SSO-Server 认证中心开放接口](/sso/sso-apidoc)。 + + + + + + + diff --git a/sa-token-doc/sso/sso-type1.md b/sa-token-doc/sso/sso-type1.md new file mode 100644 index 000000000..69e2b0a38 --- /dev/null +++ b/sa-token-doc/sso/sso-type1.md @@ -0,0 +1,287 @@ +# SSO模式一 共享Cookie同步会话 + +如果我们的多个系统可以做到:前端同域、后端同Redis,那么便可以使用 **`[共享Cookie同步会话]`** 的方式做到单点登录。 + +--- + +### 1、设计思路 + +首先我们分析一下多个系统之间,为什么无法同步登录状态? +1. 前端的 `Token` 无法在多个系统下共享。 +2. 后端的 `Session` 无法在多个系统间共享。 + +所以单点登录第一招,就是对症下药: +1. 使用 `共享Cookie` 来解决 Token 共享问题。 +2. 使用 `Redis` 来解决 Session 共享问题。 + +所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名`stp.com`下的Cookie,在`s1.stp.com`、`s2.stp.com`等子域名都是可以共享访问的。 + +而共享Redis,并不需要我们把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **[权限缓存与业务缓存分离]** 的解决方案,详情戳:[Alone独立Redis插件](/plugin/alone-redis)。 + + + + + +OK,所有理论就绪,下面开始实战: + + +### 2、准备工作 + +首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: +``` url +127.0.0.1 sso.stp.com +127.0.0.1 s1.stp.com +127.0.0.1 s2.stp.com +127.0.0.1 s3.stp.com +``` + +其中:`sso.stp.com`为统一认证中心地址,当用户在其它 Client 端发起登录请求时,均将其重定向至认证中心,待到登录成功之后再原路返回到 Client 端。 + +[Some Name](../include/include-qa.md#hostsInvalid ':include') + + +### 3、指定Cookie的作用域 +在`sso.stp.com`访问服务器,其Cookie也只能写入到`sso.stp.com`下,为了将Cookie写入到其父级域名`stp.com`下,我们需要更改 SSO-Server 端的 yml 配置: + + + +``` yaml +sa-token: + cookie: + # 配置 Cookie 作用域 + domain: stp.com +``` + +``` properties +# 配置 Cookie 作用域 +sa-token.cookie.domain=stp.com +``` + + +**这个配置原本是被注释掉的,现在将其打开。**另外我们格外需要注意: +在SSO模式一测试完毕之后,一定要将这个配置再次注释掉,因为模式一与模式二三使用不同的授权流程,这行配置会影响到我们模式二和模式三的正常运行。 + + + + +### 4、搭建 Client 端项目 + +> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/`,如遇到难点可结合源码进行测试学习。[Sa-Token 集成示例大全下载](/more/download-demos) 。 + + +#### 4.1、引入依赖 +新建项目 sa-token-demo-sso1-client,并添加以下依赖: + + + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + + + + cn.dev33 + sa-token-sso + ${sa.top.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-alone-redis + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 权限认证,在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' + +// Sa-Token 插件:整合SSO +implementation 'cn.dev33:sa-token-sso:${sa.top.version}' + +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' + +// Sa-Token插件:权限缓存与业务缓存分离 +implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' +``` + + + +#### 4.2、新建 Controller 控制器 + +``` java +/** + * Sa-Token-SSO Client端 Controller + * @author click33 + */ +@RestController +public class SsoClientController { + + // SSO-Client端:首页 + @RequestMapping("/") + public String index(HttpServletRequest request) { + String url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), request.getQueryString()) ); + SaSsoClientConfig cfg = SaSsoManager.getClientConfig(); + + String str = "

Sa-Token SSO-Client 应用端 (模式一)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "注销 " + + "

"; + return str; + } + + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} +``` + +#### 4.3、application.yml 配置 + + + +``` yaml +# 端口 +server: + port: 9001 + +# Sa-Token 配置 +sa-token: + # SSO-相关配置 + sso-client: + # client 标识 + client: sso-client1 + # SSO-Server端主机地址 + server-url: http://sso.stp.com:9000 + + # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) + # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 + alone-redis: + # Redis数据库索引 + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s +``` + +``` properties +# 端口 +server.port=9001 + +######### Sa-Token 配置 ######### + +# client 标识 +sa-token.sso-client.client=sso-client1 +# SSO-Server端主机地址 +sa-token.sso-client.server-url=http://sso.stp.com:9000 + +# 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) +# 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 +# Redis数据库索引 +sa-token.alone-redis.database=1 +# Redis服务器地址 +sa-token.alone-redis.host=127.0.0.1 +# Redis服务器连接端口 +sa-token.alone-redis.port=6379 +# Redis服务器连接密码(默认为空) +sa-token.alone-redis.password= +# 连接超时时间 +sa-token.alone-redis.timeout=10s +``` + + + +#### 4.4、启动类 + +``` java +/** + * SSO模式一,Client端 Demo + */ +@SpringBootApplication +public class SaSso1ClientApplication { + public static void main(String[] args) { + SpringApplication.run(SaSso1ClientApplication.class, args); + + System.out.println(); + System.out.println("---------------------- 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(); + } +} +``` + + +### 5、访问测试 +启动项目,依次访问三个应用端: +- [http://s1.stp.com:9001/](http://s1.stp.com:9001/) +- [http://s2.stp.com:9001/](http://s2.stp.com:9001/) +- [http://s3.stp.com:9001/](http://s3.stp.com:9001/) + + +均返回: + +sso1--index.png + +然后点击登录,被重定向至SSO认证中心: + +sso1--login-page2.png + +我们登录之后,然后刷新页面: + +sso1-login-ok.png + +刷新另外两个Client端,均显示已登录 + +sso1-login-ok2.png + +测试完成 + + + +### 6、跨域模式下的解决方案 + +如上,我们使用简单的步骤实现了同域下的单点登录,聪明如你😏,马上想到了这种模式有着一个不小的限制: + +> [!TIP| style:callout] +> 所有子系统的域名,必须同属一个父级域名 + +如果我们的子系统在完全不同的域名下,我们又该怎么完成单点登录功能呢? + +且往下看,[SSO模式二:URL重定向传播会话](/sso/sso-type2) + + + + + diff --git a/sa-token-doc/sso/sso-type2.md b/sa-token-doc/sso/sso-type2.md new file mode 100644 index 000000000..8dc16149b --- /dev/null +++ b/sa-token-doc/sso/sso-type2.md @@ -0,0 +1,369 @@ +# SSO模式二 URL重定向传播会话 + +如果我们的多个系统:部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 **`[URL重定向传播会话]`** 的方式做到单点登录。 + + +### 1、设计思路 + +首先我们再次复习一下,多个系统之间为什么无法同步登录状态? + +1. 前端的`Token`无法在多个系统下共享。 +2. 后端的`Session`无法在多个系统间共享。 + +关于第二点,我们已在 "SSO模式一" 章节中阐述,使用 [Alone独立Redis插件](/plugin/alone-redis) 做到权限缓存直连 SSO-Redis 数据中心,在此不再赘述。 + +而第一点,才是我们解决问题的关键所在,在跨域模式下,意味着 "共享Cookie方案" 的失效,我们必须采用一种新的方案来传递Token。 + +1. 用户在 子系统 点击 `[登录]` 按钮。 +2. 用户跳转到子系统登录接口 `/sso/login`,并携带 `back参数` 记录初始页面URL。 + - 形如:`http://{sso-client}/sso/login?back=xxx` +3. 子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带`redirect参数`记录子系统的登录页URL。 + - 形如:`http://{sso-server}/sso/auth?redirect=xxx?back=xxx` +4. 用户进入了 SSO认证中心 的登录页面,开始登录。 +5. 用户 输入账号密码 并 登录成功,SSO认证中心再次将用户重定向至子系统的登录接口`/sso/login`,并携带`ticket码`参数。 + - 形如:`http://{sso-client}/sso/login?back=xxx&ticket=xxxxxxxxx` +6. 子系统根据 `ticket码` 从 `SSO-Redis` 中获取账号id,并在子系统登录此账号会话。 +7. 子系统将用户再次重定向至最初始的 `back` 页面。 + +整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击`[登录]`按钮,由于此用户在SSO认证中心已有会话存在, +所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。 + + + + + +下面我们按照步骤依次完成上述过程: + +### 2、准备工作 +首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: +``` url +127.0.0.1 sa-sso-server.com +127.0.0.1 sa-sso-client1.com +127.0.0.1 sa-sso-client2.com +127.0.0.1 sa-sso-client3.com +``` + +[Some Name](../include/include-qa.md#hostsInvalid ':include') + + +### 3、搭建 Client 端项目 + +> [!TIP| label:demo | style:callout] +> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/`,如遇到难点可结合源码进行测试学习。[Sa-Token 集成示例大全下载](/more/download-demos) 。 + +#### 3.1、去除 SSO-Server 的 Cookie 作用域配置 +在SSO模式一章节中我们打开了配置: + + +``` yaml +sa-token: + cookie: + # 配置 Cookie 作用域 + domain: stp.com +``` + +``` properties +# 配置 Cookie 作用域 +sa-token.cookie.domain=stp.com +``` + + +此为模式一专属配置,现在我们将其注释掉**(一定要注释掉!)** + + +#### 3.2、创建 SSO-Client 端项目 +创建一个 SpringBoot 项目 `sa-token-demo-sso2-client`,引入依赖: + + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + + + + cn.dev33 + sa-token-sso + ${sa.top.version} + + + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-alone-redis + ${sa.top.version} + + + + + cn.dev33 + sa-token-forest + ${sa.top.version} + +``` + +``` gradle +// Sa-Token 权限认证,在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' + +// Sa-Token 插件:整合SSO +implementation 'cn.dev33:sa-token-sso:${sa.top.version}' + +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' +implementation 'org.apache.commons:commons-pool2' + +// Sa-Token插件:权限缓存与业务缓存分离 +implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' + +// Sa-Token插件:整合 Forest 请求工具 +implementation 'cn.dev33:sa-token-forest:${sa.top.version}' +``` + + + +#### 3.3、创建 SSO-Client 端认证接口 + +同 SSO-Server 一样,Sa-Token 为 SSO-Client 端所需代码也提供了完整的封装,你只需提供一个访问入口,接入 Sa-Token 的方法即可。 + +``` java + +/** + * Sa-Token-SSO Client端 Controller + */ +@RestController +public class SsoClientController { + + // 首页 + @RequestMapping("/") + public String index() { + String str = "

Sa-Token SSO-Client 应用端 (模式二)

" + + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + + "

" + + "登录 - " + + "单应用注销 - " + + "全端注销 - " + + "账号资料" + + "

"; + return str; + } + + /* + * SSO-Client端:处理所有SSO相关请求 + * http://{host}:{port}/sso/login -- Client 端登录地址 + * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) + * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 + */ + @RequestMapping("/sso/*") + public Object ssoRequest() { + return SaSsoClientProcessor.instance.dister(); + } + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + } + + // 当前应用独自注销 (不退出其它应用) + @RequestMapping("/sso/logoutByAlone") + public Object logoutByAlone() { + StpUtil.logout(); + return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); + } + +} +``` + +全局异常处理: +``` java +@RestControllerAdvice +public class GlobalExceptionHandler { + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } +} +``` + + +##### 3.4、配置SSO认证中心地址 +你需要在 `application.yml` 配置如下信息: + + + +``` yaml +# 端口 +server: + port: 9002 + +# sa-token配置 +sa-token: + # 打印操作日志 + is-log: true + # SSO-相关配置 + sso-client: + # 应用标识 + client: sso-client2 + # SSO-Server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # API 接口调用秘钥 (单点注销时会用到) + secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + + # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) + # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 + alone-redis: + # Redis数据库索引 (默认为0) + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s +``` + +``` properties +# 端口 +server.port=9002 + +######### Sa-Token 配置 ######### +# 打印操作日志 +sa-token.is-log=true +# 应用标识 +sa-token.sso-client.client=sso-client2 +# SSO-Server端 统一认证地址 +sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# API 接口调用秘钥 (单点注销时会用到) +sa-token.sso-client.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) +# 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 +# Redis数据库索引 +sa-token.alone-redis.database=1 +# Redis服务器地址 +sa-token.alone-redis.host=127.0.0.1 +# Redis服务器连接端口 +sa-token.alone-redis.port=6379 +# Redis服务器连接密码(默认为空) +sa-token.alone-redis.password= +# 连接超时时间 +sa-token.alone-redis.timeout=10s +``` + + + +注意点:`sa-token.alone-redis` 的配置需要和SSO-Server端连接同一个Redis**(database 值也要一样!database 值也要一样!database 值也要一样!重说三!)** + +#### 3.5、写启动类 +``` java +@SpringBootApplication +public class SaSso2ClientApplication { + public static void main(String[] args) { + SpringApplication.run(SaSso2ClientApplication.class, args); + + System.out.println(); + System.out.println("---------------------- 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(); + } +} +``` +启动项目 + + +### 4、测试访问 + +(1) 依次启动 `SSO-Server` 与 `SSO-Client`,然后从浏览器访问:[http://sa-sso-client1.com:9002/](http://sa-sso-client1.com:9002/) + +(注:先前版本文档测试demo端口号为9001,后为了方便区分三种模式改为了9002,因此出现文字描述与截图端口号不一致情况,请注意甄别,后不再赘述) + +sso-client-index.png + +(2) 首次打开,提示当前未登录,我们点击 **`登录`** 按钮,页面会被重定向到登录中心 + +sso-server-auth.png + +(3) SSO-Server提示我们在认证中心尚未登录,我们点击 **`登录`** 按钮进行模拟登录 + + + +(4) SSO-Server认证中心登录成功,系统重定向回 client + +sso-client-index-ok.png + +(5) 页面被重定向至`Client`端首页,并提示登录成功,至此,`Client1`应用已单点登录成功! + +(6) 我们再次访问`Client2`:[http://sa-sso-client2.com:9002/](http://sa-sso-client2.com:9002/) + +sso-client2-index.png + +(7) 提示未登录,我们点击 **`登录`** 按钮,会直接提示登录成功 + +sso-client2-index-ok.png + +(8) 同样的方式,我们打开`Client3`,也可以直接登录成功:[http://sa-sso-client3.com:9002/](http://sa-sso-client3.com:9002/) + +sso-client3-index-ok.png + +至此,测试完毕! + +可以看出,除了在`Client1`端我们需要手动登录一次之外,在`Client2端`和`Client3端`都是可以无需再次认证,直接登录成功的。 + +我们可以通过 F12控制台 Network 跟踪整个过程 + +sso-genzong + + + + + +### 5、跨 Redis 的单点登录 +以上流程解决了跨域模式下的单点登录,但是后端仍然采用了共享Redis来同步会话,如果我们的架构设计中Client端与Server端无法共享Redis,又该怎么完成单点登录? + +这就要采用模式三了,且往下看:[SSO模式三:Http请求获取会话](/sso/sso-type3) + + + + + + + + + diff --git a/sa-token-doc/sso/sso-type3.md b/sa-token-doc/sso/sso-type3.md new file mode 100644 index 000000000..4511f990b --- /dev/null +++ b/sa-token-doc/sso/sso-type3.md @@ -0,0 +1,311 @@ +# SSO模式三 Http请求获取会话 + +如果既无法做到前端同域,也无法做到后端同Redis,那么可以使用模式三完成单点登录 + +> [!WARNING| label:小提示] +> 阅读本篇之前请务必先熟读SSO模式二!因为模式三仅仅属于模式二的一个特殊场景,熟读模式二有助于您快速理解本章内容 + + +### 1、问题分析 +我们先来分析一下,当后端不使用共享 Redis 时,会对架构产生哪些影响: + +1. sso-client 端无法直连 Redis 校验 ticket,取出账号id。 +2. sso-client 端无法与 sso-server 端共用一套会话,需要自行维护子会话。 +3. 由于不是一套会话,所以无法“一次注销,全端下线”,需要额外编写代码完成单点注销。 + +所以模式三的主要目标:也就是在 模式二的基础上 解决上述 三个难题 + +> [!TIP| label:demo | style:callout] +> 模式三的 Demo 示例地址:`/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/` +> ,如遇难点可参考示例。[Sa-Token 集成示例大全下载](/more/download-demos) 。 + + +### 2、在Client 端更改 Ticket 校验方式 + +如果想要更直观的感受模式二与模式三的差距,可以把前面章节创建的模式二 demo 代码复制一份,在新复制的项目上继续更改来测试模式三。 + +#### 2.1、去除 Alone-Redis 依赖 + +模式三要求 sso-client 与 sso-server 连接不同的 redis,所以此处没有必要再引入 sa-token-alone-redis 机制,可以去除相关依赖: + + + +``` xml + + + cn.dev33 + sa-token-alone-redis + ${sa.top.version} + +``` + +``` gradle +// Sa-Token插件:权限缓存与业务缓存分离 +implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' +``` + + + +#### 2.2、SSO-Client 端更改配置 + +更改 `application.yml` : + + + +``` yaml +# 端口 +server: + port: 9003 + +# sa-token配置 +sa-token: + # 打印操作日志 + is-log: true + + # sso-client 相关配置 + sso-client: + # 应用标识 + client: sso-client3 + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 使用 Http 请求校验 ticket (模式三) + is-http: true + # API 接口调用秘钥 + secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +spring: + # 配置 Redis 连接 (此处与 SSO-Server 端连接不同的 Redis) + redis: + # Redis数据库索引 + database: 3 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s +``` + +``` properties +# 端口 +server.port=9003 + +# sa-token配置 + +# 打印操作日志 +sa-token.is-log=true + +# sso-client 相关配置 +# 应用标识 +sa-token.sso-client.client=sso-client3 +# sso-server 端主机地址 +sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# 使用 Http 请求校验 ticket (模式三) +sa-token.sso-client.is-http=true +# API 接口调用秘钥 +sa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# 配置 Redis 连接 (此处与 SSO-Server 端连接不同的 Redis) +# Redis数据库索引 +spring.redis.database=3 +# Redis服务器地址 +spring.redis.host=127.0.0.1 +# Redis服务器连接端口 +spring.redis.port=6379 +# Redis服务器连接密码(默认为空) +spring.redis.password= +# 连接超时时间 +spring.redis.timeout=10s +``` + + + + + +#### 2.3、测试 + +重启项目,访问测试: +- [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/) +- [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/) +- [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/) + +> [!WARNING| label:小提示] +> 注:如果已测试运行模式二,可先将Redis中的数据清空,以防旧数据对测试造成干扰 + +测试步骤同模式二,不再赘述。 + + + + + + +### 3、后记 +当我们熟读三种模式的单点登录之后,其实不难发现:所谓单点登录,其本质就是多个系统之间的会话共享。 + +当我们理解这一点之后,三种模式的工作原理也浮出水面: + +- 模式一:采用共享 Cookie 来做到前端 Token 的共享,从而达到后端的 Session 会话共享。 +- 模式二:采用 URL 重定向,以 ticket 码为授权中介,做到多个系统间的会话传播。 +- 模式三:采用 Http 请求主动查询会话,做到 Client 端与 Server 端的会话同步。 + + + + diff --git a/sa-token-doc/sso/user-data-sync.md b/sa-token-doc/sso/user-data-sync.md new file mode 100644 index 000000000..62584994c --- /dev/null +++ b/sa-token-doc/sso/user-data-sync.md @@ -0,0 +1,305 @@ +# 用户数据同步 / 迁移 + +本篇文章仅提供架构设计的略微参考,真实场景中每个公司的架构设计都是千差万别的,一套设计理论未必能够适应所有公司的项目。 +所以如果你觉着本篇文章的设计理念不能契合你公司的需求,请以你公司的原设计为准。 + +--- + +### 数据同步需求 + +在前面的不同架构 SSO 对接示例中,我们均假设了一个前提: + +-- _所有的 sso-client 只负责业务操作,不存储 user 数据,user 数据全部来源于 sso-server,包括登录认证也都是基于 sso-server 里的 user 账号进行校验操作。_ + +这种架构比较简洁、清晰,是一种理想化的 SSO 架构模型。 + +然而更多时候,我们遇到的实际情况是: + +-- _公司已经有了 N 多个系统,每个系统都有自己独立的一套账号认证体系,现在老板要让这 N 个毫无关系的系统集成单点登录。_ + +要完成这种需求,首先你得考虑两个问题: +1. 问题一:sso-client 需不需要保留 user 数据。 + - sso-client 不涉及 user 信息连表查的业务,就可以不保留 user 信息。 + - sso-client 涉及 user 信息连表查业务,就需要在 sso-client 保留 user 数据。 +2. 问题二:如果保留的话,是和 sso-server 强同步,还是弱同步。 + - 强同步就是指 sso-client 的 user 数据和 sso-server 的 user 数据,字段值必须保持一致。比如说:一个用户在server端昵称修改为“张三”,那么在 client 端也要实时同步修改。 + - 弱同步就是指两边可以各改各的。比如说:一个用户 server 端修改了昵称为 “张三”,他在 client 端依然可以昵称为 “李四”。 + + +由此可大致分为三种设计方案: + +| 方案序号 | 方案名称 | 简单说明 | 适用系统 | +| :-------- | :-------- | :-------- | :-------- | +| 方案一 | 统一迁移 | 统一把用户数据迁移到 sso-server 认证中心再进行对接 | 比较简单的系统,业务上不需要 user 信息连表查 | +| 方案二 | 实时同步 | 按照一定的规则,使 sso-client 和 sso-server 保持 user 信息实时同步 | 一般业务上需要 user 信息连表查的系统 | +| 方案三 | 字段关联 | 不同步,但找一个关键字段,将 sso-client 和 sso-server 的 user 账号进行关联起来 | sso-client 不打算过分依赖 sso-server 的 user 数据,只是想借助 sso-server 完成一下统一登录 | + +下面逐一拆解三种方案具体实现。 + + +### 1、方案一:统一迁移 + +对接工作开发前,sso-client 的 user 数据完全迁移到 sso-server 中,且自身不再保留 user 数据,只进行业务数据处理操作。 + +这种方案其实不必过多讲解,因为数据完成迁移后整个架构就转化为了上述的“理想化SSO模型”,后续对接也比较方便。 +迁移方式可以选择数据库同步工具,或者手写代码从 sso-client 库读取数据然后 insert 到 sso-server 库中。 +这并非此文探讨的重点,因此不再过多赘述了。 + +- 方案优点:架构简洁明了,SSO 登录、注销对接起来非常方便 +- 方案缺点:sso-client 不存储 user 信息,因此业务上需要连表查询 user 信息的地方会比较麻烦(例如:拉取帖子列表时需要附加显示用户头像和昵称信息) + +方案适用范围:适合业务比较简单,不涉及 user资料连表查业务 的子系统。 + + +### 2、方案二:实时同步 + +首先,对接前,数据还是要迁移的,只不过迁移后 sso-client 的 user 数据不删除掉,依然保留。 + +然后在项目运行阶段,每当 sso-server 的 user 数据发生变动时(增删改),逐一向每个 sso-client 推送变化信息。使 sso-client 与 sso-server 的 user 数据保持强同步。 + +你可能会有疑问,那 sso-client 的 user 数据发生变动时,要不要向 sso-server 推送信息,我的建议是:尽量不要让 sso-client 的 user 信息主动发生变化。 + +举个例子: + +> 公司有电商、论坛、短视频 3 个子系统 + 1 个 sso-server 认证中心,无论用户从哪个子系统点击 “修改我的资料” 按钮时,都应该统一跳转到 sso-server 认证中心进行修改, +> 修改完毕后再由 sso-server 将 user 信息推送至 3 个子系统。以此来保证 4 个系统间的 user 信息同步。 + +- 方案优点:sso-client 存储了 user 信息,可以比较方便的进行 user 连表查操作。 +- 方案缺点:sso-server 与 sso-client 的 user 数据同步功能不算简单,开发起来可能要耗费一段不小的工期。 + +方案适用范围:一般业务上需要 user 信息连表查的子系统都适合。 + + +### 3、方案三:字段关联 + +如果子系统不需要和 sso-server 做到信息强同步,可以使用字段关联法做到账户关联进行登录。 + +举个例子:公司有三个子系统,电商、论坛、短视频。同一个用户可以在这三个子系统以及 sso-server 认证中心拥有不同的昵称、头像等信息,互不干扰。 + +例如,在 sso-server 认证中心里,张三的数据库信息为: + +| id | username | avatar | password | age | email | +| :-------- | :-------- | :-------- | :-------- | :-------- | :-------- | +| 10001 | ... | ... | ... | ... | ... | +| 10002 | 小明 | cat.jpg | 123456 | 18 | `23397@xx.com` | +| 10003 | ... | ... | ... | ... | ... | + +在电商系统里中,张三的数据库信息为: + +| id | name | avatar | money | email | +| :-------- | :-------- | :-------- | :-------- | :-------- | +| 100334 | ... | ... | ... | ... | +| 100335 | 二明 | dog.jpg | 1000 | `23397@xx.com` | +| 100336 | ... | ... | ... | ... | + +这里的关键点在于,虽然用户 “张三” 在每个系统里的资料都是不同的,但是程序要想办法将它们识别为同一个用户, +要做到这一点,就需要我们准备一个关键字段将信息打通串联起来。例如表中的 “邮箱” 信息可以作为这个“关联字段”。 + +(注:此处仅展示使用邮箱作为关联字段的操作,实际上除了邮箱以外,手机号、身份证号等具有唯一性的信息都可以作为关联字段) + +首先,在 sso-server 端,我们需要重写一下 `checkTicketAppendData` 函数,使其在 “校验 ticket 返回 loginId” 时,追加返回 email 字段。 + +``` java +// 配置SSO相关参数 +@Autowired +private void configSso(SaSsoServerTemplate ssoServerTemplate) { + + // 其它配置 ... + + // 配置:Ticket校验函数 + ssoServerTemplate.strategy.checkTicketAppendData = (loginId, result) -> { + System.out.println("-------- 追加返回信息到 sso-client --------"); + + // 在校验 ticket 后,给 sso-client 端追加返回信息的函数 + SysUser user = sysUserMapper.getById(loginId); + result.set("email", user.getEmail()); + // result.set("user", user); // 你也可以将整个user 对象的信息都返回到 sso-client,自由决定 + + return result; + }; + +} +``` + +在 sso-client 端,重写 ticketResultHandle 函数,根据 sso-server 返回的信息查询本地 user 信息并登录: + +``` java +// 配置SSO相关参数 +@Autowired +private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + // 其它配置 ... + + // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) + ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { + System.out.println("--------- 自定义 ticket 校验结果处理函数 ---------"); + System.out.println("此账号在 sso-server 的 userId:" + ctr.loginId); + System.out.println("此账号在 sso-server 会话剩余有效期:" + ctr.remainSessionTimeout + " 秒"); + System.out.println("此账号返回的 email 信息:" + ctr.result.get("email")); + + // 模拟代码: + // 根据 email 字段找到此账号在本系统对应的 user 信息 + String email = (String) ctr.result.get("email"); + SysUser user = sysUserMapper.getByEmail(email); + + // 如果找不到,说明是首次登录本系统的新用户,需要自动注册一个新账号给他 + if(user == null) { + // 涉及到数据库操作,此处仅做模拟代码 + // 1、构建 user 信息 + // 2、插入到数据库 + // 3、查询出最新刚插入的这条 user 信息 + user = sysUserMapper.getByEmail(email); + } + + // 进行登录 + StpUtil.login(user.getId(), ctr.remainSessionTimeout); + StpUtil.getSession().set("user", user); + + // 一切工作完毕,重定向回 back 页面 + return SaHolder.getResponse().redirect(back); + }; + +} +``` + +至此完毕。 + +- 方案优点: + - 1、sso-client 不需要和 sso-server 保持信息强同步,实现起来不复杂,架构也比较清晰易维护。 + - 2、同一个用户的信息,sso-client 可以和 sso-client 保持不同,各自维护各自的,互不干扰。 +- 方案缺点:好像没啥缺点,除非你觉着上述的第2条优点属于缺点。 + +方案适用范围:在 user 信息方面不打算过分依赖 sso-server 的系统,希望自己维护自己的 user 信息,只是想借助 sso-server 完成一下统一登录。 + + +### 4、扩展:没有关联字段 + +如果我们的子系统 user 表没有邮箱、手机号等唯一性字段和 sso-server 的 user 表进行关联,该怎么办呢? + +没有字段,那就创造个字段,例如: + +| id | name | avatar | age | center_id | +| :-------- | :-------- | :-------- | :-------- | :-------- | +| 205421 | ... | ... | ... | ... | +| 205422 | 小风筝 | dog.jpg | 21 | 10002 | +| 205423 | ... | ... | ... | ... | + +如上表所示,我们可以在子系统的 user 表新增一列 `center_id`,记录这个用户在认证中心所属的账号id。然后在登录时根据这个 `center_id` 来查找相应的用户。 + +由于 sso-server 端默认就是会返回 loginId 参数的,因此在 sso-server 端不必再重写一下 `checkTicketAppendData` 函数来追加返回信息了, +我们只需要重写 sso-client 端的 `ticketResultHandle` 函数即可: + +``` java +// 配置SSO相关参数 +@Autowired +private void configSso(SaSsoClientTemplate ssoClientTemplate) { + + // 其它配置 ... + + // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) + ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { + System.out.println("--------- 自定义 ticket 校验结果处理函数 ---------"); + System.out.println("此账号在 sso-server 的 userId:" + ctr.loginId); + System.out.println("此账号在 sso-server 会话剩余有效期:" + ctr.remainSessionTimeout + " 秒"); + + // 模拟代码: + // 根据 center_id 字段找到此账号在本系统对应的 user 信息 + long centerId = SaFoxUtil.getValueByType(ctr.loginId, long.class); + SysUser user = sysUserMapper.getByCenterId(centerId); + + // 如果找不到,说明是首次登录本系统的新用户,需要自动注册一个新账号给他 + if(user == null) { + // 涉及到数据库操作,此处仅做模拟 + // 1、构建 user 信息 + // 2、插入到数据库 + // 3、查询出最新刚插入的这条 user 信息 + user = sysUserMapper.getByCenterId(userId); + } + + // 进行登录 + // 注意此处需要使用 centerId 进行登录,否则该账号将无法正常完成单点注销功能 + StpUtil.login(centerId, ctr.remainSessionTimeout); + StpUtil.getSession().set("user", user); + + // 一切工作完毕,重定向回 back 页面 + return SaHolder.getResponse().redirect(back); + }; + +} +``` + +至此完毕。 + + +> [!INFO| label:提问:按照方案三,一个用户登录过程中,sso-server 和 sso-client 对这个用户账号的完整处理步骤是怎样的?] +> 1. 用户进入 sso-client 登录页面,点击上面的 [ 使用 xx 认证中心快捷登录 ] 按钮,浏览器跳转至 sso-server 认证中心。 +> 2. 如果用户在 sso-server 有账号,则直接登录,如果没有,则注册账号并登录。 +> 3. sso-server 重定向回 sso-client 端,并携带 ticket 参数。 +> 4. sso-client 获取 ticket 参数,并解析出 center_id 值。 +> 5. 根据 center_id 从 user 表查数据: +> - 5.1 查的到,证明有账号,直接登录。 +> - 5.2 查不到,证明无账号,程序自动给他添加一条 user 账号,并登录。 +> 6. 登录完成。 + + + +### 5、解决模式三下,loginId 与 centerId 不一致的问题 + +按照字段关联法登录之后,如果一个用户在本地应用端的 userId 和认证中心端的 userId 不一致,则可能发生单点注销失败的情况: + +假设,一个用户在认证中心的 userId=10002,在本地应用端的 userId=100335, +则在本地应用端发起单点注销时,其传递的 loginId 值是 100335,在 sso-server 是找不到 userId=100335 用户的,自然无法单点注销成功。 + +解决方案是在本地应用端重写 loginId 与 centerId 转换策略函数,做到本地应用 userId 与认证中心 userId 的互相映射: + +``` java +@RestController +public class SsoClientController { + + // 配置SSO相关参数 + @Autowired + private void configSso(SaSsoClientTemplate ssoClientTemplate) { + // 重写 loginId 与 centerId 转换策略函数,做到本地应用 userId 与认证中心 userId 的互相映射 + + // 将 centerId 转换为 loginId 的函数 + ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> { + return "Stu" + centerId; + }; + // 将 loginId 转换为 centerId 的函数 + ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> { + return loginId.toString().substring(3); + }; + } + +} +``` + +如上代码,演示了应用本地 loginId 与认证中心 centerId 不一致时的转换写法(演示的逻辑为添加和裁剪指定前缀),真实项目中,应该根据用户表存储的映射关系来做查询返回。 + +值得注意的是,在重写转换策略后,我们在消息推送时也应该严格按照转换写法提交 loginId 参数,例如: + +``` java +// 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 +@RequestMapping("/sso/myInfo") +public Object myInfo() { + // 如果尚未登录 + if( ! StpUtil.isLogin()) { + return "尚未登录,无法获取"; + } + + // 原写法:直接调用 StpUtil.getLoginId() 当做 centerId 来提交 + // Object centerId = StpUtil.getLoginId(); + + // 新写法:获取本地 loginId 对应的认证中心 centerId + Object centerId = SaSsoClientUtil.getSsoTemplate().strategy.convertLoginIdToCenterId.run(StpUtil.getLoginId()); + + // 推送消息 + SaSsoMessage message = new SaSsoMessage(); + message.setType("userinfo"); + message.set("loginId", centerId); + SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); + + // 返回给前端 + return result; +} +``` \ No newline at end of file diff --git a/sa-token-doc/start/download.md b/sa-token-doc/start/download.md new file mode 100644 index 000000000..cd6c30226 --- /dev/null +++ b/sa-token-doc/start/download.md @@ -0,0 +1,208 @@ +# 其它环境引入 Sa-Token 的示例 + +目前已实现的对接框架综合。 + +如需一次性获取官方仓库内全部可运行示例的压缩包,请见:[Sa-Token 集成示例大全下载](/more/download-demos) 。 + +------ + +## Maven依赖 +根据不同基础框架引入不同的 Sa-Token 依赖: + + + + +如果你使用的框架基于 ServletAPI 构建( SpringMVC、SpringBoot等 ),请引入此包 +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 + + +注:如果你使用的框架基于 Reactor 模型构建(WebFlux、SpringCloud Gateway 等),请引入此包 +``` xml + + + cn.dev33 + sa-token-reactor-spring-boot-starter + ${sa.top.version} + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + +参考:[Solon官网](https://solon.noear.org/) +``` xml + + + cn.dev33 + sa-token-solon-plugin + ${sa.top.version} + +``` + + +参考:[JFinal官网](https://jfinal.com/) +``` xml + + + cn.dev33 + sa-token-jfinal-plugin + ${sa.top.version} + +``` + + +参考:[Jboot官网](http://www.jboot.com.cn/) +``` xml + + + cn.dev33 + sa-token-jboot-plugin + ${sa.top.version} + +``` + + +参考:[LoveQQ-Framework](https://gitee.com/kfyty725/loveqq-framework) +``` xml + + + cn.dev33 + sa-token-loveqq-boot-starter + ${sa.top.version} + +``` + + +参考:[quarkus-sa-token](https://github.com/quarkiverse/quarkus-sa-token) +``` xml + + + io.quarkiverse.satoken + quarkus-satoken-resteasy + 1.30.0 + +``` + + +注:如果你的项目没有使用Spring,但是Web框架是基于 ServletAPI 规范的,可以引入此包 +``` xml + + + cn.dev33 + sa-token-servlet + ${sa.top.version} + +``` +引入此依赖需要自定义 SaTokenContext 实现,参考:[自定义 SaTokenContext 指南](/fun/sa-token-context) + + +注:如果你的项目既没有使用 SpringMVC、WebFlux,也不是基于 ServletAPI 规范,那么可以引入core核心包 +``` xml + + + cn.dev33 + sa-token-core + ${sa.top.version} + +``` +引入此依赖需要自定义 SaTokenContext 实现,参考:[自定义 SaTokenContext 指南](/fun/sa-token-context) + + + + +## Gradle依赖 + + +``` gradle +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 + + +``` gradle +implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + +``` gradle +implementation 'cn.dev33:sa-token-solon-plugin:${sa.top.version}' +``` + + +``` gradle +implementation 'cn.dev33:sa-token-jfinal-plugin:${sa.top.version}' +``` + + +``` gradle +implementation 'cn.dev33:sa-token-jboot-plugin:${sa.top.version}' +``` + + +``` gradle +implementation 'cn.dev33:sa-token-loveqq-boot-starter:${sa.top.version}' +``` + + +``` gradle +implementation 'io.quarkiverse.satoken:quarkus-satoken-resteasy:1.30.0' +``` + + +``` gradle +implementation 'cn.dev33:sa-token-servlet:${sa.top.version}' +``` + + +``` gradle +implementation 'cn.dev33:sa-token-core:${sa.top.version}' +``` + + + +注:JDK版本:`v1.8+`,SpringBoot:`建议2.0以上` + + +## 测试版 +更多内测版本了解:[Sa-Token 最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md) + +Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.com/doc.html#/start/maven-pull) + +## jar包下载 + +[点击下载:sa-token-1.6.0.jar](https://pan.quark.cn/s/85e4d75f500c) + +注:当前仅提供 `v1.6.0` 版本jar包下载,更多版本请前往 maven 中央仓库获取,[直达链接](https://search.maven.org/search?q=sa-token) + + +## 获取源码 +如果你想深入了解 Sa-Token,你可以通过 `Gitee`、`GitHub`、`AtomGit` 来获取源码 (**学习测试请拉取 master 分支**,dev为正在开发的分支,有很多特性并不稳定) +- **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) +- **AtomGit**地址:[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token) +- 开源不易,求鼓励,点个`star`吧 +- 源码目录介绍: - [仓库目录](/arch/dir-intro) + + + + + + \ No newline at end of file diff --git a/sa-token-doc/start/example.md b/sa-token-doc/start/example.md new file mode 100644 index 000000000..1152be38e --- /dev/null +++ b/sa-token-doc/start/example.md @@ -0,0 +1,159 @@ +# SpringBoot 集成 Sa-Token 示例 + + +本篇带你从零开始集成 Sa-Token,只需简单 5 步,你就可以快速熟悉框架的使用姿势。 + +整合示例在官方仓库的`/sa-token-demo/sa-token-demo-springboot`文件夹下,如遇到难点可结合源码进行学习测试。[Sa-Token 集成示例大全下载](/more/download-demos) 。 + +--- + +### 1、创建项目 +在 IDE 中新建一个 SpringBoot 项目,例如:`sa-token-demo-springboot`(不会的同学请自行百度或者参考:[SpringBoot-Pure](https://gitee.com/click33/springboot-pure)) + + +### 2、添加依赖 +在项目中添加依赖: + + + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + ${sa.top.version} + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 + + +``` gradle +// Sa-Token 权限认证,在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 + + + +Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.com/doc.html#/start/maven-pull) + +更多内测版本了解:[Sa-Token最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md) + +### 3、设置配置文件 +你可以**零配置启动项目** ,但同时你也可以在 `application.yml` 中增加如下配置,定制性使用框架: + + + + +``` yaml +server: + # 端口 + port: 8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称(同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true +``` + + +``` properties +# 端口 +server.port=8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## + +# token 名称(同时也是 cookie 名称) +sa-token.token-name=satoken +# token 有效期(单位:秒) 默认30天,-1 代表永久有效 +sa-token.timeout=2592000 +# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 +sa-token.active-timeout=-1 +# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) +sa-token.is-concurrent=true +# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) +sa-token.is-share=false +# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) +sa-token.token-style=uuid +# 是否输出操作日志 +sa-token.is-log=true +``` + + + + +### 4、创建启动类 +在项目中新建包 `com.pj` ,在此包内新建主类 `SaTokenDemoApplication.java`,复制以下代码: + +``` java +@SpringBootApplication +public class SaTokenDemoApplication { + public static void main(String[] args) throws JsonProcessingException { + SpringApplication.run(SaTokenDemoApplication.class, args); + System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); + } +} +``` + +### 5、创建测试Controller +``` java +@RestController +@RequestMapping("/user/") +public class UserController { + + // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 + @RequestMapping("doLogin") + public String doLogin(String username, String password) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(username) && "123456".equals(password)) { + StpUtil.login(10001); + return "登录成功"; + } + return "登录失败"; + } + + // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin + @RequestMapping("isLogin") + public String isLogin() { + return "当前会话是否登录:" + StpUtil.isLogin(); + } + +} +``` + +### 6、运行 +启动代码,从浏览器依次访问上述测试接口: + +运行结果 + +运行结果 + + + + +### 出发 +通过这个示例,你已经对 Sa-Token 有了初步的了解。那么,坐稳扶好,让我们开始吧:[登录认证](/use/login-auth) + + + + + + + diff --git a/sa-token-doc/start/maven-pull.md b/sa-token-doc/start/maven-pull.md new file mode 100644 index 000000000..84f674939 --- /dev/null +++ b/sa-token-doc/start/maven-pull.md @@ -0,0 +1,49 @@ +# Maven 依赖一直无法拉取成功? + +--- +方法1、先重启一下试试。 + +--- +方法2、可能依赖还没有下载完毕,请看一下编辑器下方是否有正在构建项目的进度条。 + +--- +方法3、可能是网络不太稳定,导致本地下载了一些残碎文件,先把这些残碎文件删除了,再重新构建项目试试。 + +一般本地的文件都在 `C:\Users\你的电脑用户名\.m2\repository\cn\dev33`,打开后,把文件全部删除。注:如果你修改过 Maven jar 下载目录,就按照你修改的来。 + +--- +方法4、可能你给你的 Maven 配置了阿里云镜像,而部分 jar 包无法通过阿里云镜像加载成功。 + +打开你的 Maven setting.xml 文件,看看有没有以下配置: + +``` xml + + nexus-aliyun + central + Nexus aliyun + http://maven.aliyun.com/nexus/content/groups/public + +``` + +如果有的话,先把它注释掉(注释掉就直连 Maven 中央仓库了),或者修改为其它的镜像,例如腾讯云的: + +``` xml + + tencent + tencent maven + http://mirrors.cloud.tencent.com/nexus/repository/maven-public/ + central + +``` + +然后重启你的代码编辑器,重新构建项目。 + +--- --- +方法5、如果使用的是父子Maven项目,在父项目导入该依赖后,Pom无法识别的情况: + +需要先在子项目中引用该依赖,再进行重新加载。 + +若还是不行,可以新建先一个小的Maven项目尝试将该依赖下载后,再返回原父子项目中将该依赖导入。 + +--- --- +再不行的话,就加群反馈吧。 diff --git a/sa-token-doc/start/new-version.md b/sa-token-doc/start/new-version.md new file mode 100644 index 000000000..d20960ff5 --- /dev/null +++ b/sa-token-doc/start/new-version.md @@ -0,0 +1,32 @@ +# Sa-Token 最新版本 + +在线文档:[https://sa-token.com/](https://sa-token.com/) + +--- + +### 正式版本 +v1.45.0 正式版,可上生产: + +``` xml + + + cn.dev33 + sa-token-spring-boot-starter + 1.45.0 + +``` + +Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.com/doc.html#/start/maven-pull) + +--- + +### 内测版本 + +暂无内测版本。 + + + + + + + diff --git a/sa-token-doc/start/run-source-code.md b/sa-token-doc/start/run-source-code.md new file mode 100644 index 000000000..955ee7d53 --- /dev/null +++ b/sa-token-doc/start/run-source-code.md @@ -0,0 +1,74 @@ +# 源码运行指南 + +如果你想阅读或调试 Sa-Token 源码,可按以下步骤在本地运行框架及 Demo 示例。 + +--- + +### 1、下载源码 + +**方式一:Git 克隆(推荐)** + +Sa-Token 源码目前在 Gitee、GitHub、AtomGit 三个平台进行托管,**你可以从任意其一进行克隆下载源码**。 + +**学习测试建议拉取 `master` 分支**;`dev` 为开发分支,代码随时变动,有时候会运行不起来。(如果你要提交 pr,则需要拉取 dev 分支) + + + +``` shell +git clone -b master https://gitee.com/dromara/sa-token.git +``` + + +``` shell +git clone -b master https://github.com/dromara/sa-token.git +``` + + +``` shell +git clone -b master https://atomgit.com/dromara/sa-token.git +``` + + +**方式二:官网 / 仓库页下载压缩包** + +在 [Gitee](https://gitee.com/dromara/sa-token)、[GitHub](https://github.com/dromara/sa-token)、[AtomGit](https://atomgit.com/dromara/sa-token) 三者任意一个平台,使用「克隆 / 下载」下载源码压缩包,解压后即可使用。 + +--- + +### 2、从 IDEA 导入项目 + +打开 IDEA,选择 **Open**,选中源码根目录,进行打开。 + +源码目录说明请参考:[仓库目录](/arch/dir-intro)。 + +--- + +### 3、运行 + +Sa-Token 并非一个完整可运行的传统项目,而是一个需要被引入的框架。因此不存在传统意义下的运行源码,**你可以运行的是其中各项 demo 示例**。 + +打开 `sa-token-demo` 目录,选择你想要运行的示例添加为 Maven 项目,打开 `XxxApplication.java` 运行。 + +运行示例 + +注意:首次在 IDEA 中运行项目时,可能会触发报错: + +``` +java: 程序包cn.dev33.satoken.apikey不存在 +``` + +这是因为在 Sa-Token 中,`sa-token-apikey`、`sso`、`oauth2`、`sign`、`jwt` 等模块均为可选包,IDEA 有时候会跳过相关模块的编译,然后运行时又找不到这些包,触发报错。 + +**解决方案是先运行一下 `sa-token-demo-first-run` 这个 demo**,该项目强制引入了这些模块,会引导 IDEA 对相关模块进行编译。 + +如果此项目仍然无法运行成功,请根据控制台的报错提示,**把相关模块加入到此项目的 pom.xml 中**,然后点击右上角 maven 刷新依赖图标,然后再次点击运行。 + +如果还不行,那就在项目根目录进入 cmd,执行 `mvn package`,然后重新运行试试。 + +如果还是不行,那就加群反馈一下,我们再根据你的情况寻找一下新方案。 + + +### 4、运行文档 + +根目录双击执行 `preview-doc.bat` 可在本地预览 `sa-token-doc` 文档。 + diff --git a/sa-token-doc/start/solon-example.md b/sa-token-doc/start/solon-example.md new file mode 100644 index 000000000..5dfdc2943 --- /dev/null +++ b/sa-token-doc/start/solon-example.md @@ -0,0 +1,173 @@ +# Solon 集成 Sa-Token 示例 + +本篇介绍在 Solon 应用中如何集成 Sa-Token。 + +整合示例在官方仓库的 `/sa-token-demo/sa-token-demo-solon` 文件夹下,如遇到难点可结合源码进行学习测试。[Sa-Token 集成示例大全下载](/more/download-demos) 。 + +> [!tip| label:Solon 是什么?] +> Solon 是一个高效的国产应用开发框架:更快、更小、更简单。 +> +> - 启动快 5 ~ 10 倍; +> - qps 高 2~ 3 倍; +> - 运行时内存节省 1/3 ~ 1/2; +> - 打包可以缩到 1/2 ~ 1/10; +> - 同时支持 jdk8、jdk11、jdk17、jdk20。 +> +> 详情可参考:[https://solon.noear.org/](https://solon.noear.org/) + +--- + +### 1、创建项目 + +在 IDE 中新建一个 Solon 项目,例如:sa-token-demo-solon (可以借助 [Solon Initializr](https://solon.noear.org/start/) 生成) + +### 2、添加依赖 + +在项目中添加依赖: + + + +``` xml + + + cn.dev33 + sa-token-solon-plugin + ${sa.top.version} + +``` + + +``` gradle +// Sa-Token 权限认证,在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-solon-plugin:${sa.top.version}' +``` + + + + +Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.com/doc.html#/start/maven-pull) + +更多内测版本了解:[Sa-Token最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md) + + + +### 3、设置配置文件 + +你可以**零配置启动项目** ,但同时你也可以在 `app.yml` 中增加如下配置,定制性使用框架: + + + + + +```yaml +server: + # 端口 + port: 8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称(同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true +``` + + +```properties +# 端口 +server.port=8081 + +############## Sa-Token 配置 (文档: https://sa-token.com) ############## + +# token 名称(同时也是 cookie 名称) +sa-token.token-name=satoken +# token 有效期(单位:秒) 默认30天,-1 代表永久有效 +sa-token.timeout=2592000 +# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 +sa-token.active-timeout=-1 +# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) +sa-token.is-concurrent=true +# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) +sa-token.is-share=false +# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) +sa-token.token-style=uuid +# 是否输出操作日志 +sa-token.is-log=true +``` + + + + + + +### 4、创建启动类 + +在项目中新建包 `com.pj` ,在此包内新建主类 `SaTokenDemoApp.java`,复制以下代码: + +```java +@SolonMain +public class SaTokenDemoApp { + public static void main(String[] args) { + Solon.start(SaTokenDemoApp.class, args); + System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); + } +} +``` + +### 5、创建测试Controller + +```java +@Mapping("/user/") +@Controller +public class UserController { + + // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 + @Mapping("doLogin") + public String doLogin(String username, String password) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(username) && "123456".equals(password)) { + StpUtil.login(10001); + return "登录成功"; + } + return "登录失败"; + } + + // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin + @Mapping("isLogin") + public String isLogin() { + return "当前会话是否登录:" + StpUtil.isLogin(); + } + +} +``` + +### 6、运行 + +启动代码,从浏览器依次访问上述测试接口: + +运行结果 + + +运行结果 + + +### 详细了解 + +通过这个示例,你已经对 Sa-Token 有了初步的了解,那么现在开始详细了解一下它都有哪些吧: + +[登录认证](/use/login-auth) (与 Springboot 处理类似) + + + + + diff --git a/sa-token-doc/start/webflux-example.md b/sa-token-doc/start/webflux-example.md new file mode 100644 index 000000000..233f26880 --- /dev/null +++ b/sa-token-doc/start/webflux-example.md @@ -0,0 +1,213 @@ +# Spring WebFlux 集成 Sa-Token 示例 + +**Reactor** 是一种非阻塞的响应式模型,本篇将以 **WebFlux** 为例,展示 Sa-Token 与 Reactor 响应式模型框架相整合的示例, +**你可以用同样方式去对接其它 Reactor 模型框架(例如 SpringCloud Gateway)** + +整合示例在官方仓库的`/sa-token-demo/sa-token-demo-webflux`文件夹下,如遇到难点可结合源码进行测试学习。[Sa-Token 集成示例大全下载](/more/download-demos) 。 + + +> [!WARNING| label:小提示 ] +> WebFlux 常用于微服务网关架构中,如果您的应用基于单体架构且非 Reactor 模型,可以先跳过本章 + + +--- + +### 1、创建项目 +在 IDE 中新建一个 SpringBoot 项目,例如:`sa-token-demo-webflux` + + +### 2、添加依赖 +在项目中添加依赖: + + + +``` xml + + + cn.dev33 + sa-token-reactor-spring-boot-starter + ${sa.top.version} + +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + +``` gradle +// Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.com +implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + +``` gradle +// Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.com +implementation("cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}") +``` +- 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 +- 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 + + + + + + +### 3、创建启动类 +在项目中新建包 `com.pj` ,在此包内新建主类 `SaTokenDemoApplication.java`,输入以下代码: + + + +``` java +@SpringBootApplication +public class SaTokenDemoApplication { + public static void main(String[] args) throws JsonProcessingException { + SpringApplication.run(SaTokenDemoApplication.class, args); + System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); + } +} +``` + + +```kotlin +@SpringBootApplication +class SaTokenDemoApplication + +fun main(args: Array) { + runApplication(*args) + println(SaManager.getConfig()) +} +``` + + + +### 4、创建全局过滤器 +新建`SaTokenConfigure.java`,注册 Sa-Token 的全局过滤器 + + + +``` java +/** + * [Sa-Token 权限认证] 全局配置类 + */ +@Configuration +public class SaTokenConfigure { + /** + * 注册 [Sa-Token全局过滤器] + */ + @Bean + public SaReactorFilter getSaReactorFilter() { + return new SaReactorFilter() + // 指定 [拦截路由] + .addInclude("/**") /* 拦截所有path */ + // 指定 [放行路由] + .addExclude("/favicon.ico") + // 指定[认证函数]: 每次请求执行 + .setAuth(obj -> { + System.out.println("---------- sa全局认证"); + // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); + }) + // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- sa全局异常 "); + return SaResult.error(e.getMessage()); + }) + ; + } +} +``` + + +```kotlin +@Configuration +class SaTokenConfigure { + /** + * 注册 [Sa-Token全局过滤器] + */ + @Bean + fun saReactorFilter(): SaReactorFilter = SaReactorFilter() + // 指定 [拦截路由](此处为拦截所有path) + .addInclude("/**") + // 指定 [放行路由] + .addExclude("/favicon.ico") + // 指定[认证函数]: 每次请求执行 + .setAuth { + println("---------- sa全局认证") + // SaRouter.match("/test/test", SaFunction { StpUtil.checkLogin() }) + } + // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 + .setError { e: Throwable -> + println("---------- sa全局异常 ") + SaResult.error(e.message) + } +} +``` + + +你只需要按照此格式复制代码即可,有关过滤器的详细用法,会在之后的章节详细介绍。 + + +### 5、创建测试Controller + + + +``` java +@RestController +@RequestMapping("/user/") +public class UserController { + + // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 + @RequestMapping("doLogin") + public String doLogin(String username, String password) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(username) && "123456".equals(password)) { + StpUtil.login(10001); + return "登录成功"; + } + return "登录失败"; + } + + // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin + @RequestMapping("isLogin") + public String isLogin() { + return "当前会话是否登录:" + StpUtil.isLogin(); + } + +} +``` + + +```kotlin +@RestController +@RequestMapping("/user/") +class UserController { + + @RequestMapping("doLogin") + fun doLogin(username: String, password: String) = + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if ("zhang" == username && "123456" == password) { + StpUtil.login(10001) + "登录成功" + } else "登录失败" + + @RequestMapping("isLogin") + fun isLogin() = "当前会话是否登录:" + StpUtil.isLogin() + +} +``` + + +### 6、运行 +启动代码,从浏览器依次访问上述测试接口: + +运行结果 + +运行结果 + + +**注意事项:** + +更多使用示例请参考官方仓库demo + + + diff --git a/sa-token-doc/static/all-version-common.js b/sa-token-doc/static/all-version-common.js new file mode 100644 index 000000000..d94ad7cd3 --- /dev/null +++ b/sa-token-doc/static/all-version-common.js @@ -0,0 +1,238 @@ +/** + * Sa-Token 全站公共脚本 + * 引入方式: + * + * - 百度统计 / 搜索引擎自动提交:引入即执行;localhost / 127.0.0.1 下跳过 + * - 文档版本下拉:不自动执行,文档页手动调用 + * SaTokenVersions.initVersionSelects() + * + * 发新版时只需改 LATEST_VERSION / ALL_VERSIONS + */ +(function (global) { + 'use strict'; + + /** 当前线上最新版(根目录 /doc.html) */ + var LATEST_VERSION = 'v1.45.0'; + + /** 全部版本号,从新到旧(含最新版) */ + var ALL_VERSIONS = [ + 'v1.45.0', + 'v1.44.0', + 'v1.43.0', + 'v1.42.0', + 'v1.41.0', + 'v1.40.0', + 'v1.39.0', + 'v1.38.0', + 'v1.37.0', + 'v1.36.0', + 'v1.35.0', + 'v1.34.0', + 'v1.33.0', + 'v1.32.0', + 'v1.31.0', + 'v1.30.0', + 'v1.29.0', + 'v1.28.0', + 'v1.27.0', + 'v1.26.0', + 'v1.25.0', + 'v1.24.0', + 'v1.23.0', + 'v1.22.0', + 'v1.21.0', + 'v1.20.0', + 'v1.19.0', + 'v1.18.0', + 'v1.17.0', + 'v1.16.0', + 'v1.15.0', + 'v1.14.0', + 'v1.13.0', + 'v1.12.1', + 'v1.12.0', + 'v1.11.0', + 'v1.10.0', + 'v1.9.0', + 'v1.8.0', + 'v1.7.0', + 'v1.6.0', + 'v1.5.1', + 'v1.4.0', + 'v1.3.0', + 'v1.2.0', + 'v1.1.0', + 'v1.0.0' + ]; + + /** + * 根据版本号生成文档入口 URL + * - 最新版 → /doc.html + * - >= v1.31.0 → /v/{ver}/doc.html + * - <= v1.30.0 → /v/{ver}/doc/index.html + */ + function getDocUrl(version) { + if (!version) { + return '/doc.html'; + } + var ver = normalizeVersion(version); + if (ver === LATEST_VERSION) { + return '/doc.html'; + } + var parts = ver.replace(/^v/i, '').split('.').map(function (n) { + return parseInt(n, 10) || 0; + }); + var major = parts[0] || 0; + var minor = parts[1] || 0; + if (major > 1 || (major === 1 && minor >= 31)) { + return '/v/' + ver + '/doc.html'; + } + return '/v/' + ver + '/doc/index.html'; + } + + function normalizeVersion(version) { + var v = String(version || '').trim(); + if (!v) { + return ''; + } + if (v.charAt(0).toLowerCase() !== 'v') { + v = 'v' + v; + } + return v; + } + + /** 当前 URL 是否属于根目录最新版文档 */ + function isLatestPath(pathname) { + var path = pathname || (global.location && global.location.pathname) || ''; + if (/\/v\/v\d+\.\d+(\.\d+)?\//i.test(path)) { + return false; + } + return true; + } + + /** + * 判断当前页面属于哪个版本 + * @returns {string} 如 v1.44.0;根目录最新版返回 LATEST_VERSION + */ + function getCurrentVersion(pathname) { + var path = pathname || (global.location && global.location.pathname) || ''; + var match = path.match(/\/v\/(v\d+\.\d+(?:\.\d+)?)\//i); + if (match) { + return normalizeVersion(match[1]); + } + return LATEST_VERSION; + } + + /** 当前是否最新版文档 */ + function isLatestVersion(pathname) { + return getCurrentVersion(pathname) === LATEST_VERSION && isLatestPath(pathname); + } + + /** 填充单个 select.select-version */ + function fillVersionSelect(select) { + if (!select) { + return; + } + var current = getCurrentVersion(); + var html = []; + for (var i = 0; i < ALL_VERSIONS.length; i++) { + var ver = ALL_VERSIONS[i]; + var url = getDocUrl(ver); + var selected = ver === current ? ' selected' : ''; + html.push(''); + } + html.push(''); + select.innerHTML = html.join(''); + if (!select.getAttribute('onchange')) { + select.onchange = function () { + if (this.value) { + global.location.href = this.value; + } + }; + } + } + + /** + * 填充页面上所有版本下拉框 + * 仅文档页需要调用,其它页面不要调用 + */ + function initVersionSelects() { + if (!global.document) { + return; + } + var nodes = document.querySelectorAll('select.select-version'); + for (var i = 0; i < nodes.length; i++) { + fillVersionSelect(nodes[i]); + } + } + + var api = { + LATEST_VERSION: LATEST_VERSION, + ALL_VERSIONS: ALL_VERSIONS, + getDocUrl: getDocUrl, + getCurrentVersion: getCurrentVersion, + isLatestVersion: isLatestVersion, + isLatestPath: isLatestPath, + fillVersionSelect: fillVersionSelect, + initVersionSelects: initVersionSelects + }; + + global.SaTokenVersions = api; + global.saTokenLatestVersion = LATEST_VERSION; +})(typeof window !== 'undefined' ? window : this); + +/* 导航「博客」统一指向最新博客首页(旧版 /v/vX.Y.Z/ 下相对路径会指错) */ +(function () { + function fixBlogNavHref() { + if (!document || !document.querySelectorAll) { + return; + } + var links = document.querySelectorAll('a.wzi'); + for (var i = 0; i < links.length; i++) { + var a = links[i]; + var text = (a.textContent || '').replace(/\s+/g, ''); + if (text !== '博客') { + continue; + } + a.setAttribute('href', '/blog/index.html'); + } + } + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', fixBlogNavHref); + } else { + fixBlogNavHref(); + } +})(); + +/* 百度统计 + 搜索引擎自动提交(引入即执行;本地环境跳过) */ +(function() { + var host = (location.hostname || '').toLowerCase(); + if (host === 'localhost' || host === '127.0.0.1') { + console.log('本地环境,跳过百度统计 + 搜索引擎自动提交...'); + return; + } else { + console.log('百度统计 + 搜索引擎自动提交... '); + } + + /* 百度统计 */ + var _hmt = window._hmt = window._hmt || []; + (function() { + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?0123e86492363a4dd40db165092b557b"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })(); + + /* 搜索引擎自动提交 */ + (function() { + var bp = document.createElement('script'); + var curProtocol = window.location.protocol.split(':')[0]; + if (curProtocol === 'https') { + bp.src = 'https://zz.bdstatic.com/linksubmit/push.js'; + } else { + bp.src = 'http://push.zhanzhang.baidu.com/push.js'; + } + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(bp, s); + })(); +})(); diff --git a/sa-token-doc/static/custom-docsify-plugins/doc-lock-by-gzh-plugin.js b/sa-token-doc/static/custom-docsify-plugins/doc-lock-by-gzh-plugin.js new file mode 100644 index 000000000..4bbb577fe --- /dev/null +++ b/sa-token-doc/static/custom-docsify-plugins/doc-lock-by-gzh-plugin.js @@ -0,0 +1,521 @@ +// 章节锁定插件 + +// 声明 docsify 插件 +var docLockPlugin = function(hook, vm) { + + // 钩子函数:解析之前执行 + hook.beforeEach(function(content) { + return content; + }); + + // 钩子函数:每次路由切换时,解析内容之后执行 + hook.afterEach(function(html) { + return html; + }); + + // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 + hook.doneEach(function() { + isShowTanChuang(vm); + }); + + // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 + hook.ready(function() { + + }); + + + + + + // ======================================== 弹窗方法 + + // 检查成功后,多少天不再检查 + const dl_AllowDisparity = 1000 * 60 * 60 * 24 * 30 * 1; // 1个月 + // 拦截 path ,如果填 /** 代表所有路径,填 /sso/* 代表 /sso/ 目录下所有路径 + const dl_exeArray = [ + '/sso/*', '/oauth2/*', '/more/common-questions', '/up/*', '/micro/*', '/plugin/*' + ]; + // 排除 path + const dl_excludeArray = [ + '/sso/readme', '/oauth2/readme' + ]; + // 本次存储时,使用的标记 key + const dl_saveKey = 'dl_saveKey'; + + + // 判断当前是否应该弹出 + function isShowTanChuang(vm) { + // 非PC端不检查 + // if(document.body.offsetWidth < 800) { + // console.log('small screen ... wj '); + // return; + // } + + // 判断是否需要拦截 + const isExe = isExePath(vm.route.path, dl_exeArray, dl_excludeArray); + if(!isExe) { + return; + } + + // 判断是否近期已经判断过了 + try{ + const flagTime = localStorage[dl_saveKey]; + if(flagTime) { + // 记录 存储 的时间,和当前时间的差距 + const disparity = new Date().getTime() - parseInt(flagTime); + + // 差距小于指定时间,不再检测 + if(disparity < dl_AllowDisparity) { + console.log('checked ... docLock '); + return; + } + } + }catch(e){ + console.error(e); + } + + // 本次打开页面的内存内已经弹出了的话,也不再弹了 + // if(window.isYtcXsjfkasjdaaaa) { + // return; + // } + // window.isYtcXsjfkasjdaaaa = true; + + // 验证成功的回调 + const okFn = function() { + console.log('ok 了'); + localStorage.setItem(dl_saveKey, new Date().getTime() ); + $('body').css({'overflow': 'auto'}); + layer.msg('感谢你的支持,Sa-Token 将努力变得更加完善! ❤️ ❤️ ❤️ '); + } + // 点了返回的回调 + const backFu = function() { + $('body').css({'overflow': 'auto'}); + location.href = '#/'; + } + // 弹窗验证 + showDocLock(okFn, backFu); + $('body').css({'overflow': 'hidden'}); + + // 弹出弹框,邀请填写 + return; + } + + + // ======================================== 路径判断 + + /** + * 判断一个路径,是否会被成功拦截,返回 true 或 false + * @param {Object} path 要判断的路径,例如:/sso/apidoc + * @param {Object} exeArray 要拦截的路径数组,例如:['/sso/*', '/oauth2/*', '/more/common-questions' ],如果填 /** 代表所有路径,填 /sso/* 代表 /sso/ 目录下所有路径 + * @param {Object} excludeArray 要排除的路径数组,规则同上 + */ + function isExePath( path, exeArray, excludeArray) { + // 参数验证和初始化 + exeArray = exeArray || []; + excludeArray = excludeArray || []; + + // 标准化路径,确保以 / 开头 + path = normalizePath(path); + + // 先检查排除规则(优先级更高) + for (let pattern of excludeArray) { + if (matchPattern(path, pattern)) { + return false; // 被排除,不拦截 + } + } + + // 再检查拦截规则 + for (let pattern of exeArray) { + if (matchPattern(path, pattern)) { + return true; // 需要拦截 + } + } + + return false; // 默认不拦截 + } + + /** + * 标准化路径 + */ + function normalizePath(path) { + if (!path) return '/'; + if (!path.startsWith('/')) return '/' + path; + return path; + } + + /** + * 增强版模式匹配 + */ + function matchPattern(path, pattern) { + // 处理空值 + if (!pattern) return false; + + pattern = pattern.trim(); + + // /** 匹配所有 + if (pattern === '/**' || pattern === '**') { + return true; + } + + // 处理前置和后置通配符 + if (pattern === '*' || pattern === '/*') { + return true; + } + + // 精确匹配 + if (!pattern.includes('*')) { + return path === pattern || path === normalizePath(pattern); + } + + // 转换模式为正则表达式 + const regexStr = pattern + // 转义正则特殊字符 + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + // 处理 ** 通配符(匹配多级目录) + .replace(/\/\*\*/g, '(/.*)?') + // 处理 * 通配符(匹配单级目录) + .replace(/\*/g, '[^/]*') + // 确保匹配完整路径 + .replace(/^\//, '^/') + .replace(/$/, '$'); + + try { + const regex = new RegExp(regexStr); + return regex.test(path); + } catch (e) { + console.error(`Invalid pattern: ${pattern}`, e); + return false; + } + } + + + +} + + + + + + + + +// =========================== AI 生成的弹窗代码 + +function initTanChuangFun() { + + // 配置项 + const CONFIG = { + correctPassword: 'sa-token yyds', // 正确密码 + gzhImageUrl: './big-file/contact/lykj-gzh.jpg', + wechatImageUrl: './big-file/doc/zong/doc-lock-pre-wx.png' + }; + + // 弹窗HTML模板 + const modalHTML = ` + + +
+ 放大图片 +
+ `; + + // 初始化变量 + let passwordModal, passwordInput, errorMessage, verifyBtn, backBtn; + let imageOverlay, enlargedImage; + + + let okCallFn = null; + let backCallFn = null; + + /** + * 初始化弹窗 + * 将弹窗HTML插入到页面中,并绑定事件 + */ + function initModal() { + // 插入弹窗HTML到页面 + document.body.insertAdjacentHTML('beforeend', modalHTML); + + // 获取DOM元素 + passwordModal = document.getElementById('passwordModal'); + passwordInput = document.getElementById('passwordInput'); + errorMessage = document.getElementById('errorMessage'); + verifyBtn = document.getElementById('verifyBtn'); + backBtn = document.getElementById('backBtn'); + imageOverlay = document.getElementById('imageOverlay'); + enlargedImage = document.getElementById('enlargedImage'); + + // 绑定事件 + bindEvents(); + } + + /** + * 绑定所有事件 + */ + function bindEvents() { + // 触发按钮点击事件 + // document.getElementById('accessBtn').addEventListener('click', openModal); + + // 验证按钮点击事件 + verifyBtn.addEventListener('click', validatePassword); + + // 返回按钮点击事件 + backBtn.addEventListener('click', function(){ + closeModal(); + backCallFn(); + }); + + // 输入框回车事件 + passwordInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + validatePassword(); + } + }); + + // 只在非移动端绑定图片点击事件 + if (!isMobileDevice()) { + // 图片点击放大事件 + document.querySelectorAll('.qr-image').forEach(img => { + img.addEventListener('click', function() { + enlargeImage(this.src); + }); + }); + + // 放大图片关闭事件 + imageOverlay.addEventListener('click', function(e) { + if (e.target === this || e.target === enlargedImage) { + closeImageOverlay(); + } + }); + + // ESC键关闭放大图片 + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && imageOverlay.classList.contains('active')) { + closeImageOverlay(); + } + }); + } + } + + /** + * 检测是否为移动设备 + * @returns {boolean} 是否为移动设备 + */ + function isMobileDevice() { + return window.innerWidth <= 768; + } + + /** + * 打开密码弹窗 + */ + function openModal() { + passwordModal.classList.add('active'); + passwordInput.focus(); + errorMessage.classList.remove('show'); + passwordInput.value = ''; + } + + /** + * 关闭密码弹窗 + */ + function closeModal() { + passwordModal.classList.remove('active'); + } + + /** + * 密码验证函数 + * 宽松验证策略:允许左右空格,中间空格可省略 + */ + function validatePassword() { + let enteredPassword = passwordInput.value.trim(); // 去除左右空格 + + // 标准化:移除所有空格 + const normalizedEntered = enteredPassword.replace(/\s+/g, ''); + const normalizedCorrect = CONFIG.correctPassword.replace(/\s+/g, ''); + + if (normalizedEntered === normalizedCorrect) { + // 密码正确,解锁章节 + unlockChapter(); + } else { + // 密码错误,显示错误信息 + showError(); + } + } + + /** + * 显示密码错误提示 + */ + function showError() { + errorMessage.classList.add('show'); + passwordInput.value = ''; + passwordInput.focus(); + + // 添加抖动效果 + passwordInput.classList.add('shake'); + setTimeout(() => { + passwordInput.classList.remove('shake'); + }, 500); + } + + /** + * 解锁章节 + */ + function unlockChapter() { + closeModal(); + okCallFn(); + + + // // 更新章节内容 + // const lockedSection = document.querySelector('.locked-section'); + // const tocLockedItems = document.querySelectorAll('.toc a.locked'); + + // // 更新章节显示 + // lockedSection.innerHTML = ` + //

章节已解锁

+ //

感谢您加入我们的社区!现在您可以查看高级配置指南的全部内容。

+ //
+ //

高级配置内容示例:

+ //

1. 自定义插件开发:详细讲解如何为项目开发自定义插件,包括插件结构、API接口和最佳实践。

+ //

2. 性能调优指南:深入分析项目性能瓶颈,并提供多种优化方案和调优技巧。

+ //

3. 高级集成方案:介绍如何将项目与其他流行框架和工具进行深度集成。

+ //

4. 企业级部署:针对生产环境的企业级部署方案,包括高可用、负载均衡和监控配置。

+ //
+ //

+ // 您现在可以访问所有高级章节内容了! + //

+ // `; + + // // 更新目录状态 + // tocLockedItems.forEach(item => { + // if (item.textContent.includes('高级配置指南')) { + // item.classList.remove('locked'); + // item.innerHTML = ' 高级配置指南'; + // } + // }); + + // // 显示成功通知 + // showNotification('章节解锁成功!您现在可以访问高级配置指南。'); + } + + /** + * 放大图片 + * @param {string} src - 图片地址 + */ + function enlargeImage(src) { + enlargedImage.src = src; + imageOverlay.classList.add('active'); + } + + /** + * 关闭图片放大层 + */ + function closeImageOverlay() { + imageOverlay.classList.remove('active'); + } + + /** + * 显示通知 + * @param {string} message - 通知内容 + */ + function showNotification(message) { + const notification = document.createElement('div'); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background-color: #2ecc71; + color: white; + padding: 15px 25px; + border-radius: 4px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); + z-index: 1001; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; + transform: translateX(150%); + transition: transform 0.5s ease; + `; + + notification.innerHTML = `${message}`; + document.body.appendChild(notification); + + // 显示通知 + setTimeout(() => { + notification.style.transform = 'translateX(0)'; + }, 10); + + // 3秒后隐藏 + setTimeout(() => { + notification.style.transform = 'translateX(150%)'; + setTimeout(() => { + document.body.removeChild(notification); + }, 500); + }, 3000); + } + + /** + * 添加抖动动画样式 + */ + function addShakeAnimation() { + const style = document.createElement('style'); + style.textContent = ` + @keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } + 20%, 40%, 60%, 80% { transform: translateX(5px); } + } + .shake { + animation: shake 0.5s; + border-color: #e74c3c !important; + } + `; + document.head.appendChild(style); + } + + // 初始化 + document.addEventListener('DOMContentLoaded', function() { + initModal(); + addShakeAnimation(); + }); + + // 显示弹窗: 验证成功的回调、点击返回的回调 + window.showDocLock = function(okFn, backFn) { + okCallFn = okFn; + backCallFn = backFn; + // 打开 + openModal(); + } + +}; +initTanChuangFun(); \ No newline at end of file diff --git a/sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.css b/sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.css new file mode 100644 index 000000000..cee096b4f --- /dev/null +++ b/sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.css @@ -0,0 +1,347 @@ +/* 弹窗遮罩层样式 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s, visibility 0.3s; +} + +.modal-overlay.active { + opacity: 1; + visibility: visible; +} + +/* 弹窗主体样式 */ +.modal { + background: white; + border-radius: 4px; /* 弹窗圆角4px */ + width: 90%; + max-width: 500px; + padding: 25px 30px; + box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2); + transform: translateY(-20px); + transition: transform 0.3s; +} + +.modal-overlay.active .modal { + transform: translateY(0); +} + +/* 弹窗标题样式 */ +.modal-header { + display: flex; + align-items: center; + margin-bottom: 20px; + /* color: green; */ + /* color: #2d8cf0; */ + color: green; +} + +.modal-header i { + font-size: 1.8rem; + margin-right: 12px; +} + +.modal-header h2 { + font-size: 16px; + /* color: #e74c3c; */ + margin: 0; +} + +/* 密码输入区域样式 */ +.password-form { + margin-bottom: 20px; +} + +.password-input { + width: 100%; + padding: 14px; + border: 2px solid #ddd; + border-radius: 2px; /* 输入框圆角2px */ + font-size: 1rem; + transition: border-color 0.3s; + margin-bottom: 15px; +} + +.password-input:focus { + border-color: #2d8cf0; + outline: none; +} + +/* 按钮区域样式 */ +.form-actions { + display: flex; + gap: 15px; + justify-content: flex-start; +} + +.form-btn { + color: white; + border: none; + padding: 12px 24px; + border-radius: 3px; /* 按钮圆角2px */ + font-weight: 400; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s; + white-space: nowrap; + width: calc(50% - 5px); +} + +.btn-verify { + background-color: #2d8cf0; +} + +.btn-verify:hover { + background-color: #1c7ae0; +} + +.btn-back { + background-color: #aaa; +} + +.btn-back:hover { + background-color: #888; +} + +/* 错误提示样式 */ +.error-message { + color: #e74c3c; + margin-bottom: 15px; + text-align: center; + font-weight: 600; + display: none; + background-color: #ffebee; + padding: 10px; + border-radius: 2px; + border-left: 3px solid #e74c3c; +} + +.error-message.show { + display: block; +} + +/* 提示区域样式 */ +.password-help-section { + margin-top: 25px; + border-top: 1px solid #eee; + padding-top: 20px; +} + +.help-text { + text-align: left; + margin-bottom: 20px; + color: #666; + font-weight: 400; + font-size: 16px; +} + +.help-text a { + color: #0c6ae0; + text-decoration: none; + transition: color 0.2s; +} + +.help-text a:hover { + color: blue; + text-decoration: underline; +} + +/* 二维码图片区域样式 */ +.images-container { + display: flex; + gap: 15px; + justify-content: center; + align-items: center; +} + +.qr-image-container { + flex: 1; + position: relative; + overflow: hidden; + border-radius: 0px; + /* box-shadow: 0 0px 8px rgba(0, 0, 0, 0.1); */ + border: 1px #ddd solid; + cursor: pointer; + transition: transform 0.3s; + padding-bottom: 5px; +} + +.qr-image { + width: 100%; + height: 110px; /* 3:2比例 */ + object-fit: cover; + display: block; + transition: transform 0.3s; +} + +.qr-image-container:hover { + transform: translateY(-5px); +} + +.qr-image-container:hover .qr-image { + transform: scale(1.05); +} + +.image-label { + text-align: center; + font-size: 0.85rem; + color: #7f8c8d; + margin-top: 8px; + font-weight: 500; +} + +/* 图片放大效果样式 */ +.image-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; + cursor: pointer; + opacity: 0; + visibility: hidden; + transition: opacity 0.4s ease, visibility 0.4s ease; +} + +.image-overlay.active { + opacity: 1; + visibility: visible; +} + +.enlarged-image { + max-width: 80%; + max-height: 80%; + border-radius: 8px; + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5); + transform: scale(0.8); + transition: transform 0.4s ease; +} + +.image-overlay.active .enlarged-image { + transform: scale(1); +} + +/* ============================ */ +/* 移动端适配样式 */ +/* ============================ */ + +/* 移动端:屏幕宽度小于等于768px时应用 */ +@media (max-width: 768px) { + /* 弹窗宽度调整,左右留出边距 */ + .modal { + width: calc(100% - 30px); /* 左右各15px边距 */ + max-width: 100%; + padding: 20px; + margin: 0 15px; + } + + /* 弹窗标题调整 */ + .modal-header { + margin-bottom: 15px; + } + + .modal-header h2 { + font-size: 1.1rem; + line-height: 1.3; + } + + .modal-header i { + font-size: 1.5rem; + margin-right: 10px; + } + + /* 密码输入框调整 */ + .password-input { + padding: 12px; + font-size: 0.95rem; + margin-bottom: 10px; + } + + /* 按钮区域调整 */ + .form-actions { + flex-direction: column; /* 按钮垂直排列 */ + gap: 10px; + justify-content: stretch; + } + + .form-btn { + width: 100%; + padding: 14px; + font-size: 1rem; + } + + /* 错误提示调整 */ + .error-message { + font-size: 0.9rem; + padding: 8px; + margin-bottom: 10px; + } + + /* 提示区域调整 */ + .password-help-section { + margin-top: 15px; + padding-top: 15px; + } + + .help-text { + font-size: 0.95rem; + margin-bottom: 15px; + } + + /* 移动端隐藏图片区域 */ + .images-container { + display: none; /* 移动端隐藏图片 */ + } + + /* 图片放大效果在移动端隐藏 */ + .image-overlay { + display: none; + } + + /* 弹窗内容垂直居中优化 */ + .modal { + max-height: 80vh; + overflow-y: auto; + } +} + +/* 超小屏幕适配:屏幕宽度小于等于480px时应用 */ +@media (max-width: 480px) { + .modal { + width: calc(100% - 20px); /* 左右各10px边距 */ + margin: 0 10px; + padding: 15px; + } + + .modal-header h2 { + font-size: 1rem; + } + + .help-text { + font-size: 0.9rem; + line-height: 1.4; + } + + .password-input { + font-size: 0.9rem; + } + + .form-btn { + font-size: 0.95rem; + } +} \ No newline at end of file diff --git a/sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.js b/sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.js new file mode 100644 index 000000000..b1d99e839 --- /dev/null +++ b/sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.js @@ -0,0 +1,525 @@ +// 章节锁定插件 + +// 声明 docsify 插件 +var docLockPlugin = function(hook, vm) { + + // 钩子函数:解析之前执行 + hook.beforeEach(function(content) { + return content; + }); + + // 钩子函数:每次路由切换时,解析内容之后执行 + hook.afterEach(function(html) { + return html; + }); + + // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 + hook.doneEach(function() { + // isShowTanChuang(vm); + }); + + // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 + hook.ready(function() { + + }); + + + + + + // ======================================== 弹窗方法 + + // 检查成功后,多少天不再检查 + const dl_AllowDisparity = 1000 * 60 * 60 * 24 * 30 * 1; // 1个月 + // 拦截 path ,如果填 /** 代表所有路径,填 /sso/* 代表 /sso/ 目录下所有路径 + const dl_exeArray = [ + '/sso/*', '/oauth2/*', '/more/common-questions', '/up/*', '/micro/*', '/plugin/*' + ]; + // 排除 path + const dl_excludeArray = [ + '/sso/readme', '/oauth2/readme' + ]; + // 本次存储时,使用的标记 key + const dl_saveKey = 'dl_saveKey'; + + + // 判断当前是否应该弹出 + function isShowTanChuang(vm) { + // 非PC端不检查 + // if(document.body.offsetWidth < 800) { + // console.log('small screen ... wj '); + // return; + // } + + // 判断是否需要拦截 + const isExe = isExePath(vm.route.path, dl_exeArray, dl_excludeArray); + if(!isExe) { + return; + } + + // 判断是否近期已经判断过了 + try{ + const flagTime = localStorage[dl_saveKey]; + if(flagTime) { + // 记录 存储 的时间,和当前时间的差距 + const disparity = new Date().getTime() - parseInt(flagTime); + + // 差距小于指定时间,不再检测 + if(disparity < dl_AllowDisparity) { + console.log('checked ... docLock '); + return; + } + } + }catch(e){ + console.error(e); + } + + // 本次打开页面的内存内已经弹出了的话,也不再弹了 + // if(window.isYtcXsjfkasjdaaaa) { + // return; + // } + // window.isYtcXsjfkasjdaaaa = true; + + // 验证成功的回调 + const okFn = function() { + console.log('ok 了'); + localStorage.setItem(dl_saveKey, new Date().getTime() ); + $('body').css({'overflow': 'auto'}); + layer.msg('感谢你的支持,Sa-Token 将努力变得更加完善! ❤️ ❤️ ❤️ '); + } + // 点了返回的回调 + const backFu = function() { + $('body').css({'overflow': 'auto'}); + location.href = '#/'; + } + // 弹窗验证 + showDocLock(okFn, backFu); + $('body').css({'overflow': 'hidden'}); + + // 弹出弹框,邀请填写 + return; + } + + + // ======================================== 路径判断 + + /** + * 判断一个路径,是否会被成功拦截,返回 true 或 false + * @param {Object} path 要判断的路径,例如:/sso/apidoc + * @param {Object} exeArray 要拦截的路径数组,例如:['/sso/*', '/oauth2/*', '/more/common-questions' ],如果填 /** 代表所有路径,填 /sso/* 代表 /sso/ 目录下所有路径 + * @param {Object} excludeArray 要排除的路径数组,规则同上 + */ + function isExePath( path, exeArray, excludeArray) { + // 参数验证和初始化 + exeArray = exeArray || []; + excludeArray = excludeArray || []; + + // 标准化路径,确保以 / 开头 + path = normalizePath(path); + + // 先检查排除规则(优先级更高) + for (let pattern of excludeArray) { + if (matchPattern(path, pattern)) { + return false; // 被排除,不拦截 + } + } + + // 再检查拦截规则 + for (let pattern of exeArray) { + if (matchPattern(path, pattern)) { + return true; // 需要拦截 + } + } + + return false; // 默认不拦截 + } + + /** + * 标准化路径 + */ + function normalizePath(path) { + if (!path) return '/'; + if (!path.startsWith('/')) return '/' + path; + return path; + } + + /** + * 增强版模式匹配 + */ + function matchPattern(path, pattern) { + // 处理空值 + if (!pattern) return false; + + pattern = pattern.trim(); + + // /** 匹配所有 + if (pattern === '/**' || pattern === '**') { + return true; + } + + // 处理前置和后置通配符 + if (pattern === '*' || pattern === '/*') { + return true; + } + + // 精确匹配 + if (!pattern.includes('*')) { + return path === pattern || path === normalizePath(pattern); + } + + // 转换模式为正则表达式 + const regexStr = pattern + // 转义正则特殊字符 + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + // 处理 ** 通配符(匹配多级目录) + .replace(/\/\*\*/g, '(/.*)?') + // 处理 * 通配符(匹配单级目录) + .replace(/\*/g, '[^/]*') + // 确保匹配完整路径 + .replace(/^\//, '^/') + .replace(/$/, '$'); + + try { + const regex = new RegExp(regexStr); + return regex.test(path); + } catch (e) { + console.error(`Invalid pattern: ${pattern}`, e); + return false; + } + } + + + +} + + + + + + + + +// =========================== AI 生成的弹窗代码 + +function initTanChuangFun() { + + // 配置项 + const CONFIG = { + correctPassword: 'sa-token yyds', // 正确密码 + qqImageUrl: './big-file/doc/zong/doc-lock-pre-qq.png', + wechatImageUrl: './big-file/doc/zong/doc-lock-pre-wx.png' + }; + + // 弹窗HTML模板 + const modalHTML = ` + + +
+ 放大图片 +
+ `; + + // 初始化变量 + let passwordModal, passwordInput, errorMessage, verifyBtn, backBtn; + let imageOverlay, enlargedImage; + + + let okCallFn = null; + let backCallFn = null; + + /** + * 初始化弹窗 + * 将弹窗HTML插入到页面中,并绑定事件 + */ + function initModal() { + // 插入弹窗HTML到页面 + document.body.insertAdjacentHTML('beforeend', modalHTML); + + // 获取DOM元素 + passwordModal = document.getElementById('passwordModal'); + passwordInput = document.getElementById('passwordInput'); + errorMessage = document.getElementById('errorMessage'); + verifyBtn = document.getElementById('verifyBtn'); + backBtn = document.getElementById('backBtn'); + imageOverlay = document.getElementById('imageOverlay'); + enlargedImage = document.getElementById('enlargedImage'); + + // 绑定事件 + bindEvents(); + } + + /** + * 绑定所有事件 + */ + function bindEvents() { + // 触发按钮点击事件 + // document.getElementById('accessBtn').addEventListener('click', openModal); + + // 验证按钮点击事件 + verifyBtn.addEventListener('click', validatePassword); + + // 返回按钮点击事件 + backBtn.addEventListener('click', function(){ + closeModal(); + backCallFn(); + }); + + // 输入框回车事件 + passwordInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + validatePassword(); + } + }); + + // 只在非移动端绑定图片点击事件 + if (!isMobileDevice()) { + // 图片点击放大事件 + document.querySelectorAll('.qr-image').forEach(img => { + img.addEventListener('click', function() { + enlargeImage(this.src); + }); + }); + + // 放大图片关闭事件 + imageOverlay.addEventListener('click', function(e) { + if (e.target === this || e.target === enlargedImage) { + closeImageOverlay(); + } + }); + + // ESC键关闭放大图片 + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && imageOverlay.classList.contains('active')) { + closeImageOverlay(); + } + }); + } + } + + /** + * 检测是否为移动设备 + * @returns {boolean} 是否为移动设备 + */ + function isMobileDevice() { + return window.innerWidth <= 768; + } + + /** + * 打开密码弹窗 + */ + function openModal() { + passwordModal.classList.add('active'); + passwordInput.focus(); + errorMessage.classList.remove('show'); + passwordInput.value = ''; + } + + /** + * 关闭密码弹窗 + */ + function closeModal() { + passwordModal.classList.remove('active'); + } + + /** + * 密码验证函数 + * 宽松验证策略:允许左右空格,中间空格可省略 + */ + function validatePassword() { + let enteredPassword = passwordInput.value.trim(); // 去除左右空格 + + // 标准化:移除所有空格 + const normalizedEntered = enteredPassword.replace(/\s+/g, ''); + const normalizedCorrect = CONFIG.correctPassword.replace(/\s+/g, ''); + + if (normalizedEntered === normalizedCorrect) { + // 密码正确,解锁章节 + unlockChapter(); + } else { + // 密码错误,显示错误信息 + showError(); + } + } + + /** + * 显示密码错误提示 + */ + function showError() { + errorMessage.classList.add('show'); + passwordInput.value = ''; + passwordInput.focus(); + + // 添加抖动效果 + passwordInput.classList.add('shake'); + setTimeout(() => { + passwordInput.classList.remove('shake'); + }, 500); + } + + /** + * 解锁章节 + */ + function unlockChapter() { + closeModal(); + okCallFn(); + + + // // 更新章节内容 + // const lockedSection = document.querySelector('.locked-section'); + // const tocLockedItems = document.querySelectorAll('.toc a.locked'); + + // // 更新章节显示 + // lockedSection.innerHTML = ` + //

章节已解锁

+ //

感谢您加入我们的社区!现在您可以查看高级配置指南的全部内容。

+ //
+ //

高级配置内容示例:

+ //

1. 自定义插件开发:详细讲解如何为项目开发自定义插件,包括插件结构、API接口和最佳实践。

+ //

2. 性能调优指南:深入分析项目性能瓶颈,并提供多种优化方案和调优技巧。

+ //

3. 高级集成方案:介绍如何将项目与其他流行框架和工具进行深度集成。

+ //

4. 企业级部署:针对生产环境的企业级部署方案,包括高可用、负载均衡和监控配置。

+ //
+ //

+ // 您现在可以访问所有高级章节内容了! + //

+ // `; + + // // 更新目录状态 + // tocLockedItems.forEach(item => { + // if (item.textContent.includes('高级配置指南')) { + // item.classList.remove('locked'); + // item.innerHTML = ' 高级配置指南'; + // } + // }); + + // // 显示成功通知 + // showNotification('章节解锁成功!您现在可以访问高级配置指南。'); + } + + /** + * 放大图片 + * @param {string} src - 图片地址 + */ + function enlargeImage(src) { + enlargedImage.src = src; + imageOverlay.classList.add('active'); + } + + /** + * 关闭图片放大层 + */ + function closeImageOverlay() { + imageOverlay.classList.remove('active'); + } + + /** + * 显示通知 + * @param {string} message - 通知内容 + */ + function showNotification(message) { + const notification = document.createElement('div'); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background-color: #2ecc71; + color: white; + padding: 15px 25px; + border-radius: 4px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); + z-index: 1001; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; + transform: translateX(150%); + transition: transform 0.5s ease; + `; + + notification.innerHTML = `${message}`; + document.body.appendChild(notification); + + // 显示通知 + setTimeout(() => { + notification.style.transform = 'translateX(0)'; + }, 10); + + // 3秒后隐藏 + setTimeout(() => { + notification.style.transform = 'translateX(150%)'; + setTimeout(() => { + document.body.removeChild(notification); + }, 500); + }, 3000); + } + + /** + * 添加抖动动画样式 + */ + function addShakeAnimation() { + const style = document.createElement('style'); + style.textContent = ` + @keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } + 20%, 40%, 60%, 80% { transform: translateX(5px); } + } + .shake { + animation: shake 0.5s; + border-color: #e74c3c !important; + } + `; + document.head.appendChild(style); + } + + // 初始化 + document.addEventListener('DOMContentLoaded', function() { + initModal(); + addShakeAnimation(); + }); + + // 显示弹窗: 验证成功的回调、点击返回的回调 + window.showDocLock = function(okFn, backFn) { + okCallFn = okFn; + backCallFn = backFn; + // 打开 + openModal(); + } + +}; +initTanChuangFun(); \ No newline at end of file diff --git a/sa-token-doc/static/doc.css b/sa-token-doc/static/doc.css new file mode 100644 index 000000000..2eb8861f4 --- /dev/null +++ b/sa-token-doc/static/doc.css @@ -0,0 +1,622 @@ +/* 调整一下左侧树的样式 */ +body{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;} + +#main {padding-bottom: 100px;} +#main h2 {font-size: 1.6rem;} +#main h3 {font-size: 1.25rem;} + +.main-box .markdown-section{ /* padding: 38px 20px; */ max-width: 100%; /* margin-left: 12%; */} +.main-box .markdown-section h4{font-size: 1rem;} + +.main-box red, .main-box red *{ color: red !important; } +.main-box green, .main-box green *{ color: green !important; } +.main-box blue, .main-box blue *{ color: blue;!important; } +.main-box purple, .main-box purple *{ color: purple;!important; } +.main-box question, .main-box question *{ color: purple;!important; } + + + +/* ------- 多设备适配 start ------- */ +.sub-nav-draw-box{ display: none; } +body{ + --doc-left-width: 300px; + --doc-context-width: 1000px; + --doc-right-width: 300px; +} + +/* 大于 1100px,就显示左中右结构 */ +@media screen and (min-width: 1100px) { + .doc-right-bj-box{ display: block; } + .main-box .content{left: 0;} + .main-box .markdown-section{width: var(--doc-context-width); padding: 38px 20px; border: 0px green solid;} + .main-box .doc-right-bj-box{left: calc(50% + (var(--doc-context-width) / 2) + 10px);} + .main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar{ + position: fixed; + top: 120px; + left: calc(50% + (var(--doc-context-width) / 2) + 10px); + width: var(--doc-right-width) !important; + border: 0px #000 solid; + line-height: 1.4em; + width: calc(300px - 25px); + max-height: 50vh; + overflow: auto; + } + .main-box .sidebar{width: var(--doc-left-width);} + .main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar::-webkit-scrollbar{ width: 0px; } + .main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar li.active a{ color: #42B983; } + .main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar li a{ font-size: 12px; color: #888; } + /* .main-box .app-sub-sidebar{display: none;} */ +} +/* 小于 1100px时 */ +@media screen and (max-width: 1100px) { + .doc-right-bj-box{ display: none; } +} + + + +/* 大于 1600px */ +@media screen and (min-width: 1600px) { + body{ + --doc-left-width: 300px; + --doc-context-width: 1000px; + --doc-right-width: 300px; + } +} +/* 小于 1100px - 1600px 之间 */ +@media screen and (max-width: 1600px) { + body{ + --doc-left-width: 200px; + --doc-context-width: calc( 100vw - 400px - 50px); + --doc-right-width: calc(200px - 20px); + + /* 窄屏幕时,广告更换为上下显示 */ + .main-box .top-ad-box .mad-img{ float: none; margin-right: 10px; border: 0px; } + .main-box .top-ad-box .mad-text{ float: none; display: block; width: 100%; margin-top: 10px; color: #000; word-break: normal; } + } +} +/* 小于 1100px时 */ +@media screen and (max-width: 1100px) { + .doc-right-bj-box{ display: none; } +} +/* 小于 800px时 */ +/* @media screen and (max-width: 800px) { + .doc-right-bj-box{ display: none; } +} */ + + +/* 媒体查询 */ +@media screen and (max-width: 800px) { + .nav-left .logo-box .logo-text, + .nav-left .logo-box sub{display: none;} + /* .main-box .markdown-section{max-width: 1000px; margin-left: auto; margin-top: 40px;} */ +} +/* 手机端不显示广告,和一些其它东西 */ +@media (max-width: 576px) {.wwads-cn,.p-none{display:none!important}} + +/* ------- 多设备适配 end ------- */ + + +/* 右侧盒子 */ +.doc-right-bj-box{ + width: var(--doc-right-width); + padding: 10px; + position: fixed; + margin-top: 10px; + top: 60px; + border: 0px #000 solid; + font-size: 12px; +} +.doc-right-bj-box-title{ font-size: 14px; color: #888; padding-bottom: 8px; border-bottom: 1px #aaa solid; } +.doc-right-more-item{ position: absolute; border: 0px #000 solid; color: #000; width: 100%;} + + + + +/* ------- 头部样式 ------- */ +.doc-header{position: fixed; top: 0; z-index: 1000; width: 100%; height: 60px; line-height: 60px;} +.doc-header{/* background-color: hsla(0,0%,100%,0.97); */ background-color: rgba(255, 255, 255, 0.97); box-shadow: 0 1px 3px rgba(26,26,26,0.1);} + +/* 左边导航 */ +.nav-left{display: inline-block; float: left;} +.nav-left > a{text-decoration: none; color: inherit;} +.logo-box {display: inline-block; cursor: pointer; color: #000; padding-left: 24px; height: 60px; line-height: 60px;} +.logo-box img {width: 50px; height: 50px; vertical-align: middle; position: relative; top: -1px; margin-right: 5px;} +.logo-box .logo-text {display: inline-block; margin: 0; padding: 0; color: #000; vertical-align: middle; font-size: 26px;font-weight: 500;} +.logo-box sub{margin-left: 5px; color: #666;} + +/* 右边导航 */ +.doc-header .nav-right{margin: 0; float: right; padding-right: 3em; margin-right: 20px !important;} +.doc-header .nav-right>*{padding: 0px; margin: 0 10px;} +.doc-header .nav-right>*:last-child{position: relative; z-index: 1002;} +.doc-header .nav-right>select{border-color: #999; color: #666; outline: 0; cursor: pointer; transition: all 0.2s; background-color: #FFF; border-width: 1px; outline: 0;} +.doc-header .nav-right>select:hover{box-shadow: 0 0 10px #aaa;} + +.github-corner{z-index: 1001 !important;} +.doc-header .nav-right .wzi{font-size: 14px; line-height: 61px; transition: color 0.2s; position: relative; text-decoration: none;} +.doc-header .nav-right .wzi::after{ + content: ''; + position: absolute; + left: 0; + right: 0; + top: calc(50% + 0.8em); + height: 2px; + background-color: var(--a-color); + transform: scaleX(0); + transform-origin: center; + transition: transform 0.25s ease; +} +.doc-header .nav-right .wzi:hover::after, +.doc-header .nav-right .wzi.nav-active::after{transform: scaleX(1);} + +.nav-right a{color: #34495E;} + +/* 搜索框 */ +.sear-box{display: inline-block; width: 180px; margin-right: 20px; line-height: 26px; text-align: left;} +.sear-box{/* position: fixed; */ } +.sear-box .search{margin-bottom: 0px; padding: 0; border: 0;} +.results-panel{border: 1px #aaa solid; border-radius: 2px; padding: 10px; max-height: 60vh; overflow: auto; position: absolute; background-color: #FFF; width: 266px;width: 316px;} +.sear-box .search input{border: 1px solid #e3e3e3; color: #345; border-radius: 15px; line-height: 30px; padding-left: 30px; transition: all 0.2s;} +.sear-box .search input{background: #fff url(./search-icon.svg) 10px 8px no-repeat; background-size: 14px;} +.clear-button{display: none !important;} + +/* 工具栏超链接 展开、收缩div */ +.zk-box{display: inline-block;} +/* 外层盒 */ +.zk-box .zk-context{max-height: 0px; position: absolute; overflow: hidden;} +.zk-box:hover .zk-context{max-height: 400px;} +/* 内层盒 */ +.zk-context>div{padding: 1em 0.5em 1em 1em; border: 1px #ccc solid; border-radius: 2px; background-color: #FFF; font-size: 12px; transition: all 0.2s; opacity: 0;} +.zk-box:hover .zk-context>div{opacity: 1;} +/* 小链接 */ +.zk-box .zk-context a{font-size: 14px; display: block; line-height: 32px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;} +.zk-box .zk-context a{text-align: left; padding: 0 1.5em 0 1em;} +.zk-box .zk-context .zk-fengexian{border-bottom: 1px #d9d9d9 solid; margin: 10px 0;} + +/* 下三角小图标 */ +.zk-icon{display: inline-block; width: 0px; height: 0px; position: relative;top: 3px; margin-left: 4px;} +.zk-icon{border-style: solid; border-width: 5px; border-color: #aaa transparent transparent transparent; } + +/* + * 版本 / 语言下拉:同一套样式 + * 语言框用 #translateSelectLanguage 写死——切换语种时 translate.js 会重绘 select, + * 临时 class 会被冲掉,不能指望 JS 事后补 class + */ +.select-version, +#translateSelectLanguage{ + background-color: transparent !important; +} +.nav-left .select-version, +#translateSelectLanguage{ + display: inline-block !important; + vertical-align: middle; + margin-left: 10px !important; + border: 1px solid #999 !important; + color: #666 !important; + outline: 0; + cursor: pointer; + transition: all 0.2s; + background-color: #FFF; + font-size: 12px !important; + padding: 2px 2px !important; + position: relative; + top: 2px; + appearance: auto; + -webkit-appearance: menulist; + height: 22px !important; + line-height: 16px !important; + box-sizing: border-box; +} +.nav-left .select-version:hover, +#translateSelectLanguage:hover{box-shadow: 0 0 10px #aaa;} + +/* 语言框单独限宽:长语种名(如 Bahasa Indonesia)别把顶栏撑爆 */ +#translateSelectLanguage{ + width: 88px !important; + max-width: 88px !important; +} + +/* + * 语言容器用 contents:不生成盒子,内部 select 跟版本 select 同级排版 + * 否则多一层 span,高低会对不齐 + */ +.nav-left .select-language-box{ + display: contents; +} + + +/* ------- 调整一下左侧树的字体样式 ------- */ +.main-box .sidebar{padding-top: 25px; margin-top: 60px;} +.sidebar .sidebar-nav>ul>li>p{/* font-size: 1.2em; */ margin-top: 10px;} +.sidebar .sidebar-nav>ul>li> strong{/* font-size: 1.2em; */ margin-top: 10px;} +/* .sidebar ul li a{color: #222;} */ +.sidebar .sidebar-nav>ul>li>ul>li>a{/* color: #222; */font-size: 14px; /* font-weight: 700; */} +.main-box .sidebar-nav ul li{margin-top: 0; margin-bottom: 0;} +.main-box .sidebar ul li a{color: #00323c;} + +/* 做到悬浮出现下划线的效果 */ +.main-box .sidebar>.sidebar-nav>ul{padding-left: 6px;} +.main-box .sidebar li a:hover{color: #42b983;} +/* .main-box .sidebar li{white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin: 5px 0;} +.main-box .sidebar li a{display: inline; line-height: 30px; padding: 5px 0 2px;} +.main-box .sidebar li a:hover{text-decoration: none; color: #42b983; border-bottom: 1px #42b983 solid;} +.main-box .sidebar li.active>a{border: 0px;} +.main-box .sidebar li.active>a:after{content: ''; position: absolute; height: 30px; right: 0; border-right: 3px #42b983 solid;} */ + +.sidebar .sidebar-nav>ul>li>ul>li.active-rep>a{ color: #42B983; font-weight: 700; } + + + +/* .main-box .sidebar .app-sub-sidebar li:before{float: none;} */ + +/* ============== code代码样式优化 ================ */ + +.main-box .markdown-section code, .main-box .markdown-section pre{background-color: rgba(0, 0, 0, 0.04);} + +/* 背景变黑 */ +.main-box [data-lang]{padding: 0px !important; border-radius: 2px;overflow-x: auto; overflow-y: hidden;} +.main-box [v-pre] code{border: 0px red solid; border-radius: 0px; /* background-color: #282828; */ background-color: #191919; color: #FFF;} +.main-box [v-pre] code{padding: 1.5em 1.3em; margin-left: 40px !important;} +/* .main-box h2{margin-top: 70px;} */ + +/* 代码行号盒子样式 */ +.code-line-box {list-style-type: none; border-right: 1px solid #000; position: absolute; top: 0; left: 0; width: 40px; user-select: none;} +.code-line-box {padding: calc(1.5em + 1px) 0px !important; padding-bottom: calc(1.5em + 20px) !important; margin: 0px !important;} +.code-line-box {line-height: inherit !important; background-color: #191919; color: #aaa;font-weight: 400;font-size: 0.85em;text-align: center;} + +/* xml语言样式优化 */ +/* .lang-xml .token.comment{color: #CDAB53;} */ +.lang-xml .token.tag *{color: #db2d20;} +.lang-xml .token.attr-value{color: #A6E22E;} + +/* html语言样式优化 */ +.lang-html .token.comment{color: #CDAB53;} +.lang-html .token.tag *{color: #db2d20;} +.lang-html .token.tag .attr-name, +.lang-html .token.tag .attr-name *{color: #A6E22E; opacity: 0.9;} +.lang-html .token.tag .attr-value, +.lang-html .token.tag .attr-value *{color: #E6DB74; opacity: 0.9;} +.lang-html .token.annotation.punctuation{color: #ddd;} +.lang-html .token.punctuation{color: #ddd;} + +/* java语言样式优化 */ +.main-box .lang-java{color: #01a252 !important;; opacity: 1;} +.lang-java .token.keyword{color: #db2d20;} +.lang-java .token.namespace,.lang-java .token.namespace *{color: #01A252; opacity: 1;} +.lang-java .token.class-name,.lang-java .cm-variable{color: #55b5db; opacity: 1;} +/* .lang-java .token.comment{color: #CDAB53;} */ +.lang-java .token.annotation.punctuation{color: #ddd;} +.lang-java .token.punctuation{color: #ddd;} + +/* cmd语言样式优化 */ +.main-box .lang-cmd{color: #01A252 !important; opacity: 1;} + +/* url语言样式优化 */ +.main-box .lang-url{color: #E96917 !important; opacity: 1;} + +/* js语言样式优化 */ +.main-box .lang-js{color: #01a252 !important;} +/* .lang-js .token.comment{color: #CDAB53;} */ +/* .lang-js .token.string{color: #fded02;} */ +.lang-js .token.string{color: #ddd;} +.lang-js .token.punctuation{color: #ddd;} + +/* yaml 和 properties 语言优化 */ +.lang-yaml .token.punctuation{color: #eee;} +.lang-properties .token.attr-name{color: #22a2c9;} + + +/* ------- markdown 内容样式优化 ------- */ + +/* GitHub折线图最大宽度 */ +[alt=github-chart]{max-width: 897px;} +/* 大屏幕时,某些图片限制一下宽度 */ +@media screen and (min-width: 800px) { + [title=s-w],[title=s-w-sh]{max-width: 80%;} + .s-w,.s-w-sh{ max-width: 80%; } +} +.s-w-sh, [title=s-w-sh]{display: inline-block; border: 1px #eee solid;} +.w-100, [title=w-100]{display: inline-block; border: 1px #eee solid; max-width: 100%;} + +/* 鼠标悬浮时切换img */ +.hover-change-img {border: 1px #eee solid; max-width: 100%; } +.hover-change-img:hover img:first-child{ display: none; } +.hover-change-img img:last-child{ display: none; } +.hover-change-img img:first-child{ display: inline-block; } +.hover-change-img:hover img:last-child{ display: inline-block; } + +/* 公众号table */ +.gzh-table{ /* table-layout:fixed !important; */} +/* .gzh-table,.gzh-table tr,.gzh-table td{display: block !important;} */ +/* .gzh-table tbody{display: block !important; width: 100% !important;} */ +#main .gzh-table tr{background-color: #FFF;} +.gzh-table td{padding: 20px !important; width: 20%; border: 0;} +.gzh-table td b{display: block; margin-bottom: 10px; } + +/* tab选项卡优化 */ +/* .docsify-tabs--classic{background-color: rgba(255, 255, 255, 0.2);} */ +.docsify-tabs__tab{outline: 0; cursor: pointer;} +.docsify-tabs--classic .docsify-tabs__tab--active{box-shadow: 0 0 0;} +/* tab卡片插件样式优化 */ +.main-box{ + --docsifytabs-border-color: #ddd; + --docsifytabs-tab-color: #777; +} + + +/* 调整表格的响应式 */ +#main table{margin-left: 0px;} +@media screen and (min-width: 800px) { + #main table tr th{min-width: 100px;} +} + +/* 提示框加上灰色背景 */ +.main-box .markdown-section blockquote{padding: 1px 24px 1px 30px; background-color: #f8f8f8;} + +/* 行级代码样式 */ +blockquote code {font-weight: 400;} + +/* 赞助列表 */ +.zanzhu-box{margin-top: -10px;} +.zanzhu-box table tr td:nth-child(2){color: red;} +#main .zanzhu-box table tr td:first-child a{border-color: rgba(0,0,0,0); color: inherit;} +#main .zanzhu-box table tr td:first-child a:hover{border-color: var(--a-hover-color); color: var(--a-hover-color);} + +/* 展开和收起 */ +#main .zanzhu-box{/* height: 500px; */ overflow-y: hidden; transition: all 1.5s;} +#main .zanzhu-box table{display: table;} +.zhankai-btn-box{margin-top: 10px;} +.zk-btn--1,.zk-btn--2{cursor: pointer;} +.zk-btn--2{display: none;} + +/* 角标位置修复 */ +.badge-box a:nth-child(-n+2) img{position: relative; top: 1px;} + +body { + --a-color: #01a252; + --a-hover-color: #0969da; +} + +/* 超链接样式 */ +#main *:not(h1,h2,h3,h4,h5,h6) a{font-weight: 400; text-decoration: none; font-family: "思源黑体";} +#main *:not(h1,h2,h3,h4,h5,h6) a{color: var(--a-color); border-bottom: 1px var(--a-color) solid;} +#main *:not(h1,h2,h3,h4,h5,h6) a:hover{color: var(--a-hover-color); border-bottom: 1px var(--a-hover-color) solid;} + +#main .un-dec-a-pre+p a, +#main p[align=center] a, +#main .doc-inline-ad a, +#main .doc-inline-ad a:hover{border-bottom: 0; text-decoration: none;} + +/* 正文区标题下广告(独立样式,不与右侧栏 top-ad-box 共用) */ +#main .doc-inline-ad{font-size: 12px; margin: 16px 0 20px;} +#main .doc-inline-ad__card{position: relative; padding: 10px 28px 10px 10px; background-color: #F4F8FA; overflow: hidden;} +#main .doc-inline-ad__close{ + position: absolute; + top: 6px; + right: 8px; + z-index: 1; + width: 20px; + height: 20px; + line-height: 18px; + text-align: center; + font-size: 18px; + color: #aaa; + cursor: pointer; + user-select: none; + text-decoration: none; +} +#main .doc-inline-ad__close:hover{color: #666; text-decoration: none;} +#main .doc-inline-ad__body{display: flex; align-items: center; gap: 16px;} +#main .doc-inline-ad__img{height: 80px; width: auto; flex-shrink: 0; border: 0; border-radius: 0; box-shadow: none;} +#main .doc-inline-ad__img:hover{box-shadow: none;} +#main .doc-inline-ad__text{flex: 1; display: flex; flex-direction: column; gap: 6px;} +#main .doc-inline-ad__text p{margin: 0; color: #999; font-size: 12px; font-weight: 400; line-height: 1.5; word-break: break-all;} + +#main footer a.footer-beian, +#main footer a.footer-beian:hover{font-size: 12px; color: #999; border-bottom: 1px #999 solid;} +#main footer a.footer-beian:hover{color: #777; border-bottom-color: #777;} + +/* toc目录树 */ +.toc-box>li{margin-bottom: 15px;} +.toc-box .toc-h2{list-style-type: none; font-size: 18px; margin-top: 20px;} +.toc-box .toc-h3,.toc-box .toc-h4{margin-left: 1em;} +.toc-box .toc-h5,.toc-box .toc-h6{margin-left: 2em;} +#main .toc-box .toc-h2 a span{color: #34495e;} +#main .toc-box a{border-color: rgba(0,0,0,0); transition: 0s;} +#main .toc-box a span{color: inherit;} + + +/* 加载图片的按钮 */ +.show-img{ + background-color: #FFF; + padding: 8px 15px; + border: 1px #42b983 solid; + color: #42b983; + cursor: pointer; + border-radius: 2px; + transition: all 0.2s; +} +.show-img:hover{ + background-color: #eaf6eb; +} +.show-to-img{cursor: pointer;} + +/* 保证点开图片时在最上面 */ +.medium-zoom-image.medium-zoom-image--opened{ + z-index: 10000; +} + + +/* ------------- 答题按钮 ------------- */ +#main .dt-btn,#main .case-btn{ + background-color: #e7ecf3; + color: #385481; + display: inline-block; + border: 1px #d7dce3 solid !important; + border-radius: 1px; + /* border-bottom-width: 0px !important; */ + + margin-top: 10px; + width: 100%; + padding: 8px 14px; + font-size: 14px; + transition: all 0.15s; + text-decoration: none !important; + font-weight: 400; + + /* 背景 */ + background-image: url(icon/dati.svg); + background-repeat: no-repeat; + background-size: 20px 20px; + background-position: 1em 12px; + text-indent: 2em; +} +/* 代码示例按钮 */ +#main .case-btn{ + background-color: #feedeb; + color: #dd4949; + border: 1px #decdcb solid !important; + background-size: 18px 18px; + background-image: url(icon/code.svg); +} +#main .case-btn.case-btn-video{ + background-color: #ECF5FF; + color: #1979DA; + border: 1px #49A9DA solid !important; + background-image: url(icon/video.svg); +} +#main .dt-btn{display: none;} + + +/* ------------- 背景色相关 ------------- */ +/* 侧边栏需要透明 */ +.sidebar-toggle{background-color: transparent !important;} +.sidebar{background-color: transparent !important;} + +/* 变色的动画 */ +.doc-header{transition: background-color 0.3s !important;} + +/* 调色按钮 */ +.theme-btn{width: 25px; height: 25px; line-height: 60px; vertical-align: middle; position: relative; top: -1px;} +.theme-box{width: 156px; text-align: left; line-height: 20px; margin-top: -20px;} +.theme-box span{ + display: inline-block; + width: 20px; + height: 20px; + margin: 1px 2px; + border: 1px #ccc solid; + cursor: pointer; + border-radius: 1px; +} + +/* ------------- details标签 ------------- */ +.main-box details{ + border: 1px #42B983 solid; + background-color: #f4fdef; + overflow: hidden; + max-height: 44px; + margin-bottom: 1em; + /* transition: all 1s; */ +} +.main-box details[open]{ /* max-height: 1000px; */ overflow: auto; animation: slideDown 0.6s linear both;} +@keyframes slideDown { + 0% { max-height: 44px; overflow: hidden; } + 99% { max-height: 1500px; overflow: hidden; } + 100% { max-height: 1500px; overflow: auto; } +} +.main-box details summary{ + padding: 11px 14px; + background-color: #f4fdef; + color: #01a252; + cursor: pointer; +} +.main-box details pre{ + margin-left: 1em; + margin-right: 1em; +} +.main-box details table{margin-left: 1em !important; margin-right: 1em; width: auto;} +.main-box details p{padding: 0 14px;} + +/* 广告盒子 */ +.ad-title{ font-size: 12px; color: #aaa; padding-bottom: 8px; margin-bottom: 14px; border-bottom: 1px #aaa solid; } +.ad-tips{margin-bottom: 5px;} +.ad-close{float: right;} +.ad-close:hover{cursor: pointer; text-decoration: underline; color: red;} + +.main-box .top-ad-box{font-size: 12px;} +.main-box .top-ad-box a{border-bottom: 0px; text-decoration: none;} +.main-box .top-ad-box a:hover{border-bottom: 0px;} +.main-box .top-ad-box a img{border: 1px #eee solid; width: 100%; /* max-height: 80px; */ border-radius: 2px; transition: all 0.1s !important;} +.main-box .top-ad-box a img:hover{box-shadow: 0 0 20px #ddd;} + +.mad-bg-box{ padding: 10px; background-color: #F4F8FA; /* border: 1px #eee solid; */ overflow: hidden; } +.mad-wh-box{ width: 100%; } +.main-box .top-ad-box .mad-img{ width: 130px; float: left; margin-right: 10px; border: 0px; } +.main-box .top-ad-box .mad-text{ float: left; width: calc(100% - 140px); color: #000; font-size: 14px; line-height: 20px; word-break: break-all; } + +.main-box .top-ad-box2 a img{ width: 48.5%; margin-bottom: 2px; } +.main-box .top-ad-box2 a:nth-child(2n+1) img{margin-right: 2px; } + + +/* 帮助按钮 */ +.help-btn{transition: all 0.5s; text-align: center; background-color: rgba(255, 255, 255, 0.5); cursor: pointer; font-size: 13px; line-height: 40px;} +.help-btn{ margin-top: 20px; border: 1px #42b983 solid; color: green;} +.help-btn a{ text-decoration: none !important; color: inherit; height: 100%; width: 100%; display: inline-block; } +.help-btn:hover{box-shadow: 0 0 20px #D1EEE1 !important;} +.xiaozhushou-intro p{line-height: 14px;} + +/* ew-wa */ +.ew-wa{ margin-top: 14px; line-height: 18px; color: #aaa; } +.ew-wa a{ margin-right: 5px; text-decoration: none; color: #8693A7; } +.ew-wa a:hover{text-decoration: underline; color: #44f; } + +/* 按钮发光动画 */ +/* .help-btn{animation: helpbtnanimation 3s infinite;} +@keyframes helpbtnanimation{ + 0%{box-shadow: 0 0 1px #42B983;} + 50%{box-shadow: 0 0 20px #42B983;} + 100%{box-shadow: 0 0 20px #FFF;} +} */ + +/* ********** 赞助者名单 ******** */ +.zanzhu-table{text-align: left;} +/* 赞助排序盒子 */ +.zanzhu-sort-box{font-size: 14px; margin-bottom: 10px;} +.zanzhu-sort-box .zanzhu-sort-btn{text-decoration: none; color: #999; cursor: pointer;} +.zanzhu-sort-box .zanzhu-sort-btn:hover{text-decoration: underline; color: #557;} +.zanzhu-sort-box .zanzhu-sort-btn.zz-sort-native{text-decoration: underline; color: #557;} +/* 底部按钮盒子 */ +.zz-btn-box{color: #666; font-size: 14px;} +.zz-btn-box button{padding: 5px 10px; cursor: pointer; border: 1px #ccc solid; color: #999; background-color: #FFF;} +.zz-btn-box button:hover{box-shadow: 0 0 10px #ddd;} + +.syzz-show-btn{border: 1px #ccc solid; padding: 5px 10px; background-color: #FFF; color: #666; cursor: pointer;} +.syzz-show-btn:hover{box-shadow: 0 0 10px #ddd;} + +/* ********** 团队成员名单 ******** */ +.markdown-section .team-table{ display: table; text-align: left; } +.team-table img{ width: 45px; height: 45px; } + +/* ajax加载时的转圈圈样式 */ +.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} +.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} +.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } + + +/* 万维广告 */ +.wwads-cn{margin-top: 0px !important;} +.wwads-cn>a>img{width: 80px !important;} + +/* 提示框 */ +.main-box .alert{ border-radius: 0px !important; } +/* .main-box .alert ul,.main-box .alert ol{ margin-top: -5px; margin-bottom: -10px; } */ +.main-box .alert.tip .title .icon.icon-tip{ + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%2301354d' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM8 5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/%3E%3C/svg%3E"); +} +.main-box .alert.flat.note{background-color: #E8F4FF;} +.main-box .alert.flat.tip{background-color: #F0F9EB;} +.main-box .alert.flat.warning{background-color: #FDF6EC;} + +/* 公安备案 */ +.foot-beian-gongan{display:inline-flex;align-items:center;gap:4px;vertical-align:middle;} +.foot-beian-gongan-icon{width:16px;height:16px;flex-shrink:0;} +#main footer a.footer-beian img{width:14px;height:14px;vertical-align:-2px;margin-right:3px;} diff --git a/sa-token-doc/static/docsify-copy-code.min.js b/sa-token-doc/static/docsify-copy-code.min.js new file mode 100644 index 000000000..d48cab493 --- /dev/null +++ b/sa-token-doc/static/docsify-copy-code.min.js @@ -0,0 +1,9 @@ +/*! + * docsify-copy-code + * v2.1.0 + * https://github.com/jperasmus/docsify-copy-code + * (c) 2017-2019 JP Erasmus + * MIT license + */ +!function(){"use strict";function r(o){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o})(o)}!function(o,e){void 0===e&&(e={});var t=e.insertAt;if(o&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],c=document.createElement("style");c.type="text/css","top"===t&&n.firstChild?n.insertBefore(c,n.firstChild):n.appendChild(c),c.styleSheet?c.styleSheet.cssText=o:c.appendChild(document.createTextNode(o))}}(".docsify-copy-code-button,.docsify-copy-code-button span{cursor:pointer;transition:all .25s ease}.docsify-copy-code-button{position:absolute;z-index:1;top:0;right:0;overflow:visible;padding:.65em .8em;border:0;border-radius:0;outline:0;font-size:1em;background:grey;background:var(--theme-color,grey);color:#fff;opacity:0}.docsify-copy-code-button span{border-radius:3px;background:inherit;pointer-events:none}.docsify-copy-code-button .error,.docsify-copy-code-button .success{position:absolute;z-index:-100;top:50%;left:0;padding:.5em .65em;font-size:.825em;opacity:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.docsify-copy-code-button.error .error,.docsify-copy-code-button.success .success{opacity:1;-webkit-transform:translate(-115%,-50%);transform:translate(-115%,-50%)}.docsify-copy-code-button:focus,pre:hover .docsify-copy-code-button{opacity:1}"),document.querySelector('link[href*="docsify-copy-code"]')&&console.warn("[Deprecation] Link to external docsify-copy-code stylesheet is no longer necessary."),window.DocsifyCopyCodePlugin={init:function(){return function(o,e){o.ready(function(){console.warn("[Deprecation] Manually initializing docsify-copy-code using window.DocsifyCopyCodePlugin.init() is no longer necessary.")})}}},window.$docsify=window.$docsify||{},window.$docsify.plugins=[function(o,s){o.doneEach(function(){var o=Array.apply(null,document.querySelectorAll("pre[data-lang]")),c={buttonText:"Copy to clipboard",errorText:"Error",successText:"Copied"};s.config.copyCode&&Object.keys(c).forEach(function(t){var n=s.config.copyCode[t];"string"==typeof n?c[t]=n:"object"===r(n)&&Object.keys(n).some(function(o){var e=-1',''.concat(c.buttonText,""),''.concat(c.errorText,""),''.concat(c.successText,""),""].join("");o.forEach(function(o){o.insertAdjacentHTML("beforeend",e)})}),o.mounted(function(){document.querySelector(".content").addEventListener("click",function(o){if(o.target.classList.contains("docsify-copy-code-button")){var e="BUTTON"===o.target.tagName?o.target:o.target.parentNode,t=document.createRange(),n=e.parentNode.querySelector("code"),c=window.getSelection();t.selectNode(n),c.removeAllRanges(),c.addRange(t);try{document.execCommand("copy")&&(e.classList.add("success"),setTimeout(function(){e.classList.remove("success")},1e3))}catch(o){console.error("docsify-copy-code: ".concat(o)),e.classList.add("error"),setTimeout(function(){e.classList.remove("error")},1e3)}"function"==typeof(c=window.getSelection()).removeRange?c.removeRange(t):"function"==typeof c.removeAllRanges&&c.removeAllRanges()}})})}].concat(window.$docsify.plugins||[])}(); +//# sourceMappingURL2=docsify-copy-code.min.js.map \ No newline at end of file diff --git a/sa-token-doc/static/docsify-plugin.js b/sa-token-doc/static/docsify-plugin.js new file mode 100644 index 000000000..700d283ae --- /dev/null +++ b/sa-token-doc/static/docsify-plugin.js @@ -0,0 +1,219 @@ +// 声明 docsify 插件 +var myDocsifyPlugin = function(hook, vm) { + + // 文档页顶栏:根据当前路由高亮对应导航项 + function updateDocNavActive() { + var header = document.querySelector('.doc-header .nav-right'); + if (!header) return; + + var hash = location.hash || '#/'; + var onDocPage = location.pathname.indexOf('doc.html') !== -1; + + header.querySelectorAll('.wzi').forEach(function(a) { + a.classList.remove('nav-active'); + }); + + header.querySelectorAll(':scope > a.wzi').forEach(function(a) { + var href = a.getAttribute('href'); + if (!href || href.indexOf('javascript') === 0 || href.indexOf('http') === 0) return; + if (href.indexOf('#/') === 0 && (hash === href || hash.indexOf(href + '/') === 0)) { + a.classList.add('nav-active'); + } + }); + + header.querySelectorAll(':scope > .zk-box').forEach(function(box) { + var trigger = box.querySelector(':scope > .wzi'); + if (!trigger) return; + box.querySelectorAll('.zk-context a[href^="#/"]').forEach(function(link) { + var href = link.getAttribute('href'); + if (hash === href || hash.indexOf(href + '/') === 0) { + trigger.classList.add('nav-active'); + } + }); + }); + + var docLink = header.querySelector(':scope > a.wzi[href*="doc.html"]'); + if (docLink && onDocPage) { + var hashNavActive = header.querySelector(':scope > a.wzi.nav-active[href^="#/"]'); + var zkNavActive = header.querySelector(':scope > .zk-box > .wzi.nav-active'); + if (!hashNavActive && !zkNavActive) { + docLink.classList.add('nav-active'); + } + } + } + + // 功能6:标题下面的广告(正文区 doc-inline-ad,仅 SSO / OAuth2 章节) + function renderDocInlineAd(routePath) { + if (!/^\/(sso|oauth2)\//.test(routePath) || $(window).width() < 800) { + return; + } + + var ad = ``; + + // 如果一周内用户点击过关闭广告,则不再展现 + var allowJg = 1000 * 60 * 60 * 24 * 7; + try { + var closeAdTime = localStorage.closeMdTopAdTime; + if (closeAdTime) { + var closeAdJg = new Date().getTime() - parseInt(closeAdTime); + if (closeAdJg < allowJg) { + return; + } + } + } catch (e) { + console.error(e); + } + + $('#main h1').after(ad); + + $('#main .doc-inline-ad .doc-inline-ad__close').click(function(e) { + e.preventDefault(); + e.stopPropagation(); + layer.confirm('关闭后,一周内不再展现此信息', function() { + $('#main .doc-inline-ad').fadeOut(1000); + layer.msg('关闭成功'); + localStorage.closeMdTopAdTime = new Date().getTime(); + }); + }); + } + + // 钩子函数:解析之前执行 + hook.beforeEach(function(content) { + try{ + // 功能 1,替换全局变量 + content = content.replace(/\$\{sa.top.version\}/g, window.saTokenTopVersion); + + // 添加 [toc] 标记 + content = content.replace(/\[\[toc\]\]/g, '
'); + + }catch(e){ + // + } + return content; + }); + + // 钩子函数:每次路由切换时,解析内容之后执行 + hook.afterEach(function(html) { + + // 功能 2,文章底部添加仓库地址 + var giteeUrl = `https://gitee.com/dromara/sa-token/tree/dev/sa-token-doc/${vm.route.file}`; + var githubUrl = `https://github.com/dromara/sa-token/tree/dev/sa-token-doc/${vm.route.file}`; + var atomgitUrl = `https://atomgit.com/dromara/sa-token/tree/dev/sa-token-doc/${vm.route.file}`; + var footer = ` +







+
+ 发现错误? 您可以在 GiteeGitHubAtomGit 帮助我们完善此页文档! + 或 加入讨论群 交流反馈。 +
+

我们坚信,即使再复杂的技术,也可以用清晰、干练、易懂的文字描述出它的具体细节,如果你在阅读文档时有难以理解的章节,那一定是我们还没有优化好它, + 请向我们 反馈 你的困惑之处,我们将持续优化文档。

+
+ 鲁ICP备18046274号-5 + | + 鲁公网安备37011202002956号 +
+ `; + return html + footer; + }); + + // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 + hook.doneEach(function() { + + // 功能3,给代码盒子,添加行数样式 + $('pre code').each(function(){ + var lines = $(this).text().split('\n').length; + var $numbering = $('
    ').addClass('code-line-box'); + $(this) + .addClass('has-numbering') + .parent() + .append($numbering); + for(i=1;i<=lines;i++){ + $numbering.append($('
  • ').text(i)); + } + }); + + // 功能4,添加 toc 目录 + var dStr = ""; + $('#main h2, #main h3, #main h4, #main h5, #main h6').each(function() { + $('.toc-box').append('
  • ' + this.innerHTML + '
  • '); + }); + + // 功能5,统计赞助人数 + // if($('.zanzhu-count').length && $('.zanzhu-box table').length) { + // $('.zanzhu-count').html($('.zanzhu-box table tr').length); + // } + + // 功能5,渲染赞助数据 + if($('.zanzhu-table').length) { + // $('.zanzhu-count').html($('.zanzhu-box table tr').length); + // console.log(123); + renderDonateTable(); + onZanzhuSortClick(); + } + + // 功能7,更新文档导航高亮 + updateDocNavActive(); + + // 功能6,标题下面的广告 + renderDocInlineAd(vm.route.path); + + }); + + // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 + hook.ready(function() { + updateDocNavActive(); + window.addEventListener('hashchange', updateDocNavActive); + + // 将搜索框转移到右上角 + document.querySelector(".sear-box").innerHTML = ''; + document.querySelector(".sear-box").append(document.querySelector(".search")); + document.querySelector(".search input").placeholder = '搜索…'; + + // 点击input时,展开 + $('.sear-box input').click(function() { + if($('.search input').val() != '') { + $('.results-panel').addClass('show'); + } + }); + // 失去焦点时,收缩 + $('.sear-box input').blur(function() { + setTimeout(function() { + $('.results-panel').removeClass('show'); + }, 200); + }) + // 选择一项时,收缩 + $('.sear-box').on('click', '.matching-post', function() { + console.log('click……'); + // $('.search input').val(''); + $('.results-panel').removeClass('show'); + }); + + // 点击按钮,加载图片 + $(document).on('click', '.show-img', function(){ + var src = $(this).attr('img-src'); + var img = ''; + $(this).after(img); + $(this).remove(); + }) + + // 点击按钮,加载图片 + $(document).on('click', '.show-to-img', function(){ + open(this.src); + }) + + }); + +} \ No newline at end of file diff --git a/sa-token-doc/static/docsify-plugins/docsify-betterembed-1.1.1.js b/sa-token-doc/static/docsify-plugins/docsify-betterembed-1.1.1.js new file mode 100644 index 000000000..752788840 --- /dev/null +++ b/sa-token-doc/static/docsify-plugins/docsify-betterembed-1.1.1.js @@ -0,0 +1,5 @@ +const PMEregexGetImport=/(.*?)/gs,PMEregexReplaceImport=e=>new RegExp(`(.*?)`,"gs"),PMEregexGetImportName=//g,PMEregexGetEmbedImportName=/^(.*?).md#(.*?) ':include'\)$/gm;function PMEcreateElementFromHTML(e){var t=document.createElement("div");return t.innerHTML=e.trim(),t}function partialMarkdownEmbed(n,e){n.beforeEach(m=>{if(PMEregexGetEmbedImportName.test(m))return m.match(PMEregexGetEmbedImportName).forEach(e=>{var t=e.split(".md#")[1].split(" ':include')")[0],r=e.replace("#"+t,"");m=m.replace(e,` + +${r} + +`)}),m}),n.afterEach(a=>{if(PMEregexGetImport.test(a))return a.match(PMEregexGetImport).forEach(e=>{var t,r=PMEcreateElementFromHTML(e);const m=[];for(let e=1;e<6;e++)0===m.length&&0!==(t=r.querySelectorAll("div > h"+e)).length&&t.forEach(e=>m.push(e.id));n.doneEach(()=>{const t=window.location.hash.split("?id=")[0];m.forEach(e=>{document.querySelectorAll(`.section-link[href='${t}?id=${e}']`).forEach(e=>{e.parentElement.nextElementSibling.remove(),e.parentElement.remove()})})});var o=e.match(PMEregexGetImportName)[0].split("\x3c!-- embedImport:start:")[1].split(" --\x3e")[0],e=e.split(``)[1].split(``)[0];a=a.replace(PMEregexReplaceImport(o),e)}),a})}window.$docsify=window.$docsify||{},$docsify.plugins=[partialMarkdownEmbed,...$docsify.plugins||[]]; \ No newline at end of file diff --git a/sa-token-doc/static/docsify-plugins/docsify-plugin-flexible-alerts.min-1.1.1.js b/sa-token-doc/static/docsify-plugins/docsify-plugin-flexible-alerts.min-1.1.1.js new file mode 100644 index 000000000..2ebc31835 --- /dev/null +++ b/sa-token-doc/static/docsify-plugins/docsify-plugin-flexible-alerts.min-1.1.1.js @@ -0,0 +1,9 @@ +/*! + * docsify-plugin-flexible-alerts + * v1.1.1 + * https://github.com/fzankl/docsify-plugin-flexible-alerts#readme + * (c) 2022 Fabian Zankl + * MIT license + */ +!function(){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}!function(t,e){void 0===e&&(e={});var a=e.insertAt;if(t&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],l=document.createElement("style");l.type="text/css","top"===a&&o.firstChild?o.insertBefore(l,o.firstChild):o.appendChild(l),l.styleSheet?l.styleSheet.cssText=t:l.appendChild(document.createTextNode(t))}}(".alert{word-wrap:break-word;display:block;margin-bottom:1rem!important;padding:.75rem 1.25rem!important;position:relative;word-break:break-word}.alert>*{max-width:100%}.alert>:first-child{margin-top:0}.alert>:last-child{margin-bottom:0}.alert:before{content:unset!important}.alert+.alert{margin-top:-.25rem!important}.alert p{margin-bottom:.5rem;margin-top:.5rem}.alert .title{align-items:center;display:flex;flex-wrap:wrap;font-weight:600;margin:0}.icon{background-repeat:no-repeat;display:inline-block;height:16px;margin-right:.5rem;width:16px}.alert.callout{background:var(--background);border:1px solid #eee;border-left-width:.25rem;border-radius:.25rem}.alert.callout.note{border-left-color:#17a2b8!important}.alert.callout.note .title{color:#17a2b8}.alert.callout.note .icon-note{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%2317a2b8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM8 5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/%3E%3C/svg%3E\")}.alert.callout.tip{border-left-color:#28a745!important}.alert.callout.tip .title{color:#28a745}.alert.callout.tip .icon-tip{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 352 512' fill='%2328a745' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.52 18.85 42.36 58.23 52.21 91.45.04.26.07.52.11.78h160.24c.04-.26.07-.51.11-.78 9.85-33.22 35.69-72.6 52.21-91.45C335.55 260.85 352 220.37 352 176 352 78.61 272.91-.3 175.45 0 73.44.31 0 82.97 0 176zm176-80c-44.11 0-80 35.89-80 80 0 8.84-7.16 16-16 16s-16-7.16-16-16c0-61.76 50.24-112 112-112 8.84 0 16 7.16 16 16s-7.16 16-16 16z'/%3E%3C/svg%3E\")}.alert.callout.warning{border-left-color:#f0ad4e!important}.alert.callout.warning .title{color:#f0ad4e}.alert.callout.warning .icon-warning{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 17 16' fill='%23f0ad4e' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 5zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z'/%3E%3C/svg%3E\")}.alert.callout.attention{border-left-color:#dc3545!important}.alert.callout.attention .title{color:#dc3545}.alert.callout.attention .icon-attention{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%23dc3545' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath fill-rule='evenodd' d='M11.354 4.646a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708l6-6a.5.5 0 0 1 .708 0z'/%3E%3C/svg%3E\")}.alert.flat{background-color:#e2e3e5;border:1px solid #d6d8db;border-radius:.125rem;color:#383d41}.alert.flat.note{background-color:#cdeefd;border-color:#b4e6fc;color:#02587f}.alert.flat.note .title{color:#01354d}.alert.flat.note .icon-note{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%2301354d' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM8 5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/%3E%3C/svg%3E\")}.alert.flat.tip{background-color:#dbefdc;border-color:#c9e7cb;color:#285b2a}.alert.flat.tip .title{color:#18381a}.alert.flat.tip .icon-tip{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 352 512' fill='%2318381a' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.52 18.85 42.36 58.23 52.21 91.45.04.26.07.52.11.78h160.24c.04-.26.07-.51.11-.78 9.85-33.22 35.69-72.6 52.21-91.45C335.55 260.85 352 220.37 352 176 352 78.61 272.91-.3 175.45 0 73.44.31 0 82.97 0 176zm176-80c-44.11 0-80 35.89-80 80 0 8.84-7.16 16-16 16s-16-7.16-16-16c0-61.76 50.24-112 112-112 8.84 0 16 7.16 16 16s-7.16 16-16 16z'/%3E%3C/svg%3E\")}.alert.flat.warning{background-color:#ffddd3;border-color:#ffc9ba;color:#852d12}.alert.flat.warning .title{color:#581e0c}.alert.flat.warning .icon-warning{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 17 16' fill='%23581e0c' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 5zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z'/%3E%3C/svg%3E\")}.alert.flat.attention{background-color:#fdd9d7;border-color:#fcc2bf;color:#7f231c}.alert.flat.attention .title{color:#551713}.alert.flat.attention .icon-attention{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%23551713' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath fill-rule='evenodd' d='M11.354 4.646a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708l6-6a.5.5 0 0 1 .708 0z'/%3E%3C/svg%3E\")}"),function(){var e={style:"callout",note:{label:"Note",icon:"icon-note",className:"note"},tip:{label:"Tip",icon:"icon-tip",className:"tip"},warning:{label:"Warning",icon:"icon-warning",className:"warning"},attention:{label:"Attention",icon:"icon-attention",className:"attention"},typeMappings:{info:"note",danger:"attention"}};function a(t,e){var o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;for(var l in e)try{e[l].constructor===Object&&o<1?t[l]=a(t[l],e[l],o+1):t[l]=e[l]}catch(a){t[l]=e[l]}return t}window.$docsify=window.$docsify||{},window.$docsify.plugins=[].concat((function(o,l){var r=a(e,l.config["flexible-alerts"]||l.config.flexibleAlerts),i=function(t,e,a,o){var l=(t||"").match(new RegExp("".concat(e,":(([\\s\\w\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF-]*))")));return l?o?o(l[1]):l[1]:o?o(a):a};o.afterEach((function(e,a){a(e.replace(/<\s*blockquote[^>]*>[\s]+?(?:

    )?\[!(\w*)((?:\|[\w*:[\s\w\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]*)*?)\]([\s\S]*?)(?:<\/p>)?<\s*\/\s*blockquote>/g,(function(e,a,o,n){!r[a.toLowerCase()]&&r.typeMappings[a.toLowerCase()]&&(a=r.typeMappings[a.toLowerCase()]);var c=r[a.toLowerCase()];if(!c)return e;var d=i(o,"style",r.style),s=i(o,"iconVisibility","visible",(function(t){return"hidden"!==t})),g=i(o,"labelVisibility","visible",(function(t){return"hidden"!==t})),m=i(o,"label",c.label),u=i(o,"icon",c.icon),f=i(o,"className",c.className);if("object"===t(m)){var p=Object.keys(m).filter((function(t){return l.route.path.indexOf(t)>-1}));p&&p.length>0?m=m[p[0]]:(g=!1,s=!1)}var w=''),h='

    '.concat(s?w:"").concat(g?m:"","

    ");return'
    \n ').concat(s||g?h:"","\n

    ").concat(n,"

    \n
    ")})))}))}),window.$docsify.plugins)}()}(); +//# sourceMappingURL=docsify-plugin-flexible-alerts.min.js.map \ No newline at end of file diff --git a/sa-token-doc/static/docsify-plugins/progress.update.js b/sa-token-doc/static/docsify-plugins/progress.update.js new file mode 100644 index 000000000..f29b8189f --- /dev/null +++ b/sa-token-doc/static/docsify-plugins/progress.update.js @@ -0,0 +1,76 @@ +// 显示文档阅读进度的进度条 +// +// 修改于:https://github.com/HerbertHe/docsify-progress +// +// 1、将最外层盒子的 z-index 值从 999 修改为 9999999999 + +function plugin(hook, vm) { + let marginTop + hook.mounted(function () { + const content = document.getElementsByClassName("content")[0] + marginTop = parseFloat( + window.getComputedStyle(content).paddingTop.replace("px", "") + ) + + let insertDOM = ` +
    +
    +
    + ` + const mainDOM = document.getElementsByTagName("body")[0] + mainDOM.innerHTML = mainDOM.innerHTML + insertDOM + + function switcher() { + const body = document.getElementsByTagName("body")[0] + if (!body.classList.contains("close")) { + body.classList.add("close") + } else { + body.classList.remove("close") + } + } + + const btn = document.querySelector("div.sidebar-toggle-button") + btn.addEventListener("click", function (e) { + e.stopPropagation() + switcher() + }) + }) + hook.ready(function () { + window.addEventListener("scroll", function (e) { + let totalHeight = + marginTop + + parseFloat( + window + .getComputedStyle(document.getElementById("main")) + .height.replace("px", "") + ) + let scrollTop = + document.body.scrollTop + document.documentElement.scrollTop + let remain = totalHeight - document.body.offsetHeight + document.getElementById("progress-display").style.width = + Math.ceil((scrollTop / remain) * 100) + "%" + }) + }) +} + +// Docsify plugin options +window.$docsify["progress"] = Object.assign( + { + position: "top", + color: "var(--theme-color,#42b983)", + height: "3px", + }, + window.$docsify["progress"] +) +window.$docsify.plugins = [].concat(plugin, window.$docsify.plugins) \ No newline at end of file diff --git a/sa-token-doc/static/docsify-plugins/sub-nav-draw.js b/sa-token-doc/static/docsify-plugins/sub-nav-draw.js new file mode 100644 index 000000000..09b2fdd16 --- /dev/null +++ b/sa-token-doc/static/docsify-plugins/sub-nav-draw.js @@ -0,0 +1,61 @@ +// 提取次级导航栏显示到右上角 +// + +// 是否都开右边菜单 +let isOpenRightSubTitle = false; + + // 重新定位 active-rep 对应的菜单 +function positioningVmActiveRep(vm) { + const vmPath = '#' + vm.route.path; + $('.sidebar-nav>ul>li>ul>li>a').each(function(item) { + if($(this).attr('href') === vmPath) { + // $(this).parent().attr('active-rep', true); + $(this).parent().addClass('active-rep') + // console.log($(this)); + } + }) +} + +function subNavDraw(hook, vm) { + + // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 + hook.doneEach(function () { + // 只在宽屏下展现,太小的屏幕不展现 + if(document.body.clientWidth < 1100) { + isOpenRightSubTitle = false; + return; + } else { + isOpenRightSubTitle = true; + } + + // 修改高度 + const $dom = $('.app-sub-sidebar'); + console.log($dom, $dom.height()); + $('.doc-right-more-item').css({ top: (($dom.height() ?? 0) + 80) + 'px' }) + + // 重新定位 active-rep 对应的菜单 + positioningVmActiveRep(vm); + }) + + + // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 + hook.ready(function () { + + }) + + +} + +window.$docsify.plugins = [].concat(subNavDraw, window.$docsify.plugins) + +// 滚动时设置一下左侧滚动条高度,不要超出可视区域 +$(document).scroll(function(){ + if(isOpenRightSubTitle) { + try{ + const offsetTop = $('.active-rep').get(0).offsetTop; + $('.sidebar').scrollTop(offsetTop - ($('.sidebar').height() / 2)) + } catch (e) { + // console.log(e); + } + } +}) diff --git a/sa-token-doc/static/docsify-tabs.min.js b/sa-token-doc/static/docsify-tabs.min.js new file mode 100644 index 000000000..8c9328ac5 --- /dev/null +++ b/sa-token-doc/static/docsify-tabs.min.js @@ -0,0 +1,9 @@ +/*! + * docsify-tabs + * v1.4.4 + * https://jhildenbiddle.github.io/docsify-tabs/ + * (c) 2018-2020 John Hildenbiddle + * MIT license + */ +!function(){"use strict";!function(t,a){void 0===a&&(a={});var o=a.insertAt;if(t&&"undefined"!=typeof document){var c=document.head||document.getElementsByTagName("head")[0],e=document.createElement("style");e.type="text/css","top"===o&&c.firstChild?c.insertBefore(e,c.firstChild):c.appendChild(e),e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t))}}(':root{--docsifytabs-border-color:#ededed;--docsifytabs-border-px:1px;--docsifytabs-margin:1.5em 0;--docsifytabs-tab-background:#f8f8f8;--docsifytabs-tab-background--active:var(--docsifytabs-content-background);--docsifytabs-tab-color:#999;--docsifytabs-tab-color--active:inherit;--docsifytabs-tab-highlight-px:3px;--docsifytabs-tab-highlight-color:var(--theme-color,currentColor);--docsifytabs-tab-padding:0.6em 1em;--docsifytabs-content-background:inherit;--docsifytabs-content-padding:1.5rem}.docsify-tabs:before,.docsify-tabs__tab{z-index:1}.docsify-tabs__tab--active,.docsify-tabs__tab:focus{z-index:2}.docsify-tabs{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;position:relative}.docsify-tabs:before{content:"";-ms-flex-order:0;order:0;-ms-flex:1;flex:1 1}.docsify-tabs__tab{-ms-flex-order:-1;order:-1;position:relative;margin:0;font-size:inherit;appearance:none}.docsify-tabs__content{visibility:hidden;position:absolute;overflow:hidden;height:0;width:100%}.docsify-tabs__content :first-child{margin-top:0}.docsify-tabs__content :first-child~:last-child,.docsify-tabs__content :last-child{margin-bottom:0}.docsify-tabs__tab--active+.docsify-tabs__content{visibility:visible;position:relative;overflow:auto;height:auto}[class*=docsify-tabs--]{margin:1.5em 0;margin:var(--docsifytabs-margin)}[class*=docsify-tabs--] .docsify-tabs__tab{padding:.6em 1em;padding:var(--docsifytabs-tab-padding);background:#f8f8f8;background:var(--docsifytabs-tab-background);color:#999;color:var(--docsifytabs-tab-color)}[class*=docsify-tabs--] .docsify-tabs__tab--active{background:inherit;background:var(--docsifytabs-tab-background--active);color:inherit;color:var(--docsifytabs-tab-color--active)}[class*=docsify-tabs--] .docsify-tabs__content{padding:1.5rem;padding:var(--docsifytabs-content-padding);background:inherit;background:var(--docsifytabs-content-background)}.docsify-tabs--classic .docsify-tabs__content,.docsify-tabs--classic .docsify-tabs__tab,.docsify-tabs--classic:before{border-width:1px;border-color:#ededed;border:var(--docsifytabs-border-px) solid var(--docsifytabs-border-color)}.docsify-tabs--classic:before{margin-right:1px;margin-right:var(--docsifytabs-border-px);border-top-width:0;border-left-width:0;border-right-width:0}.docsify-tabs--classic .docsify-tabs__tab:first-of-type{border-top-left-radius:var(--docsifytabs-border-radius-px)}.docsify-tabs--classic .docsify-tabs__tab:last-of-type{border-top-right-radius:var(--docsifytabs-border-radius-px)}.docsify-tabs--classic .docsify-tabs__tab~.docsify-tabs__tab{margin-left:-1px;margin-left:calc(0px - var(--docsifytabs-border-px))}.docsify-tabs--classic .docsify-tabs__tab--active{border-bottom-width:0;box-shadow:inset 0 3px 0 0 var(--theme-color,currentColor);box-shadow:inset 0 var(--docsifytabs-tab-highlight-px) 0 0 var(--docsifytabs-tab-highlight-color)}.docsify-tabs--classic .docsify-tabs__content{margin-top:-1px;margin-top:calc(0px - var(--docsifytabs-border-px));border-top:0;border-radius:0;border-radius:0 var(--docsifytabs-border-radius-px) var(--docsifytabs-border-radius-px) var(--docsifytabs-border-radius-px)}.docsify-tabs--material .docsify-tabs__tab{margin-bottom:2px;margin-bottom:calc(var(--docsifytabs-tab-highlight-px) - var(--docsifytabs-border-px));background:transparent;border:0}.docsify-tabs--material .docsify-tabs__tab--active{box-shadow:0 3px 0 0 var(--theme-color,currentColor);box-shadow:0 var(--docsifytabs-tab-highlight-px) 0 0 var(--docsifytabs-tab-highlight-color);background:transparent}.docsify-tabs--material .docsify-tabs__content{border-width:1px 0;border-color:#ededed;border-left:0 solid var(--docsifytabs-border-color);border-bottom:var(--docsifytabs-border-px) solid var(--docsifytabs-border-color);border-right:0 solid var(--docsifytabs-border-color);border-top:var(--docsifytabs-border-px) solid var(--docsifytabs-border-color)}',{insertAt:"top"});var u="tabs:replace",y={tabsContainer:"content",tabBlock:"docsify-tabs",tabButton:"docsify-tabs__tab",tabButtonActive:"docsify-tabs__tab--active",tabContent:"docsify-tabs__content"},p={codeMarkup:/(```[\s\S]*?```)/gm,commentReplaceMarkup:new RegExp("\x3c!-- ".concat(u," (.*) --\x3e")),tabBlockMarkup:/[\r\n]*(\s*)()[\r\n]+([\s|\S]*?)[\r\n\s]+()/m,tabCommentMarkup:/[\r\n]*(\s*)[\r\n]+([\s\S]*?)[\r\n]*\s*(?=)/m},m={persist:!0,sync:!0,theme:"classic",tabComments:!0,tabHeadings:!0};function g(t,a){var o=1 --\x3e'),a="\n".concat(s,"\x3c!-- ").concat(u,"
--\x3e");for(var i=function(){var t=(f[2]||"[Tab]").trim(),a=(f[3]||"").trim();o=o.replace(f[0],function(){return["\n".concat(s,"\x3c!-- ").concat(u,' --\x3e"),"\n".concat(s,"\x3c!-- ").concat(u,'
--\x3e'),"\n\n".concat(s).concat(a),"\n\n".concat(s,"\x3c!-- ").concat(u,"
--\x3e")].join("")})};null!==(f=(m.tabComments?p.tabCommentMarkup.exec(o):null)||(m.tabHeadings?p.tabHeadingMarkup.exec(o):null));)i()}o=(o=o.replace(r,function(){return t})).replace(n,function(){return a}),d=d.replace(b[0],function(){return o})};null!==(b=p.tabBlockMarkup.exec(d));)a();return t.forEach(function(t,a){d=d.replace(t,function(){return o[a]})}),d}(t)),t}),t.afterEach(function(t,a){o&&(t=function(o){for(var c,t=function(){var t=c[0],a=c[1]||"";o=o.replace(t,function(){return a})};null!==(c=p.commentReplaceMarkup.exec(o));)t();return o}(t)),a(t)}),t.doneEach(function(){var t,a,c,e;o&&(t=document.querySelector(".".concat(y.tabsContainer)),a=t?Array.apply(null,t.querySelectorAll(".".concat(y.tabBlock))):[],c=JSON.parse(sessionStorage.getItem(window.location.href))||{},e=JSON.parse(sessionStorage.getItem("*"))||[],s(),a.forEach(function(a,t){var o=a.querySelector(".".concat(y.tabButtonActive));o||(m.sync&&e.length&&(o=e.map(function(t){return a.querySelector(".".concat(y.tabButton,'[data-tab="').concat(t,'"]'))}).filter(function(t){return t})[0]),!o&&m.persist&&(o=a.querySelector(".".concat(y.tabButton,'[data-tab="').concat(c[t],'"]'))),(o=o||a.querySelector(".".concat(y.tabButton)))&&o.classList.add(y.tabButtonActive))}))}),t.mounted(function(){var t=document.querySelector(".".concat(y.tabsContainer));t&&t.addEventListener("click",function(t){g(t.target)}),window.addEventListener("hashchange",s,!1)})},window.$docsify.plugins||[])))}(); +//# sourceMappingURL2=docsify-tabs.min.js.map \ No newline at end of file diff --git a/sa-token-doc/static/docsify.min.js b/sa-token-doc/static/docsify.min.js new file mode 100644 index 000000000..48991bb2f --- /dev/null +++ b/sa-token-doc/static/docsify.min.js @@ -0,0 +1 @@ +!function(){function c(i){var o=Object.create(null);return function(e){var n=f(e)?e:JSON.stringify(e);return o[n]||(o[n]=i(e))}}var a=c(function(e){return e.replace(/([A-Z])/g,function(e){return"-"+e.toLowerCase()})}),u=Object.prototype.hasOwnProperty,m=Object.assign||function(e){for(var n=arguments,i=1;i=e||n.classList.contains("hidden")?S(h,"add","sticky"):S(h,"remove","sticky"))}function ee(e,n,o,i){var t=[];null!=(n=l(n))&&(t=k(n,"a"));var a,r=decodeURI(e.toURL(e.getCurrentPath()));return t.sort(function(e,n){return n.href.length-e.href.length}).forEach(function(e){var n=decodeURI(e.getAttribute("href")),i=o?e.parentNode:e;e.title=e.title||e.innerText,0!==r.indexOf(n)||a?S(i,"remove","active"):(a=e,S(i,"add","active"))}),i&&(v.title=a?a.title||a.innerText+" - "+J:J),a}function ne(e,n){for(var i=0;ithis.end&&e>=this.next}[this.direction]}},{key:"_defaultEase",value:function(e,n,i,o){return(e/=o/2)<1?i/2*e*e+n:-i/2*(--e*(e-2)-1)+n}}]),re);function re(){var e=0c){n=n||p;break}n=p}!n||(r=fe[ve(e,n.getAttribute("data-id"))])&&r!==a&&(a&&a.classList.remove("active"),r.classList.add("active"),a=r,!pe&&h.classList.contains("sticky")&&(e=i.clientHeight,r=a.offsetTop+a.clientHeight+40,a=a.offsetTop>=t.scrollTop&&r<=t.scrollTop+e,i.scrollTop=a?t.scrollTop:+r"']/),xe=/[&<>"']/g,Se=/[<>"']|&(?!#?\w+;)/,Ae=/[<>"']|&(?!#?\w+;)/g,$e={"&":"&","<":"<",">":">",'"':""","'":"'"};var ze=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function Fe(e){return e.replace(ze,function(e,n){return"colon"===(n=n.toLowerCase())?":":"#"===n.charAt(0)?"x"===n.charAt(1)?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1)):""})}var Ee=/(^|[^\[])\^/g;var Te=/[^\w:]/g,Ce=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;var Re={},je=/^[^:]+:\/*[^/]*$/,Oe=/^([^:]+:)[\s\S]*$/,Le=/^([^:]+:\/*[^/]*)[\s\S]*$/;function qe(e,n){Re[" "+e]||(je.test(e)?Re[" "+e]=e+"/":Re[" "+e]=Pe(e,"/",!0));var i=-1===(e=Re[" "+e]).indexOf(":");return"//"===n.substring(0,2)?i?n:e.replace(Oe,"$1")+n:"/"===n.charAt(0)?i?n:e.replace(Le,"$1")+n:e+n}function Pe(e,n,i){var o=e.length;if(0===o)return"";for(var t=0;tn)i.splice(n);else for(;i.length>=1,e+=e;return i+e},We=we.defaults,Xe=Be,Qe=Ze,Je=Me,Ke=Ve;function en(e,n,i){var o=n.href,t=n.title?Je(n.title):null,n=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?{type:"link",raw:i,href:o,title:t,text:n}:{type:"image",raw:i,href:o,title:t,text:Je(n)}}var nn=function(){function e(e){this.options=e||We}return e.prototype.space=function(e){e=this.rules.block.newline.exec(e);if(e)return 1=i.length?e.slice(i.length):e}).join("\n")}(i,n[3]||"");return{type:"code",raw:i,lang:n[2]&&n[2].trim(),text:e}}},e.prototype.heading=function(e){var n=this.rules.block.heading.exec(e);if(n){var i=n[2].trim();return/#$/.test(i)&&(e=Xe(i,"#"),!this.options.pedantic&&e&&!/ $/.test(e)||(i=e.trim())),{type:"heading",raw:n[0],depth:n[1].length,text:i}}},e.prototype.nptable=function(e){e=this.rules.block.nptable.exec(e);if(e){var n={type:"table",header:Qe(e[1].replace(/^ *| *\| *$/g,"")),align:e[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:e[3]?e[3].replace(/\n$/,"").split("\n"):[],raw:e[0]};if(n.header.length===n.align.length){for(var i=n.align.length,o=0;o ?/gm,"");return{type:"blockquote",raw:n[0],text:e}}},e.prototype.list=function(e){e=this.rules.block.list.exec(e);if(e){for(var n,i,o,t,a,r=e[0],c=e[2],u=1s[1].length:o[1].length>s[0].length||3/i.test(e[0])&&(n=!1),!i&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?i=!0:i&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(i=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:n,inRawBlock:i,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):Je(e[0]):e[0]}},e.prototype.link=function(e){var n=this.rules.inline.link.exec(e);if(n){e=n[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;var i=Xe(e.slice(0,-1),"\\");if((e.length-i.length)%2==0)return}else{var o=Ke(n[2],"()");-1$/.test(e)?i.slice(1):i.slice(1,-1):i)&&i.replace(this.rules.inline._escapes,"$1"),title:o&&o.replace(this.rules.inline._escapes,"$1")},n[0])}},e.prototype.reflink=function(e,n){if((i=this.rules.inline.reflink.exec(e))||(i=this.rules.inline.nolink.exec(e))){var e=(i[2]||i[1]).replace(/\s+/g," ");if((e=n[e.toLowerCase()])&&e.href)return en(i,e,i[0]);var i=i[0].charAt(0);return{type:"text",raw:i,text:i}}},e.prototype.strong=function(e,n,i){void 0===i&&(i="");var o=this.rules.inline.strong.start.exec(e);if(o&&(!o[1]||o[1]&&(""===i||this.rules.inline.punctuation.exec(i)))){n=n.slice(-1*e.length);var t,a="**"===o[0]?this.rules.inline.strong.endAst:this.rules.inline.strong.endUnd;for(a.lastIndex=0;null!=(o=a.exec(n));)if(t=this.rules.inline.strong.middle.exec(n.slice(0,o.index+3)))return{type:"strong",raw:e.slice(0,t[0].length),text:e.slice(2,t[0].length-2)}}},e.prototype.em=function(e,n,i){void 0===i&&(i="");var o=this.rules.inline.em.start.exec(e);if(o&&(!o[1]||o[1]&&(""===i||this.rules.inline.punctuation.exec(i)))){n=n.slice(-1*e.length);var t,a="*"===o[0]?this.rules.inline.em.endAst:this.rules.inline.em.endUnd;for(a.lastIndex=0;null!=(o=a.exec(n));)if(t=this.rules.inline.em.middle.exec(n.slice(0,o.index+2)))return{type:"em",raw:e.slice(0,t[0].length),text:e.slice(1,t[0].length-1)}}},e.prototype.codespan=function(e){var n=this.rules.inline.code.exec(e);if(n){var i=n[2].replace(/\n/g," "),o=/[^ ]/.test(i),e=/^ /.test(i)&&/ $/.test(i);return o&&e&&(i=i.substring(1,i.length-1)),i=Je(i,!0),{type:"codespan",raw:n[0],text:i}}},e.prototype.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},e.prototype.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2]}},e.prototype.autolink=function(e,n){e=this.rules.inline.autolink.exec(e);if(e){var i,n="@"===e[2]?"mailto:"+(i=Je(this.options.mangle?n(e[1]):e[1])):i=Je(e[1]);return{type:"link",raw:e[0],text:i,href:n,tokens:[{type:"text",raw:i,text:i}]}}},e.prototype.url=function(e,n){var i,o,t,a;if(i=this.rules.inline.url.exec(e)){if("@"===i[2])t="mailto:"+(o=Je(this.options.mangle?n(i[0]):i[0]));else{for(;a=i[0],i[0]=this.rules.inline._backpedal.exec(i[0])[0],a!==i[0];);o=Je(i[0]),t="www."===i[1]?"http://"+o:o}return{type:"link",raw:i[0],text:o,href:t,tokens:[{type:"text",raw:o,text:o}]}}},e.prototype.inlineText=function(e,n,i){e=this.rules.inline.text.exec(e);if(e){i=n?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):Je(e[0]):e[0]:Je(this.options.smartypants?i(e[0]):e[0]);return{type:"text",raw:e[0],text:i}}},e}(),Ze=De,Ve=Ne,De=Ue,Ne={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?! {0,3}bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:Ze,table:Ze,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};Ne.def=Ve(Ne.def).replace("label",Ne._label).replace("title",Ne._title).getRegex(),Ne.bullet=/(?:[*+-]|\d{1,9}[.)])/,Ne.item=/^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/,Ne.item=Ve(Ne.item,"gm").replace(/bull/g,Ne.bullet).getRegex(),Ne.listItemStart=Ve(/^( *)(bull)/).replace("bull",Ne.bullet).getRegex(),Ne.list=Ve(Ne.list).replace(/bull/g,Ne.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+Ne.def.source+")").getRegex(),Ne._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",Ne._comment=/|$)/,Ne.html=Ve(Ne.html,"i").replace("comment",Ne._comment).replace("tag",Ne._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Ne.paragraph=Ve(Ne._paragraph).replace("hr",Ne.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",Ne._tag).getRegex(),Ne.blockquote=Ve(Ne.blockquote).replace("paragraph",Ne.paragraph).getRegex(),Ne.normal=De({},Ne),Ne.gfm=De({},Ne.normal,{nptable:"^ *([^|\\n ].*\\|.*)\\n {0,3}([-:]+ *\\|[-| :]*)(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)",table:"^ *\\|(.+)\\n {0,3}\\|?( *[-:]+[-| :]*)(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),Ne.gfm.nptable=Ve(Ne.gfm.nptable).replace("hr",Ne.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",Ne._tag).getRegex(),Ne.gfm.table=Ve(Ne.gfm.table).replace("hr",Ne.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",Ne._tag).getRegex(),Ne.pedantic=De({},Ne.normal,{html:Ve("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",Ne._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Ze,paragraph:Ve(Ne.normal._paragraph).replace("hr",Ne.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",Ne.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});Ze={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:Ze,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",strong:{start:/^(?:(\*\*(?=[*punctuation]))|\*\*)(?![\s])|__/,middle:/^\*\*(?:(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)|\*(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)*?\*)+?\*\*$|^__(?![\s])((?:(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)|_(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)*?_)+?)__$/,endAst:/[^punctuation\s]\*\*(?!\*)|[punctuation]\*\*(?!\*)(?:(?=[punctuation_\s]|$))/,endUnd:/[^\s]__(?!_)(?:(?=[punctuation*\s])|$)/},em:{start:/^(?:(\*(?=[punctuation]))|\*)(?![*\s])|_/,middle:/^\*(?:(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)|\*(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)*?\*)+?\*$|^_(?![_\s])(?:(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)|_(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)*?_)+?_$/,endAst:/[^punctuation\s]\*(?!\*)|[punctuation]\*(?!\*)(?:(?=[punctuation_\s]|$))/,endUnd:/[^\s]_(?!_)(?:(?=[punctuation*\s])|$)/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:Ze,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~"};Ze.punctuation=Ve(Ze.punctuation).replace(/punctuation/g,Ze._punctuation).getRegex(),Ze._blockSkip="\\[[^\\]]*?\\]\\([^\\)]*?\\)|`[^`]*?`|<[^>]*?>",Ze._overlapSkip="__[^_]*?__|\\*\\*\\[^\\*\\]*?\\*\\*",Ze._comment=Ve(Ne._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),Ze.em.start=Ve(Ze.em.start).replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.em.middle=Ve(Ze.em.middle).replace(/punctuation/g,Ze._punctuation).replace(/overlapSkip/g,Ze._overlapSkip).getRegex(),Ze.em.endAst=Ve(Ze.em.endAst,"g").replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.em.endUnd=Ve(Ze.em.endUnd,"g").replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.strong.start=Ve(Ze.strong.start).replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.strong.middle=Ve(Ze.strong.middle).replace(/punctuation/g,Ze._punctuation).replace(/overlapSkip/g,Ze._overlapSkip).getRegex(),Ze.strong.endAst=Ve(Ze.strong.endAst,"g").replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.strong.endUnd=Ve(Ze.strong.endUnd,"g").replace(/punctuation/g,Ze._punctuation).getRegex(),Ze.blockSkip=Ve(Ze._blockSkip,"g").getRegex(),Ze.overlapSkip=Ve(Ze._overlapSkip,"g").getRegex(),Ze._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,Ze._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,Ze._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,Ze.autolink=Ve(Ze.autolink).replace("scheme",Ze._scheme).replace("email",Ze._email).getRegex(),Ze._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,Ze.tag=Ve(Ze.tag).replace("comment",Ze._comment).replace("attribute",Ze._attribute).getRegex(),Ze._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ze._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,Ze._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,Ze.link=Ve(Ze.link).replace("label",Ze._label).replace("href",Ze._href).replace("title",Ze._title).getRegex(),Ze.reflink=Ve(Ze.reflink).replace("label",Ze._label).getRegex(),Ze.reflinkSearch=Ve(Ze.reflinkSearch,"g").replace("reflink",Ze.reflink).replace("nolink",Ze.nolink).getRegex(),Ze.normal=De({},Ze),Ze.pedantic=De({},Ze.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:Ve(/^!?\[(label)\]\((.*?)\)/).replace("label",Ze._label).getRegex(),reflink:Ve(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Ze._label).getRegex()}),Ze.gfm=De({},Ze.normal,{escape:Ve(Ze.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\'+(i?e:gn(e,!0))+"\n":"
"+(i?e:gn(e,!0))+"
\n"},e.prototype.blockquote=function(e){return"
\n"+e+"
\n"},e.prototype.html=function(e){return e},e.prototype.heading=function(e,n,i,o){return this.options.headerIds?"'+e+"\n":""+e+"\n"},e.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"},e.prototype.list=function(e,n,i){var o=n?"ol":"ul";return"<"+o+(n&&1!==i?' start="'+i+'"':"")+">\n"+e+"\n"},e.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},e.prototype.checkbox=function(e){return" "},e.prototype.paragraph=function(e){return"

    "+e+"

    \n"},e.prototype.table=function(e,n){return"\n\n"+e+"\n"+(n=n&&""+n+"")+"
    \n"},e.prototype.tablerow=function(e){return"\n"+e+"\n"},e.prototype.tablecell=function(e,n){var i=n.header?"th":"td";return(n.align?"<"+i+' align="'+n.align+'">':"<"+i+">")+e+"\n"},e.prototype.strong=function(e){return""+e+""},e.prototype.em=function(e){return""+e+""},e.prototype.codespan=function(e){return""+e+""},e.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},e.prototype.del=function(e){return""+e+""},e.prototype.link=function(e,n,i){if(null===(e=dn(this.options.sanitize,this.options.baseUrl,e)))return i;e='"},e.prototype.image=function(e,n,i){if(null===(e=dn(this.options.sanitize,this.options.baseUrl,e)))return i;i=''+i+'":">"},e.prototype.text=function(e){return e},e}(),ln=function(){function e(){}return e.prototype.strong=function(e){return e},e.prototype.em=function(e){return e},e.prototype.codespan=function(e){return e},e.prototype.del=function(e){return e},e.prototype.html=function(e){return e},e.prototype.text=function(e){return e},e.prototype.link=function(e,n,i){return""+i},e.prototype.image=function(e,n,i){return""+i},e.prototype.br=function(){return""},e}(),vn=function(){function e(){this.seen={}}return e.prototype.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},e.prototype.getNextSafeSlug=function(e,n){var i=e,o=0;if(this.seen.hasOwnProperty(i))for(o=this.seen[e];i=e+"-"+ ++o,this.seen.hasOwnProperty(i););return n||(this.seen[e]=o,this.seen[i]=0),i},e.prototype.slug=function(e,n){void 0===n&&(n={});e=this.serialize(e);return this.getNextSafeSlug(e,n.dryrun)},e}(),hn=we.defaults,_n=Ie,mn=function(){function i(e){this.options=e||hn,this.options.renderer=this.options.renderer||new sn,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new ln,this.slugger=new vn}return i.parse=function(e,n){return new i(n).parse(e)},i.parseInline=function(e,n){return new i(n).parseInline(e)},i.prototype.parse=function(e,n){void 0===n&&(n=!0);for(var i,o,t,a,r,c,u,f,p,d,g,s,l,v,h,_="",m=e.length,b=0;bAn error occurred:

    "+wn(e.message+"",!0)+"
    ";throw e}}xn.options=xn.setOptions=function(e){return bn(xn.defaults,e),yn(xn.defaults),xn},xn.getDefaults=Me,xn.defaults=we,xn.use=function(a){var n,e=bn({},a);if(a.renderer){var i,r=xn.defaults.renderer||new sn;for(i in a.renderer)!function(o){var t=r[o];r[o]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var i=a.renderer[o].apply(r,e);return i=!1===i?t.apply(r,e):i}}(i);e.renderer=r}if(a.tokenizer){var t,c=xn.defaults.tokenizer||new nn;for(t in a.tokenizer)!function(){var o=c[t];c[t]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var i=a.tokenizer[t].apply(c,e);return i=!1===i?o.apply(c,e):i}}();e.tokenizer=c}a.walkTokens&&(n=xn.defaults.walkTokens,e.walkTokens=function(e){a.walkTokens(e),n&&n(e)}),xn.setOptions(e)},xn.walkTokens=function(e,n){for(var i=0,o=e;iAn error occurred:

    "+wn(e.message+"",!0)+"
    ";throw e}},xn.Parser=mn,xn.parser=mn.parse,xn.Renderer=sn,xn.TextRenderer=ln,xn.Lexer=fn,xn.lexer=fn.lex,xn.Tokenizer=nn,xn.Slugger=vn;var Sn=xn.parse=xn;function An(e,i){if(void 0===i&&(i='
      {inner}
    '),!e||!e.length)return"";var o="";return e.forEach(function(e){var n=e.title.replace(/(<([^>]+)>)/g,"");o+='
  • '+e.title+"
  • ",e.children&&(o+=An(e.children,i))}),i.replace("{inner}",o)}function $n(e,n){return'

    '+n.slice(5).trim()+"

    "}function zn(e,o){var t=[],a={};return e.forEach(function(e){var n=e.level||1,i=n-1;o?@[\]^`{|}~]/g;function Tn(e){return e.toLowerCase()}function Cn(e){if("string"!=typeof e)return"";var n=e.trim().replace(/[A-Z]+/g,Tn).replace(/<[^>]+>/g,"").replace(En,"").replace(/\s/g,"-").replace(/-+/g,"-").replace(/^(\d)/,"_$1"),e=Fn[n],e=u.call(Fn,n)?e+1:0;return n=(Fn[n]=e)?n+"-"+e:n}Cn.clear=function(){Fn={}};var Rn={baseURL:"https://github.githubassets.com/images/icons/emoji/",data:{100:"unicode/1f4af.png?v8",1234:"unicode/1f522.png?v8","+1":"unicode/1f44d.png?v8","-1":"unicode/1f44e.png?v8","1st_place_medal":"unicode/1f947.png?v8","2nd_place_medal":"unicode/1f948.png?v8","3rd_place_medal":"unicode/1f949.png?v8","8ball":"unicode/1f3b1.png?v8",a:"unicode/1f170.png?v8",ab:"unicode/1f18e.png?v8",abacus:"unicode/1f9ee.png?v8",abc:"unicode/1f524.png?v8",abcd:"unicode/1f521.png?v8",accept:"unicode/1f251.png?v8",accordion:"unicode/1fa97.png?v8",adhesive_bandage:"unicode/1fa79.png?v8",adult:"unicode/1f9d1.png?v8",aerial_tramway:"unicode/1f6a1.png?v8",afghanistan:"unicode/1f1e6-1f1eb.png?v8",airplane:"unicode/2708.png?v8",aland_islands:"unicode/1f1e6-1f1fd.png?v8",alarm_clock:"unicode/23f0.png?v8",albania:"unicode/1f1e6-1f1f1.png?v8",alembic:"unicode/2697.png?v8",algeria:"unicode/1f1e9-1f1ff.png?v8",alien:"unicode/1f47d.png?v8",ambulance:"unicode/1f691.png?v8",american_samoa:"unicode/1f1e6-1f1f8.png?v8",amphora:"unicode/1f3fa.png?v8",anatomical_heart:"unicode/1fac0.png?v8",anchor:"unicode/2693.png?v8",andorra:"unicode/1f1e6-1f1e9.png?v8",angel:"unicode/1f47c.png?v8",anger:"unicode/1f4a2.png?v8",angola:"unicode/1f1e6-1f1f4.png?v8",angry:"unicode/1f620.png?v8",anguilla:"unicode/1f1e6-1f1ee.png?v8",anguished:"unicode/1f627.png?v8",ant:"unicode/1f41c.png?v8",antarctica:"unicode/1f1e6-1f1f6.png?v8",antigua_barbuda:"unicode/1f1e6-1f1ec.png?v8",apple:"unicode/1f34e.png?v8",aquarius:"unicode/2652.png?v8",argentina:"unicode/1f1e6-1f1f7.png?v8",aries:"unicode/2648.png?v8",armenia:"unicode/1f1e6-1f1f2.png?v8",arrow_backward:"unicode/25c0.png?v8",arrow_double_down:"unicode/23ec.png?v8",arrow_double_up:"unicode/23eb.png?v8",arrow_down:"unicode/2b07.png?v8",arrow_down_small:"unicode/1f53d.png?v8",arrow_forward:"unicode/25b6.png?v8",arrow_heading_down:"unicode/2935.png?v8",arrow_heading_up:"unicode/2934.png?v8",arrow_left:"unicode/2b05.png?v8",arrow_lower_left:"unicode/2199.png?v8",arrow_lower_right:"unicode/2198.png?v8",arrow_right:"unicode/27a1.png?v8",arrow_right_hook:"unicode/21aa.png?v8",arrow_up:"unicode/2b06.png?v8",arrow_up_down:"unicode/2195.png?v8",arrow_up_small:"unicode/1f53c.png?v8",arrow_upper_left:"unicode/2196.png?v8",arrow_upper_right:"unicode/2197.png?v8",arrows_clockwise:"unicode/1f503.png?v8",arrows_counterclockwise:"unicode/1f504.png?v8",art:"unicode/1f3a8.png?v8",articulated_lorry:"unicode/1f69b.png?v8",artificial_satellite:"unicode/1f6f0.png?v8",artist:"unicode/1f9d1-1f3a8.png?v8",aruba:"unicode/1f1e6-1f1fc.png?v8",ascension_island:"unicode/1f1e6-1f1e8.png?v8",asterisk:"unicode/002a-20e3.png?v8",astonished:"unicode/1f632.png?v8",astronaut:"unicode/1f9d1-1f680.png?v8",athletic_shoe:"unicode/1f45f.png?v8",atm:"unicode/1f3e7.png?v8",atom:"atom.png?v8",atom_symbol:"unicode/269b.png?v8",australia:"unicode/1f1e6-1f1fa.png?v8",austria:"unicode/1f1e6-1f1f9.png?v8",auto_rickshaw:"unicode/1f6fa.png?v8",avocado:"unicode/1f951.png?v8",axe:"unicode/1fa93.png?v8",azerbaijan:"unicode/1f1e6-1f1ff.png?v8",b:"unicode/1f171.png?v8",baby:"unicode/1f476.png?v8",baby_bottle:"unicode/1f37c.png?v8",baby_chick:"unicode/1f424.png?v8",baby_symbol:"unicode/1f6bc.png?v8",back:"unicode/1f519.png?v8",bacon:"unicode/1f953.png?v8",badger:"unicode/1f9a1.png?v8",badminton:"unicode/1f3f8.png?v8",bagel:"unicode/1f96f.png?v8",baggage_claim:"unicode/1f6c4.png?v8",baguette_bread:"unicode/1f956.png?v8",bahamas:"unicode/1f1e7-1f1f8.png?v8",bahrain:"unicode/1f1e7-1f1ed.png?v8",balance_scale:"unicode/2696.png?v8",bald_man:"unicode/1f468-1f9b2.png?v8",bald_woman:"unicode/1f469-1f9b2.png?v8",ballet_shoes:"unicode/1fa70.png?v8",balloon:"unicode/1f388.png?v8",ballot_box:"unicode/1f5f3.png?v8",ballot_box_with_check:"unicode/2611.png?v8",bamboo:"unicode/1f38d.png?v8",banana:"unicode/1f34c.png?v8",bangbang:"unicode/203c.png?v8",bangladesh:"unicode/1f1e7-1f1e9.png?v8",banjo:"unicode/1fa95.png?v8",bank:"unicode/1f3e6.png?v8",bar_chart:"unicode/1f4ca.png?v8",barbados:"unicode/1f1e7-1f1e7.png?v8",barber:"unicode/1f488.png?v8",baseball:"unicode/26be.png?v8",basecamp:"basecamp.png?v8",basecampy:"basecampy.png?v8",basket:"unicode/1f9fa.png?v8",basketball:"unicode/1f3c0.png?v8",basketball_man:"unicode/26f9-2642.png?v8",basketball_woman:"unicode/26f9-2640.png?v8",bat:"unicode/1f987.png?v8",bath:"unicode/1f6c0.png?v8",bathtub:"unicode/1f6c1.png?v8",battery:"unicode/1f50b.png?v8",beach_umbrella:"unicode/1f3d6.png?v8",bear:"unicode/1f43b.png?v8",bearded_person:"unicode/1f9d4.png?v8",beaver:"unicode/1f9ab.png?v8",bed:"unicode/1f6cf.png?v8",bee:"unicode/1f41d.png?v8",beer:"unicode/1f37a.png?v8",beers:"unicode/1f37b.png?v8",beetle:"unicode/1fab2.png?v8",beginner:"unicode/1f530.png?v8",belarus:"unicode/1f1e7-1f1fe.png?v8",belgium:"unicode/1f1e7-1f1ea.png?v8",belize:"unicode/1f1e7-1f1ff.png?v8",bell:"unicode/1f514.png?v8",bell_pepper:"unicode/1fad1.png?v8",bellhop_bell:"unicode/1f6ce.png?v8",benin:"unicode/1f1e7-1f1ef.png?v8",bento:"unicode/1f371.png?v8",bermuda:"unicode/1f1e7-1f1f2.png?v8",beverage_box:"unicode/1f9c3.png?v8",bhutan:"unicode/1f1e7-1f1f9.png?v8",bicyclist:"unicode/1f6b4.png?v8",bike:"unicode/1f6b2.png?v8",biking_man:"unicode/1f6b4-2642.png?v8",biking_woman:"unicode/1f6b4-2640.png?v8",bikini:"unicode/1f459.png?v8",billed_cap:"unicode/1f9e2.png?v8",biohazard:"unicode/2623.png?v8",bird:"unicode/1f426.png?v8",birthday:"unicode/1f382.png?v8",bison:"unicode/1f9ac.png?v8",black_cat:"unicode/1f408-2b1b.png?v8",black_circle:"unicode/26ab.png?v8",black_flag:"unicode/1f3f4.png?v8",black_heart:"unicode/1f5a4.png?v8",black_joker:"unicode/1f0cf.png?v8",black_large_square:"unicode/2b1b.png?v8",black_medium_small_square:"unicode/25fe.png?v8",black_medium_square:"unicode/25fc.png?v8",black_nib:"unicode/2712.png?v8",black_small_square:"unicode/25aa.png?v8",black_square_button:"unicode/1f532.png?v8",blond_haired_man:"unicode/1f471-2642.png?v8",blond_haired_person:"unicode/1f471.png?v8",blond_haired_woman:"unicode/1f471-2640.png?v8",blonde_woman:"unicode/1f471-2640.png?v8",blossom:"unicode/1f33c.png?v8",blowfish:"unicode/1f421.png?v8",blue_book:"unicode/1f4d8.png?v8",blue_car:"unicode/1f699.png?v8",blue_heart:"unicode/1f499.png?v8",blue_square:"unicode/1f7e6.png?v8",blueberries:"unicode/1fad0.png?v8",blush:"unicode/1f60a.png?v8",boar:"unicode/1f417.png?v8",boat:"unicode/26f5.png?v8",bolivia:"unicode/1f1e7-1f1f4.png?v8",bomb:"unicode/1f4a3.png?v8",bone:"unicode/1f9b4.png?v8",book:"unicode/1f4d6.png?v8",bookmark:"unicode/1f516.png?v8",bookmark_tabs:"unicode/1f4d1.png?v8",books:"unicode/1f4da.png?v8",boom:"unicode/1f4a5.png?v8",boomerang:"unicode/1fa83.png?v8",boot:"unicode/1f462.png?v8",bosnia_herzegovina:"unicode/1f1e7-1f1e6.png?v8",botswana:"unicode/1f1e7-1f1fc.png?v8",bouncing_ball_man:"unicode/26f9-2642.png?v8",bouncing_ball_person:"unicode/26f9.png?v8",bouncing_ball_woman:"unicode/26f9-2640.png?v8",bouquet:"unicode/1f490.png?v8",bouvet_island:"unicode/1f1e7-1f1fb.png?v8",bow:"unicode/1f647.png?v8",bow_and_arrow:"unicode/1f3f9.png?v8",bowing_man:"unicode/1f647-2642.png?v8",bowing_woman:"unicode/1f647-2640.png?v8",bowl_with_spoon:"unicode/1f963.png?v8",bowling:"unicode/1f3b3.png?v8",bowtie:"bowtie.png?v8",boxing_glove:"unicode/1f94a.png?v8",boy:"unicode/1f466.png?v8",brain:"unicode/1f9e0.png?v8",brazil:"unicode/1f1e7-1f1f7.png?v8",bread:"unicode/1f35e.png?v8",breast_feeding:"unicode/1f931.png?v8",bricks:"unicode/1f9f1.png?v8",bride_with_veil:"unicode/1f470-2640.png?v8",bridge_at_night:"unicode/1f309.png?v8",briefcase:"unicode/1f4bc.png?v8",british_indian_ocean_territory:"unicode/1f1ee-1f1f4.png?v8",british_virgin_islands:"unicode/1f1fb-1f1ec.png?v8",broccoli:"unicode/1f966.png?v8",broken_heart:"unicode/1f494.png?v8",broom:"unicode/1f9f9.png?v8",brown_circle:"unicode/1f7e4.png?v8",brown_heart:"unicode/1f90e.png?v8",brown_square:"unicode/1f7eb.png?v8",brunei:"unicode/1f1e7-1f1f3.png?v8",bubble_tea:"unicode/1f9cb.png?v8",bucket:"unicode/1faa3.png?v8",bug:"unicode/1f41b.png?v8",building_construction:"unicode/1f3d7.png?v8",bulb:"unicode/1f4a1.png?v8",bulgaria:"unicode/1f1e7-1f1ec.png?v8",bullettrain_front:"unicode/1f685.png?v8",bullettrain_side:"unicode/1f684.png?v8",burkina_faso:"unicode/1f1e7-1f1eb.png?v8",burrito:"unicode/1f32f.png?v8",burundi:"unicode/1f1e7-1f1ee.png?v8",bus:"unicode/1f68c.png?v8",business_suit_levitating:"unicode/1f574.png?v8",busstop:"unicode/1f68f.png?v8",bust_in_silhouette:"unicode/1f464.png?v8",busts_in_silhouette:"unicode/1f465.png?v8",butter:"unicode/1f9c8.png?v8",butterfly:"unicode/1f98b.png?v8",cactus:"unicode/1f335.png?v8",cake:"unicode/1f370.png?v8",calendar:"unicode/1f4c6.png?v8",call_me_hand:"unicode/1f919.png?v8",calling:"unicode/1f4f2.png?v8",cambodia:"unicode/1f1f0-1f1ed.png?v8",camel:"unicode/1f42b.png?v8",camera:"unicode/1f4f7.png?v8",camera_flash:"unicode/1f4f8.png?v8",cameroon:"unicode/1f1e8-1f1f2.png?v8",camping:"unicode/1f3d5.png?v8",canada:"unicode/1f1e8-1f1e6.png?v8",canary_islands:"unicode/1f1ee-1f1e8.png?v8",cancer:"unicode/264b.png?v8",candle:"unicode/1f56f.png?v8",candy:"unicode/1f36c.png?v8",canned_food:"unicode/1f96b.png?v8",canoe:"unicode/1f6f6.png?v8",cape_verde:"unicode/1f1e8-1f1fb.png?v8",capital_abcd:"unicode/1f520.png?v8",capricorn:"unicode/2651.png?v8",car:"unicode/1f697.png?v8",card_file_box:"unicode/1f5c3.png?v8",card_index:"unicode/1f4c7.png?v8",card_index_dividers:"unicode/1f5c2.png?v8",caribbean_netherlands:"unicode/1f1e7-1f1f6.png?v8",carousel_horse:"unicode/1f3a0.png?v8",carpentry_saw:"unicode/1fa9a.png?v8",carrot:"unicode/1f955.png?v8",cartwheeling:"unicode/1f938.png?v8",cat:"unicode/1f431.png?v8",cat2:"unicode/1f408.png?v8",cayman_islands:"unicode/1f1f0-1f1fe.png?v8",cd:"unicode/1f4bf.png?v8",central_african_republic:"unicode/1f1e8-1f1eb.png?v8",ceuta_melilla:"unicode/1f1ea-1f1e6.png?v8",chad:"unicode/1f1f9-1f1e9.png?v8",chains:"unicode/26d3.png?v8",chair:"unicode/1fa91.png?v8",champagne:"unicode/1f37e.png?v8",chart:"unicode/1f4b9.png?v8",chart_with_downwards_trend:"unicode/1f4c9.png?v8",chart_with_upwards_trend:"unicode/1f4c8.png?v8",checkered_flag:"unicode/1f3c1.png?v8",cheese:"unicode/1f9c0.png?v8",cherries:"unicode/1f352.png?v8",cherry_blossom:"unicode/1f338.png?v8",chess_pawn:"unicode/265f.png?v8",chestnut:"unicode/1f330.png?v8",chicken:"unicode/1f414.png?v8",child:"unicode/1f9d2.png?v8",children_crossing:"unicode/1f6b8.png?v8",chile:"unicode/1f1e8-1f1f1.png?v8",chipmunk:"unicode/1f43f.png?v8",chocolate_bar:"unicode/1f36b.png?v8",chopsticks:"unicode/1f962.png?v8",christmas_island:"unicode/1f1e8-1f1fd.png?v8",christmas_tree:"unicode/1f384.png?v8",church:"unicode/26ea.png?v8",cinema:"unicode/1f3a6.png?v8",circus_tent:"unicode/1f3aa.png?v8",city_sunrise:"unicode/1f307.png?v8",city_sunset:"unicode/1f306.png?v8",cityscape:"unicode/1f3d9.png?v8",cl:"unicode/1f191.png?v8",clamp:"unicode/1f5dc.png?v8",clap:"unicode/1f44f.png?v8",clapper:"unicode/1f3ac.png?v8",classical_building:"unicode/1f3db.png?v8",climbing:"unicode/1f9d7.png?v8",climbing_man:"unicode/1f9d7-2642.png?v8",climbing_woman:"unicode/1f9d7-2640.png?v8",clinking_glasses:"unicode/1f942.png?v8",clipboard:"unicode/1f4cb.png?v8",clipperton_island:"unicode/1f1e8-1f1f5.png?v8",clock1:"unicode/1f550.png?v8",clock10:"unicode/1f559.png?v8",clock1030:"unicode/1f565.png?v8",clock11:"unicode/1f55a.png?v8",clock1130:"unicode/1f566.png?v8",clock12:"unicode/1f55b.png?v8",clock1230:"unicode/1f567.png?v8",clock130:"unicode/1f55c.png?v8",clock2:"unicode/1f551.png?v8",clock230:"unicode/1f55d.png?v8",clock3:"unicode/1f552.png?v8",clock330:"unicode/1f55e.png?v8",clock4:"unicode/1f553.png?v8",clock430:"unicode/1f55f.png?v8",clock5:"unicode/1f554.png?v8",clock530:"unicode/1f560.png?v8",clock6:"unicode/1f555.png?v8",clock630:"unicode/1f561.png?v8",clock7:"unicode/1f556.png?v8",clock730:"unicode/1f562.png?v8",clock8:"unicode/1f557.png?v8",clock830:"unicode/1f563.png?v8",clock9:"unicode/1f558.png?v8",clock930:"unicode/1f564.png?v8",closed_book:"unicode/1f4d5.png?v8",closed_lock_with_key:"unicode/1f510.png?v8",closed_umbrella:"unicode/1f302.png?v8",cloud:"unicode/2601.png?v8",cloud_with_lightning:"unicode/1f329.png?v8",cloud_with_lightning_and_rain:"unicode/26c8.png?v8",cloud_with_rain:"unicode/1f327.png?v8",cloud_with_snow:"unicode/1f328.png?v8",clown_face:"unicode/1f921.png?v8",clubs:"unicode/2663.png?v8",cn:"unicode/1f1e8-1f1f3.png?v8",coat:"unicode/1f9e5.png?v8",cockroach:"unicode/1fab3.png?v8",cocktail:"unicode/1f378.png?v8",coconut:"unicode/1f965.png?v8",cocos_islands:"unicode/1f1e8-1f1e8.png?v8",coffee:"unicode/2615.png?v8",coffin:"unicode/26b0.png?v8",coin:"unicode/1fa99.png?v8",cold_face:"unicode/1f976.png?v8",cold_sweat:"unicode/1f630.png?v8",collision:"unicode/1f4a5.png?v8",colombia:"unicode/1f1e8-1f1f4.png?v8",comet:"unicode/2604.png?v8",comoros:"unicode/1f1f0-1f1f2.png?v8",compass:"unicode/1f9ed.png?v8",computer:"unicode/1f4bb.png?v8",computer_mouse:"unicode/1f5b1.png?v8",confetti_ball:"unicode/1f38a.png?v8",confounded:"unicode/1f616.png?v8",confused:"unicode/1f615.png?v8",congo_brazzaville:"unicode/1f1e8-1f1ec.png?v8",congo_kinshasa:"unicode/1f1e8-1f1e9.png?v8",congratulations:"unicode/3297.png?v8",construction:"unicode/1f6a7.png?v8",construction_worker:"unicode/1f477.png?v8",construction_worker_man:"unicode/1f477-2642.png?v8",construction_worker_woman:"unicode/1f477-2640.png?v8",control_knobs:"unicode/1f39b.png?v8",convenience_store:"unicode/1f3ea.png?v8",cook:"unicode/1f9d1-1f373.png?v8",cook_islands:"unicode/1f1e8-1f1f0.png?v8",cookie:"unicode/1f36a.png?v8",cool:"unicode/1f192.png?v8",cop:"unicode/1f46e.png?v8",copyright:"unicode/00a9.png?v8",corn:"unicode/1f33d.png?v8",costa_rica:"unicode/1f1e8-1f1f7.png?v8",cote_divoire:"unicode/1f1e8-1f1ee.png?v8",couch_and_lamp:"unicode/1f6cb.png?v8",couple:"unicode/1f46b.png?v8",couple_with_heart:"unicode/1f491.png?v8",couple_with_heart_man_man:"unicode/1f468-2764-1f468.png?v8",couple_with_heart_woman_man:"unicode/1f469-2764-1f468.png?v8",couple_with_heart_woman_woman:"unicode/1f469-2764-1f469.png?v8",couplekiss:"unicode/1f48f.png?v8",couplekiss_man_man:"unicode/1f468-2764-1f48b-1f468.png?v8",couplekiss_man_woman:"unicode/1f469-2764-1f48b-1f468.png?v8",couplekiss_woman_woman:"unicode/1f469-2764-1f48b-1f469.png?v8",cow:"unicode/1f42e.png?v8",cow2:"unicode/1f404.png?v8",cowboy_hat_face:"unicode/1f920.png?v8",crab:"unicode/1f980.png?v8",crayon:"unicode/1f58d.png?v8",credit_card:"unicode/1f4b3.png?v8",crescent_moon:"unicode/1f319.png?v8",cricket:"unicode/1f997.png?v8",cricket_game:"unicode/1f3cf.png?v8",croatia:"unicode/1f1ed-1f1f7.png?v8",crocodile:"unicode/1f40a.png?v8",croissant:"unicode/1f950.png?v8",crossed_fingers:"unicode/1f91e.png?v8",crossed_flags:"unicode/1f38c.png?v8",crossed_swords:"unicode/2694.png?v8",crown:"unicode/1f451.png?v8",cry:"unicode/1f622.png?v8",crying_cat_face:"unicode/1f63f.png?v8",crystal_ball:"unicode/1f52e.png?v8",cuba:"unicode/1f1e8-1f1fa.png?v8",cucumber:"unicode/1f952.png?v8",cup_with_straw:"unicode/1f964.png?v8",cupcake:"unicode/1f9c1.png?v8",cupid:"unicode/1f498.png?v8",curacao:"unicode/1f1e8-1f1fc.png?v8",curling_stone:"unicode/1f94c.png?v8",curly_haired_man:"unicode/1f468-1f9b1.png?v8",curly_haired_woman:"unicode/1f469-1f9b1.png?v8",curly_loop:"unicode/27b0.png?v8",currency_exchange:"unicode/1f4b1.png?v8",curry:"unicode/1f35b.png?v8",cursing_face:"unicode/1f92c.png?v8",custard:"unicode/1f36e.png?v8",customs:"unicode/1f6c3.png?v8",cut_of_meat:"unicode/1f969.png?v8",cyclone:"unicode/1f300.png?v8",cyprus:"unicode/1f1e8-1f1fe.png?v8",czech_republic:"unicode/1f1e8-1f1ff.png?v8",dagger:"unicode/1f5e1.png?v8",dancer:"unicode/1f483.png?v8",dancers:"unicode/1f46f.png?v8",dancing_men:"unicode/1f46f-2642.png?v8",dancing_women:"unicode/1f46f-2640.png?v8",dango:"unicode/1f361.png?v8",dark_sunglasses:"unicode/1f576.png?v8",dart:"unicode/1f3af.png?v8",dash:"unicode/1f4a8.png?v8",date:"unicode/1f4c5.png?v8",de:"unicode/1f1e9-1f1ea.png?v8",deaf_man:"unicode/1f9cf-2642.png?v8",deaf_person:"unicode/1f9cf.png?v8",deaf_woman:"unicode/1f9cf-2640.png?v8",deciduous_tree:"unicode/1f333.png?v8",deer:"unicode/1f98c.png?v8",denmark:"unicode/1f1e9-1f1f0.png?v8",department_store:"unicode/1f3ec.png?v8",derelict_house:"unicode/1f3da.png?v8",desert:"unicode/1f3dc.png?v8",desert_island:"unicode/1f3dd.png?v8",desktop_computer:"unicode/1f5a5.png?v8",detective:"unicode/1f575.png?v8",diamond_shape_with_a_dot_inside:"unicode/1f4a0.png?v8",diamonds:"unicode/2666.png?v8",diego_garcia:"unicode/1f1e9-1f1ec.png?v8",disappointed:"unicode/1f61e.png?v8",disappointed_relieved:"unicode/1f625.png?v8",disguised_face:"unicode/1f978.png?v8",diving_mask:"unicode/1f93f.png?v8",diya_lamp:"unicode/1fa94.png?v8",dizzy:"unicode/1f4ab.png?v8",dizzy_face:"unicode/1f635.png?v8",djibouti:"unicode/1f1e9-1f1ef.png?v8",dna:"unicode/1f9ec.png?v8",do_not_litter:"unicode/1f6af.png?v8",dodo:"unicode/1f9a4.png?v8",dog:"unicode/1f436.png?v8",dog2:"unicode/1f415.png?v8",dollar:"unicode/1f4b5.png?v8",dolls:"unicode/1f38e.png?v8",dolphin:"unicode/1f42c.png?v8",dominica:"unicode/1f1e9-1f1f2.png?v8",dominican_republic:"unicode/1f1e9-1f1f4.png?v8",door:"unicode/1f6aa.png?v8",doughnut:"unicode/1f369.png?v8",dove:"unicode/1f54a.png?v8",dragon:"unicode/1f409.png?v8",dragon_face:"unicode/1f432.png?v8",dress:"unicode/1f457.png?v8",dromedary_camel:"unicode/1f42a.png?v8",drooling_face:"unicode/1f924.png?v8",drop_of_blood:"unicode/1fa78.png?v8",droplet:"unicode/1f4a7.png?v8",drum:"unicode/1f941.png?v8",duck:"unicode/1f986.png?v8",dumpling:"unicode/1f95f.png?v8",dvd:"unicode/1f4c0.png?v8","e-mail":"unicode/1f4e7.png?v8",eagle:"unicode/1f985.png?v8",ear:"unicode/1f442.png?v8",ear_of_rice:"unicode/1f33e.png?v8",ear_with_hearing_aid:"unicode/1f9bb.png?v8",earth_africa:"unicode/1f30d.png?v8",earth_americas:"unicode/1f30e.png?v8",earth_asia:"unicode/1f30f.png?v8",ecuador:"unicode/1f1ea-1f1e8.png?v8",egg:"unicode/1f95a.png?v8",eggplant:"unicode/1f346.png?v8",egypt:"unicode/1f1ea-1f1ec.png?v8",eight:"unicode/0038-20e3.png?v8",eight_pointed_black_star:"unicode/2734.png?v8",eight_spoked_asterisk:"unicode/2733.png?v8",eject_button:"unicode/23cf.png?v8",el_salvador:"unicode/1f1f8-1f1fb.png?v8",electric_plug:"unicode/1f50c.png?v8",electron:"electron.png?v8",elephant:"unicode/1f418.png?v8",elevator:"unicode/1f6d7.png?v8",elf:"unicode/1f9dd.png?v8",elf_man:"unicode/1f9dd-2642.png?v8",elf_woman:"unicode/1f9dd-2640.png?v8",email:"unicode/1f4e7.png?v8",end:"unicode/1f51a.png?v8",england:"unicode/1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png?v8",envelope:"unicode/2709.png?v8",envelope_with_arrow:"unicode/1f4e9.png?v8",equatorial_guinea:"unicode/1f1ec-1f1f6.png?v8",eritrea:"unicode/1f1ea-1f1f7.png?v8",es:"unicode/1f1ea-1f1f8.png?v8",estonia:"unicode/1f1ea-1f1ea.png?v8",ethiopia:"unicode/1f1ea-1f1f9.png?v8",eu:"unicode/1f1ea-1f1fa.png?v8",euro:"unicode/1f4b6.png?v8",european_castle:"unicode/1f3f0.png?v8",european_post_office:"unicode/1f3e4.png?v8",european_union:"unicode/1f1ea-1f1fa.png?v8",evergreen_tree:"unicode/1f332.png?v8",exclamation:"unicode/2757.png?v8",exploding_head:"unicode/1f92f.png?v8",expressionless:"unicode/1f611.png?v8",eye:"unicode/1f441.png?v8",eye_speech_bubble:"unicode/1f441-1f5e8.png?v8",eyeglasses:"unicode/1f453.png?v8",eyes:"unicode/1f440.png?v8",face_exhaling:"unicode/1f62e-1f4a8.png?v8",face_in_clouds:"unicode/1f636-1f32b.png?v8",face_with_head_bandage:"unicode/1f915.png?v8",face_with_spiral_eyes:"unicode/1f635-1f4ab.png?v8",face_with_thermometer:"unicode/1f912.png?v8",facepalm:"unicode/1f926.png?v8",facepunch:"unicode/1f44a.png?v8",factory:"unicode/1f3ed.png?v8",factory_worker:"unicode/1f9d1-1f3ed.png?v8",fairy:"unicode/1f9da.png?v8",fairy_man:"unicode/1f9da-2642.png?v8",fairy_woman:"unicode/1f9da-2640.png?v8",falafel:"unicode/1f9c6.png?v8",falkland_islands:"unicode/1f1eb-1f1f0.png?v8",fallen_leaf:"unicode/1f342.png?v8",family:"unicode/1f46a.png?v8",family_man_boy:"unicode/1f468-1f466.png?v8",family_man_boy_boy:"unicode/1f468-1f466-1f466.png?v8",family_man_girl:"unicode/1f468-1f467.png?v8",family_man_girl_boy:"unicode/1f468-1f467-1f466.png?v8",family_man_girl_girl:"unicode/1f468-1f467-1f467.png?v8",family_man_man_boy:"unicode/1f468-1f468-1f466.png?v8",family_man_man_boy_boy:"unicode/1f468-1f468-1f466-1f466.png?v8",family_man_man_girl:"unicode/1f468-1f468-1f467.png?v8",family_man_man_girl_boy:"unicode/1f468-1f468-1f467-1f466.png?v8",family_man_man_girl_girl:"unicode/1f468-1f468-1f467-1f467.png?v8",family_man_woman_boy:"unicode/1f468-1f469-1f466.png?v8",family_man_woman_boy_boy:"unicode/1f468-1f469-1f466-1f466.png?v8",family_man_woman_girl:"unicode/1f468-1f469-1f467.png?v8",family_man_woman_girl_boy:"unicode/1f468-1f469-1f467-1f466.png?v8",family_man_woman_girl_girl:"unicode/1f468-1f469-1f467-1f467.png?v8",family_woman_boy:"unicode/1f469-1f466.png?v8",family_woman_boy_boy:"unicode/1f469-1f466-1f466.png?v8",family_woman_girl:"unicode/1f469-1f467.png?v8",family_woman_girl_boy:"unicode/1f469-1f467-1f466.png?v8",family_woman_girl_girl:"unicode/1f469-1f467-1f467.png?v8",family_woman_woman_boy:"unicode/1f469-1f469-1f466.png?v8",family_woman_woman_boy_boy:"unicode/1f469-1f469-1f466-1f466.png?v8",family_woman_woman_girl:"unicode/1f469-1f469-1f467.png?v8",family_woman_woman_girl_boy:"unicode/1f469-1f469-1f467-1f466.png?v8",family_woman_woman_girl_girl:"unicode/1f469-1f469-1f467-1f467.png?v8",farmer:"unicode/1f9d1-1f33e.png?v8",faroe_islands:"unicode/1f1eb-1f1f4.png?v8",fast_forward:"unicode/23e9.png?v8",fax:"unicode/1f4e0.png?v8",fearful:"unicode/1f628.png?v8",feather:"unicode/1fab6.png?v8",feelsgood:"feelsgood.png?v8",feet:"unicode/1f43e.png?v8",female_detective:"unicode/1f575-2640.png?v8",female_sign:"unicode/2640.png?v8",ferris_wheel:"unicode/1f3a1.png?v8",ferry:"unicode/26f4.png?v8",field_hockey:"unicode/1f3d1.png?v8",fiji:"unicode/1f1eb-1f1ef.png?v8",file_cabinet:"unicode/1f5c4.png?v8",file_folder:"unicode/1f4c1.png?v8",film_projector:"unicode/1f4fd.png?v8",film_strip:"unicode/1f39e.png?v8",finland:"unicode/1f1eb-1f1ee.png?v8",finnadie:"finnadie.png?v8",fire:"unicode/1f525.png?v8",fire_engine:"unicode/1f692.png?v8",fire_extinguisher:"unicode/1f9ef.png?v8",firecracker:"unicode/1f9e8.png?v8",firefighter:"unicode/1f9d1-1f692.png?v8",fireworks:"unicode/1f386.png?v8",first_quarter_moon:"unicode/1f313.png?v8",first_quarter_moon_with_face:"unicode/1f31b.png?v8",fish:"unicode/1f41f.png?v8",fish_cake:"unicode/1f365.png?v8",fishing_pole_and_fish:"unicode/1f3a3.png?v8",fist:"unicode/270a.png?v8",fist_left:"unicode/1f91b.png?v8",fist_oncoming:"unicode/1f44a.png?v8",fist_raised:"unicode/270a.png?v8",fist_right:"unicode/1f91c.png?v8",five:"unicode/0035-20e3.png?v8",flags:"unicode/1f38f.png?v8",flamingo:"unicode/1f9a9.png?v8",flashlight:"unicode/1f526.png?v8",flat_shoe:"unicode/1f97f.png?v8",flatbread:"unicode/1fad3.png?v8",fleur_de_lis:"unicode/269c.png?v8",flight_arrival:"unicode/1f6ec.png?v8",flight_departure:"unicode/1f6eb.png?v8",flipper:"unicode/1f42c.png?v8",floppy_disk:"unicode/1f4be.png?v8",flower_playing_cards:"unicode/1f3b4.png?v8",flushed:"unicode/1f633.png?v8",fly:"unicode/1fab0.png?v8",flying_disc:"unicode/1f94f.png?v8",flying_saucer:"unicode/1f6f8.png?v8",fog:"unicode/1f32b.png?v8",foggy:"unicode/1f301.png?v8",fondue:"unicode/1fad5.png?v8",foot:"unicode/1f9b6.png?v8",football:"unicode/1f3c8.png?v8",footprints:"unicode/1f463.png?v8",fork_and_knife:"unicode/1f374.png?v8",fortune_cookie:"unicode/1f960.png?v8",fountain:"unicode/26f2.png?v8",fountain_pen:"unicode/1f58b.png?v8",four:"unicode/0034-20e3.png?v8",four_leaf_clover:"unicode/1f340.png?v8",fox_face:"unicode/1f98a.png?v8",fr:"unicode/1f1eb-1f1f7.png?v8",framed_picture:"unicode/1f5bc.png?v8",free:"unicode/1f193.png?v8",french_guiana:"unicode/1f1ec-1f1eb.png?v8",french_polynesia:"unicode/1f1f5-1f1eb.png?v8",french_southern_territories:"unicode/1f1f9-1f1eb.png?v8",fried_egg:"unicode/1f373.png?v8",fried_shrimp:"unicode/1f364.png?v8",fries:"unicode/1f35f.png?v8",frog:"unicode/1f438.png?v8",frowning:"unicode/1f626.png?v8",frowning_face:"unicode/2639.png?v8",frowning_man:"unicode/1f64d-2642.png?v8",frowning_person:"unicode/1f64d.png?v8",frowning_woman:"unicode/1f64d-2640.png?v8",fu:"unicode/1f595.png?v8",fuelpump:"unicode/26fd.png?v8",full_moon:"unicode/1f315.png?v8",full_moon_with_face:"unicode/1f31d.png?v8",funeral_urn:"unicode/26b1.png?v8",gabon:"unicode/1f1ec-1f1e6.png?v8",gambia:"unicode/1f1ec-1f1f2.png?v8",game_die:"unicode/1f3b2.png?v8",garlic:"unicode/1f9c4.png?v8",gb:"unicode/1f1ec-1f1e7.png?v8",gear:"unicode/2699.png?v8",gem:"unicode/1f48e.png?v8",gemini:"unicode/264a.png?v8",genie:"unicode/1f9de.png?v8",genie_man:"unicode/1f9de-2642.png?v8",genie_woman:"unicode/1f9de-2640.png?v8",georgia:"unicode/1f1ec-1f1ea.png?v8",ghana:"unicode/1f1ec-1f1ed.png?v8",ghost:"unicode/1f47b.png?v8",gibraltar:"unicode/1f1ec-1f1ee.png?v8",gift:"unicode/1f381.png?v8",gift_heart:"unicode/1f49d.png?v8",giraffe:"unicode/1f992.png?v8",girl:"unicode/1f467.png?v8",globe_with_meridians:"unicode/1f310.png?v8",gloves:"unicode/1f9e4.png?v8",goal_net:"unicode/1f945.png?v8",goat:"unicode/1f410.png?v8",goberserk:"goberserk.png?v8",godmode:"godmode.png?v8",goggles:"unicode/1f97d.png?v8",golf:"unicode/26f3.png?v8",golfing:"unicode/1f3cc.png?v8",golfing_man:"unicode/1f3cc-2642.png?v8",golfing_woman:"unicode/1f3cc-2640.png?v8",gorilla:"unicode/1f98d.png?v8",grapes:"unicode/1f347.png?v8",greece:"unicode/1f1ec-1f1f7.png?v8",green_apple:"unicode/1f34f.png?v8",green_book:"unicode/1f4d7.png?v8",green_circle:"unicode/1f7e2.png?v8",green_heart:"unicode/1f49a.png?v8",green_salad:"unicode/1f957.png?v8",green_square:"unicode/1f7e9.png?v8",greenland:"unicode/1f1ec-1f1f1.png?v8",grenada:"unicode/1f1ec-1f1e9.png?v8",grey_exclamation:"unicode/2755.png?v8",grey_question:"unicode/2754.png?v8",grimacing:"unicode/1f62c.png?v8",grin:"unicode/1f601.png?v8",grinning:"unicode/1f600.png?v8",guadeloupe:"unicode/1f1ec-1f1f5.png?v8",guam:"unicode/1f1ec-1f1fa.png?v8",guard:"unicode/1f482.png?v8",guardsman:"unicode/1f482-2642.png?v8",guardswoman:"unicode/1f482-2640.png?v8",guatemala:"unicode/1f1ec-1f1f9.png?v8",guernsey:"unicode/1f1ec-1f1ec.png?v8",guide_dog:"unicode/1f9ae.png?v8",guinea:"unicode/1f1ec-1f1f3.png?v8",guinea_bissau:"unicode/1f1ec-1f1fc.png?v8",guitar:"unicode/1f3b8.png?v8",gun:"unicode/1f52b.png?v8",guyana:"unicode/1f1ec-1f1fe.png?v8",haircut:"unicode/1f487.png?v8",haircut_man:"unicode/1f487-2642.png?v8",haircut_woman:"unicode/1f487-2640.png?v8",haiti:"unicode/1f1ed-1f1f9.png?v8",hamburger:"unicode/1f354.png?v8",hammer:"unicode/1f528.png?v8",hammer_and_pick:"unicode/2692.png?v8",hammer_and_wrench:"unicode/1f6e0.png?v8",hamster:"unicode/1f439.png?v8",hand:"unicode/270b.png?v8",hand_over_mouth:"unicode/1f92d.png?v8",handbag:"unicode/1f45c.png?v8",handball_person:"unicode/1f93e.png?v8",handshake:"unicode/1f91d.png?v8",hankey:"unicode/1f4a9.png?v8",hash:"unicode/0023-20e3.png?v8",hatched_chick:"unicode/1f425.png?v8",hatching_chick:"unicode/1f423.png?v8",headphones:"unicode/1f3a7.png?v8",headstone:"unicode/1faa6.png?v8",health_worker:"unicode/1f9d1-2695.png?v8",hear_no_evil:"unicode/1f649.png?v8",heard_mcdonald_islands:"unicode/1f1ed-1f1f2.png?v8",heart:"unicode/2764.png?v8",heart_decoration:"unicode/1f49f.png?v8",heart_eyes:"unicode/1f60d.png?v8",heart_eyes_cat:"unicode/1f63b.png?v8",heart_on_fire:"unicode/2764-1f525.png?v8",heartbeat:"unicode/1f493.png?v8",heartpulse:"unicode/1f497.png?v8",hearts:"unicode/2665.png?v8",heavy_check_mark:"unicode/2714.png?v8",heavy_division_sign:"unicode/2797.png?v8",heavy_dollar_sign:"unicode/1f4b2.png?v8",heavy_exclamation_mark:"unicode/2757.png?v8",heavy_heart_exclamation:"unicode/2763.png?v8",heavy_minus_sign:"unicode/2796.png?v8",heavy_multiplication_x:"unicode/2716.png?v8",heavy_plus_sign:"unicode/2795.png?v8",hedgehog:"unicode/1f994.png?v8",helicopter:"unicode/1f681.png?v8",herb:"unicode/1f33f.png?v8",hibiscus:"unicode/1f33a.png?v8",high_brightness:"unicode/1f506.png?v8",high_heel:"unicode/1f460.png?v8",hiking_boot:"unicode/1f97e.png?v8",hindu_temple:"unicode/1f6d5.png?v8",hippopotamus:"unicode/1f99b.png?v8",hocho:"unicode/1f52a.png?v8",hole:"unicode/1f573.png?v8",honduras:"unicode/1f1ed-1f1f3.png?v8",honey_pot:"unicode/1f36f.png?v8",honeybee:"unicode/1f41d.png?v8",hong_kong:"unicode/1f1ed-1f1f0.png?v8",hook:"unicode/1fa9d.png?v8",horse:"unicode/1f434.png?v8",horse_racing:"unicode/1f3c7.png?v8",hospital:"unicode/1f3e5.png?v8",hot_face:"unicode/1f975.png?v8",hot_pepper:"unicode/1f336.png?v8",hotdog:"unicode/1f32d.png?v8",hotel:"unicode/1f3e8.png?v8",hotsprings:"unicode/2668.png?v8",hourglass:"unicode/231b.png?v8",hourglass_flowing_sand:"unicode/23f3.png?v8",house:"unicode/1f3e0.png?v8",house_with_garden:"unicode/1f3e1.png?v8",houses:"unicode/1f3d8.png?v8",hugs:"unicode/1f917.png?v8",hungary:"unicode/1f1ed-1f1fa.png?v8",hurtrealbad:"hurtrealbad.png?v8",hushed:"unicode/1f62f.png?v8",hut:"unicode/1f6d6.png?v8",ice_cream:"unicode/1f368.png?v8",ice_cube:"unicode/1f9ca.png?v8",ice_hockey:"unicode/1f3d2.png?v8",ice_skate:"unicode/26f8.png?v8",icecream:"unicode/1f366.png?v8",iceland:"unicode/1f1ee-1f1f8.png?v8",id:"unicode/1f194.png?v8",ideograph_advantage:"unicode/1f250.png?v8",imp:"unicode/1f47f.png?v8",inbox_tray:"unicode/1f4e5.png?v8",incoming_envelope:"unicode/1f4e8.png?v8",india:"unicode/1f1ee-1f1f3.png?v8",indonesia:"unicode/1f1ee-1f1e9.png?v8",infinity:"unicode/267e.png?v8",information_desk_person:"unicode/1f481.png?v8",information_source:"unicode/2139.png?v8",innocent:"unicode/1f607.png?v8",interrobang:"unicode/2049.png?v8",iphone:"unicode/1f4f1.png?v8",iran:"unicode/1f1ee-1f1f7.png?v8",iraq:"unicode/1f1ee-1f1f6.png?v8",ireland:"unicode/1f1ee-1f1ea.png?v8",isle_of_man:"unicode/1f1ee-1f1f2.png?v8",israel:"unicode/1f1ee-1f1f1.png?v8",it:"unicode/1f1ee-1f1f9.png?v8",izakaya_lantern:"unicode/1f3ee.png?v8",jack_o_lantern:"unicode/1f383.png?v8",jamaica:"unicode/1f1ef-1f1f2.png?v8",japan:"unicode/1f5fe.png?v8",japanese_castle:"unicode/1f3ef.png?v8",japanese_goblin:"unicode/1f47a.png?v8",japanese_ogre:"unicode/1f479.png?v8",jeans:"unicode/1f456.png?v8",jersey:"unicode/1f1ef-1f1ea.png?v8",jigsaw:"unicode/1f9e9.png?v8",jordan:"unicode/1f1ef-1f1f4.png?v8",joy:"unicode/1f602.png?v8",joy_cat:"unicode/1f639.png?v8",joystick:"unicode/1f579.png?v8",jp:"unicode/1f1ef-1f1f5.png?v8",judge:"unicode/1f9d1-2696.png?v8",juggling_person:"unicode/1f939.png?v8",kaaba:"unicode/1f54b.png?v8",kangaroo:"unicode/1f998.png?v8",kazakhstan:"unicode/1f1f0-1f1ff.png?v8",kenya:"unicode/1f1f0-1f1ea.png?v8",key:"unicode/1f511.png?v8",keyboard:"unicode/2328.png?v8",keycap_ten:"unicode/1f51f.png?v8",kick_scooter:"unicode/1f6f4.png?v8",kimono:"unicode/1f458.png?v8",kiribati:"unicode/1f1f0-1f1ee.png?v8",kiss:"unicode/1f48b.png?v8",kissing:"unicode/1f617.png?v8",kissing_cat:"unicode/1f63d.png?v8",kissing_closed_eyes:"unicode/1f61a.png?v8",kissing_heart:"unicode/1f618.png?v8",kissing_smiling_eyes:"unicode/1f619.png?v8",kite:"unicode/1fa81.png?v8",kiwi_fruit:"unicode/1f95d.png?v8",kneeling_man:"unicode/1f9ce-2642.png?v8",kneeling_person:"unicode/1f9ce.png?v8",kneeling_woman:"unicode/1f9ce-2640.png?v8",knife:"unicode/1f52a.png?v8",knot:"unicode/1faa2.png?v8",koala:"unicode/1f428.png?v8",koko:"unicode/1f201.png?v8",kosovo:"unicode/1f1fd-1f1f0.png?v8",kr:"unicode/1f1f0-1f1f7.png?v8",kuwait:"unicode/1f1f0-1f1fc.png?v8",kyrgyzstan:"unicode/1f1f0-1f1ec.png?v8",lab_coat:"unicode/1f97c.png?v8",label:"unicode/1f3f7.png?v8",lacrosse:"unicode/1f94d.png?v8",ladder:"unicode/1fa9c.png?v8",lady_beetle:"unicode/1f41e.png?v8",lantern:"unicode/1f3ee.png?v8",laos:"unicode/1f1f1-1f1e6.png?v8",large_blue_circle:"unicode/1f535.png?v8",large_blue_diamond:"unicode/1f537.png?v8",large_orange_diamond:"unicode/1f536.png?v8",last_quarter_moon:"unicode/1f317.png?v8",last_quarter_moon_with_face:"unicode/1f31c.png?v8",latin_cross:"unicode/271d.png?v8",latvia:"unicode/1f1f1-1f1fb.png?v8",laughing:"unicode/1f606.png?v8",leafy_green:"unicode/1f96c.png?v8",leaves:"unicode/1f343.png?v8",lebanon:"unicode/1f1f1-1f1e7.png?v8",ledger:"unicode/1f4d2.png?v8",left_luggage:"unicode/1f6c5.png?v8",left_right_arrow:"unicode/2194.png?v8",left_speech_bubble:"unicode/1f5e8.png?v8",leftwards_arrow_with_hook:"unicode/21a9.png?v8",leg:"unicode/1f9b5.png?v8",lemon:"unicode/1f34b.png?v8",leo:"unicode/264c.png?v8",leopard:"unicode/1f406.png?v8",lesotho:"unicode/1f1f1-1f1f8.png?v8",level_slider:"unicode/1f39a.png?v8",liberia:"unicode/1f1f1-1f1f7.png?v8",libra:"unicode/264e.png?v8",libya:"unicode/1f1f1-1f1fe.png?v8",liechtenstein:"unicode/1f1f1-1f1ee.png?v8",light_rail:"unicode/1f688.png?v8",link:"unicode/1f517.png?v8",lion:"unicode/1f981.png?v8",lips:"unicode/1f444.png?v8",lipstick:"unicode/1f484.png?v8",lithuania:"unicode/1f1f1-1f1f9.png?v8",lizard:"unicode/1f98e.png?v8",llama:"unicode/1f999.png?v8",lobster:"unicode/1f99e.png?v8",lock:"unicode/1f512.png?v8",lock_with_ink_pen:"unicode/1f50f.png?v8",lollipop:"unicode/1f36d.png?v8",long_drum:"unicode/1fa98.png?v8",loop:"unicode/27bf.png?v8",lotion_bottle:"unicode/1f9f4.png?v8",lotus_position:"unicode/1f9d8.png?v8",lotus_position_man:"unicode/1f9d8-2642.png?v8",lotus_position_woman:"unicode/1f9d8-2640.png?v8",loud_sound:"unicode/1f50a.png?v8",loudspeaker:"unicode/1f4e2.png?v8",love_hotel:"unicode/1f3e9.png?v8",love_letter:"unicode/1f48c.png?v8",love_you_gesture:"unicode/1f91f.png?v8",low_brightness:"unicode/1f505.png?v8",luggage:"unicode/1f9f3.png?v8",lungs:"unicode/1fac1.png?v8",luxembourg:"unicode/1f1f1-1f1fa.png?v8",lying_face:"unicode/1f925.png?v8",m:"unicode/24c2.png?v8",macau:"unicode/1f1f2-1f1f4.png?v8",macedonia:"unicode/1f1f2-1f1f0.png?v8",madagascar:"unicode/1f1f2-1f1ec.png?v8",mag:"unicode/1f50d.png?v8",mag_right:"unicode/1f50e.png?v8",mage:"unicode/1f9d9.png?v8",mage_man:"unicode/1f9d9-2642.png?v8",mage_woman:"unicode/1f9d9-2640.png?v8",magic_wand:"unicode/1fa84.png?v8",magnet:"unicode/1f9f2.png?v8",mahjong:"unicode/1f004.png?v8",mailbox:"unicode/1f4eb.png?v8",mailbox_closed:"unicode/1f4ea.png?v8",mailbox_with_mail:"unicode/1f4ec.png?v8",mailbox_with_no_mail:"unicode/1f4ed.png?v8",malawi:"unicode/1f1f2-1f1fc.png?v8",malaysia:"unicode/1f1f2-1f1fe.png?v8",maldives:"unicode/1f1f2-1f1fb.png?v8",male_detective:"unicode/1f575-2642.png?v8",male_sign:"unicode/2642.png?v8",mali:"unicode/1f1f2-1f1f1.png?v8",malta:"unicode/1f1f2-1f1f9.png?v8",mammoth:"unicode/1f9a3.png?v8",man:"unicode/1f468.png?v8",man_artist:"unicode/1f468-1f3a8.png?v8",man_astronaut:"unicode/1f468-1f680.png?v8",man_beard:"unicode/1f9d4-2642.png?v8",man_cartwheeling:"unicode/1f938-2642.png?v8",man_cook:"unicode/1f468-1f373.png?v8",man_dancing:"unicode/1f57a.png?v8",man_facepalming:"unicode/1f926-2642.png?v8",man_factory_worker:"unicode/1f468-1f3ed.png?v8",man_farmer:"unicode/1f468-1f33e.png?v8",man_feeding_baby:"unicode/1f468-1f37c.png?v8",man_firefighter:"unicode/1f468-1f692.png?v8",man_health_worker:"unicode/1f468-2695.png?v8",man_in_manual_wheelchair:"unicode/1f468-1f9bd.png?v8",man_in_motorized_wheelchair:"unicode/1f468-1f9bc.png?v8",man_in_tuxedo:"unicode/1f935-2642.png?v8",man_judge:"unicode/1f468-2696.png?v8",man_juggling:"unicode/1f939-2642.png?v8",man_mechanic:"unicode/1f468-1f527.png?v8",man_office_worker:"unicode/1f468-1f4bc.png?v8",man_pilot:"unicode/1f468-2708.png?v8",man_playing_handball:"unicode/1f93e-2642.png?v8",man_playing_water_polo:"unicode/1f93d-2642.png?v8",man_scientist:"unicode/1f468-1f52c.png?v8",man_shrugging:"unicode/1f937-2642.png?v8",man_singer:"unicode/1f468-1f3a4.png?v8",man_student:"unicode/1f468-1f393.png?v8",man_teacher:"unicode/1f468-1f3eb.png?v8",man_technologist:"unicode/1f468-1f4bb.png?v8",man_with_gua_pi_mao:"unicode/1f472.png?v8",man_with_probing_cane:"unicode/1f468-1f9af.png?v8",man_with_turban:"unicode/1f473-2642.png?v8",man_with_veil:"unicode/1f470-2642.png?v8",mandarin:"unicode/1f34a.png?v8",mango:"unicode/1f96d.png?v8",mans_shoe:"unicode/1f45e.png?v8",mantelpiece_clock:"unicode/1f570.png?v8",manual_wheelchair:"unicode/1f9bd.png?v8",maple_leaf:"unicode/1f341.png?v8",marshall_islands:"unicode/1f1f2-1f1ed.png?v8",martial_arts_uniform:"unicode/1f94b.png?v8",martinique:"unicode/1f1f2-1f1f6.png?v8",mask:"unicode/1f637.png?v8",massage:"unicode/1f486.png?v8",massage_man:"unicode/1f486-2642.png?v8",massage_woman:"unicode/1f486-2640.png?v8",mate:"unicode/1f9c9.png?v8",mauritania:"unicode/1f1f2-1f1f7.png?v8",mauritius:"unicode/1f1f2-1f1fa.png?v8",mayotte:"unicode/1f1fe-1f1f9.png?v8",meat_on_bone:"unicode/1f356.png?v8",mechanic:"unicode/1f9d1-1f527.png?v8",mechanical_arm:"unicode/1f9be.png?v8",mechanical_leg:"unicode/1f9bf.png?v8",medal_military:"unicode/1f396.png?v8",medal_sports:"unicode/1f3c5.png?v8",medical_symbol:"unicode/2695.png?v8",mega:"unicode/1f4e3.png?v8",melon:"unicode/1f348.png?v8",memo:"unicode/1f4dd.png?v8",men_wrestling:"unicode/1f93c-2642.png?v8",mending_heart:"unicode/2764-1fa79.png?v8",menorah:"unicode/1f54e.png?v8",mens:"unicode/1f6b9.png?v8",mermaid:"unicode/1f9dc-2640.png?v8",merman:"unicode/1f9dc-2642.png?v8",merperson:"unicode/1f9dc.png?v8",metal:"unicode/1f918.png?v8",metro:"unicode/1f687.png?v8",mexico:"unicode/1f1f2-1f1fd.png?v8",microbe:"unicode/1f9a0.png?v8",micronesia:"unicode/1f1eb-1f1f2.png?v8",microphone:"unicode/1f3a4.png?v8",microscope:"unicode/1f52c.png?v8",middle_finger:"unicode/1f595.png?v8",military_helmet:"unicode/1fa96.png?v8",milk_glass:"unicode/1f95b.png?v8",milky_way:"unicode/1f30c.png?v8",minibus:"unicode/1f690.png?v8",minidisc:"unicode/1f4bd.png?v8",mirror:"unicode/1fa9e.png?v8",mobile_phone_off:"unicode/1f4f4.png?v8",moldova:"unicode/1f1f2-1f1e9.png?v8",monaco:"unicode/1f1f2-1f1e8.png?v8",money_mouth_face:"unicode/1f911.png?v8",money_with_wings:"unicode/1f4b8.png?v8",moneybag:"unicode/1f4b0.png?v8",mongolia:"unicode/1f1f2-1f1f3.png?v8",monkey:"unicode/1f412.png?v8",monkey_face:"unicode/1f435.png?v8",monocle_face:"unicode/1f9d0.png?v8",monorail:"unicode/1f69d.png?v8",montenegro:"unicode/1f1f2-1f1ea.png?v8",montserrat:"unicode/1f1f2-1f1f8.png?v8",moon:"unicode/1f314.png?v8",moon_cake:"unicode/1f96e.png?v8",morocco:"unicode/1f1f2-1f1e6.png?v8",mortar_board:"unicode/1f393.png?v8",mosque:"unicode/1f54c.png?v8",mosquito:"unicode/1f99f.png?v8",motor_boat:"unicode/1f6e5.png?v8",motor_scooter:"unicode/1f6f5.png?v8",motorcycle:"unicode/1f3cd.png?v8",motorized_wheelchair:"unicode/1f9bc.png?v8",motorway:"unicode/1f6e3.png?v8",mount_fuji:"unicode/1f5fb.png?v8",mountain:"unicode/26f0.png?v8",mountain_bicyclist:"unicode/1f6b5.png?v8",mountain_biking_man:"unicode/1f6b5-2642.png?v8",mountain_biking_woman:"unicode/1f6b5-2640.png?v8",mountain_cableway:"unicode/1f6a0.png?v8",mountain_railway:"unicode/1f69e.png?v8",mountain_snow:"unicode/1f3d4.png?v8",mouse:"unicode/1f42d.png?v8",mouse2:"unicode/1f401.png?v8",mouse_trap:"unicode/1faa4.png?v8",movie_camera:"unicode/1f3a5.png?v8",moyai:"unicode/1f5ff.png?v8",mozambique:"unicode/1f1f2-1f1ff.png?v8",mrs_claus:"unicode/1f936.png?v8",muscle:"unicode/1f4aa.png?v8",mushroom:"unicode/1f344.png?v8",musical_keyboard:"unicode/1f3b9.png?v8",musical_note:"unicode/1f3b5.png?v8",musical_score:"unicode/1f3bc.png?v8",mute:"unicode/1f507.png?v8",mx_claus:"unicode/1f9d1-1f384.png?v8",myanmar:"unicode/1f1f2-1f1f2.png?v8",nail_care:"unicode/1f485.png?v8",name_badge:"unicode/1f4db.png?v8",namibia:"unicode/1f1f3-1f1e6.png?v8",national_park:"unicode/1f3de.png?v8",nauru:"unicode/1f1f3-1f1f7.png?v8",nauseated_face:"unicode/1f922.png?v8",nazar_amulet:"unicode/1f9ff.png?v8",neckbeard:"neckbeard.png?v8",necktie:"unicode/1f454.png?v8",negative_squared_cross_mark:"unicode/274e.png?v8",nepal:"unicode/1f1f3-1f1f5.png?v8",nerd_face:"unicode/1f913.png?v8",nesting_dolls:"unicode/1fa86.png?v8",netherlands:"unicode/1f1f3-1f1f1.png?v8",neutral_face:"unicode/1f610.png?v8",new:"unicode/1f195.png?v8",new_caledonia:"unicode/1f1f3-1f1e8.png?v8",new_moon:"unicode/1f311.png?v8",new_moon_with_face:"unicode/1f31a.png?v8",new_zealand:"unicode/1f1f3-1f1ff.png?v8",newspaper:"unicode/1f4f0.png?v8",newspaper_roll:"unicode/1f5de.png?v8",next_track_button:"unicode/23ed.png?v8",ng:"unicode/1f196.png?v8",ng_man:"unicode/1f645-2642.png?v8",ng_woman:"unicode/1f645-2640.png?v8",nicaragua:"unicode/1f1f3-1f1ee.png?v8",niger:"unicode/1f1f3-1f1ea.png?v8",nigeria:"unicode/1f1f3-1f1ec.png?v8",night_with_stars:"unicode/1f303.png?v8",nine:"unicode/0039-20e3.png?v8",ninja:"unicode/1f977.png?v8",niue:"unicode/1f1f3-1f1fa.png?v8",no_bell:"unicode/1f515.png?v8",no_bicycles:"unicode/1f6b3.png?v8",no_entry:"unicode/26d4.png?v8",no_entry_sign:"unicode/1f6ab.png?v8",no_good:"unicode/1f645.png?v8",no_good_man:"unicode/1f645-2642.png?v8",no_good_woman:"unicode/1f645-2640.png?v8",no_mobile_phones:"unicode/1f4f5.png?v8",no_mouth:"unicode/1f636.png?v8",no_pedestrians:"unicode/1f6b7.png?v8",no_smoking:"unicode/1f6ad.png?v8","non-potable_water":"unicode/1f6b1.png?v8",norfolk_island:"unicode/1f1f3-1f1eb.png?v8",north_korea:"unicode/1f1f0-1f1f5.png?v8",northern_mariana_islands:"unicode/1f1f2-1f1f5.png?v8",norway:"unicode/1f1f3-1f1f4.png?v8",nose:"unicode/1f443.png?v8",notebook:"unicode/1f4d3.png?v8",notebook_with_decorative_cover:"unicode/1f4d4.png?v8",notes:"unicode/1f3b6.png?v8",nut_and_bolt:"unicode/1f529.png?v8",o:"unicode/2b55.png?v8",o2:"unicode/1f17e.png?v8",ocean:"unicode/1f30a.png?v8",octocat:"octocat.png?v8",octopus:"unicode/1f419.png?v8",oden:"unicode/1f362.png?v8",office:"unicode/1f3e2.png?v8",office_worker:"unicode/1f9d1-1f4bc.png?v8",oil_drum:"unicode/1f6e2.png?v8",ok:"unicode/1f197.png?v8",ok_hand:"unicode/1f44c.png?v8",ok_man:"unicode/1f646-2642.png?v8",ok_person:"unicode/1f646.png?v8",ok_woman:"unicode/1f646-2640.png?v8",old_key:"unicode/1f5dd.png?v8",older_adult:"unicode/1f9d3.png?v8",older_man:"unicode/1f474.png?v8",older_woman:"unicode/1f475.png?v8",olive:"unicode/1fad2.png?v8",om:"unicode/1f549.png?v8",oman:"unicode/1f1f4-1f1f2.png?v8",on:"unicode/1f51b.png?v8",oncoming_automobile:"unicode/1f698.png?v8",oncoming_bus:"unicode/1f68d.png?v8",oncoming_police_car:"unicode/1f694.png?v8",oncoming_taxi:"unicode/1f696.png?v8",one:"unicode/0031-20e3.png?v8",one_piece_swimsuit:"unicode/1fa71.png?v8",onion:"unicode/1f9c5.png?v8",open_book:"unicode/1f4d6.png?v8",open_file_folder:"unicode/1f4c2.png?v8",open_hands:"unicode/1f450.png?v8",open_mouth:"unicode/1f62e.png?v8",open_umbrella:"unicode/2602.png?v8",ophiuchus:"unicode/26ce.png?v8",orange:"unicode/1f34a.png?v8",orange_book:"unicode/1f4d9.png?v8",orange_circle:"unicode/1f7e0.png?v8",orange_heart:"unicode/1f9e1.png?v8",orange_square:"unicode/1f7e7.png?v8",orangutan:"unicode/1f9a7.png?v8",orthodox_cross:"unicode/2626.png?v8",otter:"unicode/1f9a6.png?v8",outbox_tray:"unicode/1f4e4.png?v8",owl:"unicode/1f989.png?v8",ox:"unicode/1f402.png?v8",oyster:"unicode/1f9aa.png?v8",package:"unicode/1f4e6.png?v8",page_facing_up:"unicode/1f4c4.png?v8",page_with_curl:"unicode/1f4c3.png?v8",pager:"unicode/1f4df.png?v8",paintbrush:"unicode/1f58c.png?v8",pakistan:"unicode/1f1f5-1f1f0.png?v8",palau:"unicode/1f1f5-1f1fc.png?v8",palestinian_territories:"unicode/1f1f5-1f1f8.png?v8",palm_tree:"unicode/1f334.png?v8",palms_up_together:"unicode/1f932.png?v8",panama:"unicode/1f1f5-1f1e6.png?v8",pancakes:"unicode/1f95e.png?v8",panda_face:"unicode/1f43c.png?v8",paperclip:"unicode/1f4ce.png?v8",paperclips:"unicode/1f587.png?v8",papua_new_guinea:"unicode/1f1f5-1f1ec.png?v8",parachute:"unicode/1fa82.png?v8",paraguay:"unicode/1f1f5-1f1fe.png?v8",parasol_on_ground:"unicode/26f1.png?v8",parking:"unicode/1f17f.png?v8",parrot:"unicode/1f99c.png?v8",part_alternation_mark:"unicode/303d.png?v8",partly_sunny:"unicode/26c5.png?v8",partying_face:"unicode/1f973.png?v8",passenger_ship:"unicode/1f6f3.png?v8",passport_control:"unicode/1f6c2.png?v8",pause_button:"unicode/23f8.png?v8",paw_prints:"unicode/1f43e.png?v8",peace_symbol:"unicode/262e.png?v8",peach:"unicode/1f351.png?v8",peacock:"unicode/1f99a.png?v8",peanuts:"unicode/1f95c.png?v8",pear:"unicode/1f350.png?v8",pen:"unicode/1f58a.png?v8",pencil:"unicode/1f4dd.png?v8",pencil2:"unicode/270f.png?v8",penguin:"unicode/1f427.png?v8",pensive:"unicode/1f614.png?v8",people_holding_hands:"unicode/1f9d1-1f91d-1f9d1.png?v8",people_hugging:"unicode/1fac2.png?v8",performing_arts:"unicode/1f3ad.png?v8",persevere:"unicode/1f623.png?v8",person_bald:"unicode/1f9d1-1f9b2.png?v8",person_curly_hair:"unicode/1f9d1-1f9b1.png?v8",person_feeding_baby:"unicode/1f9d1-1f37c.png?v8",person_fencing:"unicode/1f93a.png?v8",person_in_manual_wheelchair:"unicode/1f9d1-1f9bd.png?v8",person_in_motorized_wheelchair:"unicode/1f9d1-1f9bc.png?v8",person_in_tuxedo:"unicode/1f935.png?v8",person_red_hair:"unicode/1f9d1-1f9b0.png?v8",person_white_hair:"unicode/1f9d1-1f9b3.png?v8",person_with_probing_cane:"unicode/1f9d1-1f9af.png?v8",person_with_turban:"unicode/1f473.png?v8",person_with_veil:"unicode/1f470.png?v8",peru:"unicode/1f1f5-1f1ea.png?v8",petri_dish:"unicode/1f9eb.png?v8",philippines:"unicode/1f1f5-1f1ed.png?v8",phone:"unicode/260e.png?v8",pick:"unicode/26cf.png?v8",pickup_truck:"unicode/1f6fb.png?v8",pie:"unicode/1f967.png?v8",pig:"unicode/1f437.png?v8",pig2:"unicode/1f416.png?v8",pig_nose:"unicode/1f43d.png?v8",pill:"unicode/1f48a.png?v8",pilot:"unicode/1f9d1-2708.png?v8",pinata:"unicode/1fa85.png?v8",pinched_fingers:"unicode/1f90c.png?v8",pinching_hand:"unicode/1f90f.png?v8",pineapple:"unicode/1f34d.png?v8",ping_pong:"unicode/1f3d3.png?v8",pirate_flag:"unicode/1f3f4-2620.png?v8",pisces:"unicode/2653.png?v8",pitcairn_islands:"unicode/1f1f5-1f1f3.png?v8",pizza:"unicode/1f355.png?v8",placard:"unicode/1faa7.png?v8",place_of_worship:"unicode/1f6d0.png?v8",plate_with_cutlery:"unicode/1f37d.png?v8",play_or_pause_button:"unicode/23ef.png?v8",pleading_face:"unicode/1f97a.png?v8",plunger:"unicode/1faa0.png?v8",point_down:"unicode/1f447.png?v8",point_left:"unicode/1f448.png?v8",point_right:"unicode/1f449.png?v8",point_up:"unicode/261d.png?v8",point_up_2:"unicode/1f446.png?v8",poland:"unicode/1f1f5-1f1f1.png?v8",polar_bear:"unicode/1f43b-2744.png?v8",police_car:"unicode/1f693.png?v8",police_officer:"unicode/1f46e.png?v8",policeman:"unicode/1f46e-2642.png?v8",policewoman:"unicode/1f46e-2640.png?v8",poodle:"unicode/1f429.png?v8",poop:"unicode/1f4a9.png?v8",popcorn:"unicode/1f37f.png?v8",portugal:"unicode/1f1f5-1f1f9.png?v8",post_office:"unicode/1f3e3.png?v8",postal_horn:"unicode/1f4ef.png?v8",postbox:"unicode/1f4ee.png?v8",potable_water:"unicode/1f6b0.png?v8",potato:"unicode/1f954.png?v8",potted_plant:"unicode/1fab4.png?v8",pouch:"unicode/1f45d.png?v8",poultry_leg:"unicode/1f357.png?v8",pound:"unicode/1f4b7.png?v8",pout:"unicode/1f621.png?v8",pouting_cat:"unicode/1f63e.png?v8",pouting_face:"unicode/1f64e.png?v8",pouting_man:"unicode/1f64e-2642.png?v8",pouting_woman:"unicode/1f64e-2640.png?v8",pray:"unicode/1f64f.png?v8",prayer_beads:"unicode/1f4ff.png?v8",pregnant_woman:"unicode/1f930.png?v8",pretzel:"unicode/1f968.png?v8",previous_track_button:"unicode/23ee.png?v8",prince:"unicode/1f934.png?v8",princess:"unicode/1f478.png?v8",printer:"unicode/1f5a8.png?v8",probing_cane:"unicode/1f9af.png?v8",puerto_rico:"unicode/1f1f5-1f1f7.png?v8",punch:"unicode/1f44a.png?v8",purple_circle:"unicode/1f7e3.png?v8",purple_heart:"unicode/1f49c.png?v8",purple_square:"unicode/1f7ea.png?v8",purse:"unicode/1f45b.png?v8",pushpin:"unicode/1f4cc.png?v8",put_litter_in_its_place:"unicode/1f6ae.png?v8",qatar:"unicode/1f1f6-1f1e6.png?v8",question:"unicode/2753.png?v8",rabbit:"unicode/1f430.png?v8",rabbit2:"unicode/1f407.png?v8",raccoon:"unicode/1f99d.png?v8",racehorse:"unicode/1f40e.png?v8",racing_car:"unicode/1f3ce.png?v8",radio:"unicode/1f4fb.png?v8",radio_button:"unicode/1f518.png?v8",radioactive:"unicode/2622.png?v8",rage:"unicode/1f621.png?v8",rage1:"rage1.png?v8",rage2:"rage2.png?v8",rage3:"rage3.png?v8",rage4:"rage4.png?v8",railway_car:"unicode/1f683.png?v8",railway_track:"unicode/1f6e4.png?v8",rainbow:"unicode/1f308.png?v8",rainbow_flag:"unicode/1f3f3-1f308.png?v8",raised_back_of_hand:"unicode/1f91a.png?v8",raised_eyebrow:"unicode/1f928.png?v8",raised_hand:"unicode/270b.png?v8",raised_hand_with_fingers_splayed:"unicode/1f590.png?v8",raised_hands:"unicode/1f64c.png?v8",raising_hand:"unicode/1f64b.png?v8",raising_hand_man:"unicode/1f64b-2642.png?v8",raising_hand_woman:"unicode/1f64b-2640.png?v8",ram:"unicode/1f40f.png?v8",ramen:"unicode/1f35c.png?v8",rat:"unicode/1f400.png?v8",razor:"unicode/1fa92.png?v8",receipt:"unicode/1f9fe.png?v8",record_button:"unicode/23fa.png?v8",recycle:"unicode/267b.png?v8",red_car:"unicode/1f697.png?v8",red_circle:"unicode/1f534.png?v8",red_envelope:"unicode/1f9e7.png?v8",red_haired_man:"unicode/1f468-1f9b0.png?v8",red_haired_woman:"unicode/1f469-1f9b0.png?v8",red_square:"unicode/1f7e5.png?v8",registered:"unicode/00ae.png?v8",relaxed:"unicode/263a.png?v8",relieved:"unicode/1f60c.png?v8",reminder_ribbon:"unicode/1f397.png?v8",repeat:"unicode/1f501.png?v8",repeat_one:"unicode/1f502.png?v8",rescue_worker_helmet:"unicode/26d1.png?v8",restroom:"unicode/1f6bb.png?v8",reunion:"unicode/1f1f7-1f1ea.png?v8",revolving_hearts:"unicode/1f49e.png?v8",rewind:"unicode/23ea.png?v8",rhinoceros:"unicode/1f98f.png?v8",ribbon:"unicode/1f380.png?v8",rice:"unicode/1f35a.png?v8",rice_ball:"unicode/1f359.png?v8",rice_cracker:"unicode/1f358.png?v8",rice_scene:"unicode/1f391.png?v8",right_anger_bubble:"unicode/1f5ef.png?v8",ring:"unicode/1f48d.png?v8",ringed_planet:"unicode/1fa90.png?v8",robot:"unicode/1f916.png?v8",rock:"unicode/1faa8.png?v8",rocket:"unicode/1f680.png?v8",rofl:"unicode/1f923.png?v8",roll_eyes:"unicode/1f644.png?v8",roll_of_paper:"unicode/1f9fb.png?v8",roller_coaster:"unicode/1f3a2.png?v8",roller_skate:"unicode/1f6fc.png?v8",romania:"unicode/1f1f7-1f1f4.png?v8",rooster:"unicode/1f413.png?v8",rose:"unicode/1f339.png?v8",rosette:"unicode/1f3f5.png?v8",rotating_light:"unicode/1f6a8.png?v8",round_pushpin:"unicode/1f4cd.png?v8",rowboat:"unicode/1f6a3.png?v8",rowing_man:"unicode/1f6a3-2642.png?v8",rowing_woman:"unicode/1f6a3-2640.png?v8",ru:"unicode/1f1f7-1f1fa.png?v8",rugby_football:"unicode/1f3c9.png?v8",runner:"unicode/1f3c3.png?v8",running:"unicode/1f3c3.png?v8",running_man:"unicode/1f3c3-2642.png?v8",running_shirt_with_sash:"unicode/1f3bd.png?v8",running_woman:"unicode/1f3c3-2640.png?v8",rwanda:"unicode/1f1f7-1f1fc.png?v8",sa:"unicode/1f202.png?v8",safety_pin:"unicode/1f9f7.png?v8",safety_vest:"unicode/1f9ba.png?v8",sagittarius:"unicode/2650.png?v8",sailboat:"unicode/26f5.png?v8",sake:"unicode/1f376.png?v8",salt:"unicode/1f9c2.png?v8",samoa:"unicode/1f1fc-1f1f8.png?v8",san_marino:"unicode/1f1f8-1f1f2.png?v8",sandal:"unicode/1f461.png?v8",sandwich:"unicode/1f96a.png?v8",santa:"unicode/1f385.png?v8",sao_tome_principe:"unicode/1f1f8-1f1f9.png?v8",sari:"unicode/1f97b.png?v8",sassy_man:"unicode/1f481-2642.png?v8",sassy_woman:"unicode/1f481-2640.png?v8",satellite:"unicode/1f4e1.png?v8",satisfied:"unicode/1f606.png?v8",saudi_arabia:"unicode/1f1f8-1f1e6.png?v8",sauna_man:"unicode/1f9d6-2642.png?v8",sauna_person:"unicode/1f9d6.png?v8",sauna_woman:"unicode/1f9d6-2640.png?v8",sauropod:"unicode/1f995.png?v8",saxophone:"unicode/1f3b7.png?v8",scarf:"unicode/1f9e3.png?v8",school:"unicode/1f3eb.png?v8",school_satchel:"unicode/1f392.png?v8",scientist:"unicode/1f9d1-1f52c.png?v8",scissors:"unicode/2702.png?v8",scorpion:"unicode/1f982.png?v8",scorpius:"unicode/264f.png?v8",scotland:"unicode/1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png?v8",scream:"unicode/1f631.png?v8",scream_cat:"unicode/1f640.png?v8",screwdriver:"unicode/1fa9b.png?v8",scroll:"unicode/1f4dc.png?v8",seal:"unicode/1f9ad.png?v8",seat:"unicode/1f4ba.png?v8",secret:"unicode/3299.png?v8",see_no_evil:"unicode/1f648.png?v8",seedling:"unicode/1f331.png?v8",selfie:"unicode/1f933.png?v8",senegal:"unicode/1f1f8-1f1f3.png?v8",serbia:"unicode/1f1f7-1f1f8.png?v8",service_dog:"unicode/1f415-1f9ba.png?v8",seven:"unicode/0037-20e3.png?v8",sewing_needle:"unicode/1faa1.png?v8",seychelles:"unicode/1f1f8-1f1e8.png?v8",shallow_pan_of_food:"unicode/1f958.png?v8",shamrock:"unicode/2618.png?v8",shark:"unicode/1f988.png?v8",shaved_ice:"unicode/1f367.png?v8",sheep:"unicode/1f411.png?v8",shell:"unicode/1f41a.png?v8",shield:"unicode/1f6e1.png?v8",shinto_shrine:"unicode/26e9.png?v8",ship:"unicode/1f6a2.png?v8",shipit:"shipit.png?v8",shirt:"unicode/1f455.png?v8",shit:"unicode/1f4a9.png?v8",shoe:"unicode/1f45e.png?v8",shopping:"unicode/1f6cd.png?v8",shopping_cart:"unicode/1f6d2.png?v8",shorts:"unicode/1fa73.png?v8",shower:"unicode/1f6bf.png?v8",shrimp:"unicode/1f990.png?v8",shrug:"unicode/1f937.png?v8",shushing_face:"unicode/1f92b.png?v8",sierra_leone:"unicode/1f1f8-1f1f1.png?v8",signal_strength:"unicode/1f4f6.png?v8",singapore:"unicode/1f1f8-1f1ec.png?v8",singer:"unicode/1f9d1-1f3a4.png?v8",sint_maarten:"unicode/1f1f8-1f1fd.png?v8",six:"unicode/0036-20e3.png?v8",six_pointed_star:"unicode/1f52f.png?v8",skateboard:"unicode/1f6f9.png?v8",ski:"unicode/1f3bf.png?v8",skier:"unicode/26f7.png?v8",skull:"unicode/1f480.png?v8",skull_and_crossbones:"unicode/2620.png?v8",skunk:"unicode/1f9a8.png?v8",sled:"unicode/1f6f7.png?v8",sleeping:"unicode/1f634.png?v8",sleeping_bed:"unicode/1f6cc.png?v8",sleepy:"unicode/1f62a.png?v8",slightly_frowning_face:"unicode/1f641.png?v8",slightly_smiling_face:"unicode/1f642.png?v8",slot_machine:"unicode/1f3b0.png?v8",sloth:"unicode/1f9a5.png?v8",slovakia:"unicode/1f1f8-1f1f0.png?v8",slovenia:"unicode/1f1f8-1f1ee.png?v8",small_airplane:"unicode/1f6e9.png?v8",small_blue_diamond:"unicode/1f539.png?v8",small_orange_diamond:"unicode/1f538.png?v8",small_red_triangle:"unicode/1f53a.png?v8",small_red_triangle_down:"unicode/1f53b.png?v8",smile:"unicode/1f604.png?v8",smile_cat:"unicode/1f638.png?v8",smiley:"unicode/1f603.png?v8",smiley_cat:"unicode/1f63a.png?v8",smiling_face_with_tear:"unicode/1f972.png?v8",smiling_face_with_three_hearts:"unicode/1f970.png?v8",smiling_imp:"unicode/1f608.png?v8",smirk:"unicode/1f60f.png?v8",smirk_cat:"unicode/1f63c.png?v8",smoking:"unicode/1f6ac.png?v8",snail:"unicode/1f40c.png?v8",snake:"unicode/1f40d.png?v8",sneezing_face:"unicode/1f927.png?v8",snowboarder:"unicode/1f3c2.png?v8",snowflake:"unicode/2744.png?v8",snowman:"unicode/26c4.png?v8",snowman_with_snow:"unicode/2603.png?v8",soap:"unicode/1f9fc.png?v8",sob:"unicode/1f62d.png?v8",soccer:"unicode/26bd.png?v8",socks:"unicode/1f9e6.png?v8",softball:"unicode/1f94e.png?v8",solomon_islands:"unicode/1f1f8-1f1e7.png?v8",somalia:"unicode/1f1f8-1f1f4.png?v8",soon:"unicode/1f51c.png?v8",sos:"unicode/1f198.png?v8",sound:"unicode/1f509.png?v8",south_africa:"unicode/1f1ff-1f1e6.png?v8",south_georgia_south_sandwich_islands:"unicode/1f1ec-1f1f8.png?v8",south_sudan:"unicode/1f1f8-1f1f8.png?v8",space_invader:"unicode/1f47e.png?v8",spades:"unicode/2660.png?v8",spaghetti:"unicode/1f35d.png?v8",sparkle:"unicode/2747.png?v8",sparkler:"unicode/1f387.png?v8",sparkles:"unicode/2728.png?v8",sparkling_heart:"unicode/1f496.png?v8",speak_no_evil:"unicode/1f64a.png?v8",speaker:"unicode/1f508.png?v8",speaking_head:"unicode/1f5e3.png?v8",speech_balloon:"unicode/1f4ac.png?v8",speedboat:"unicode/1f6a4.png?v8",spider:"unicode/1f577.png?v8",spider_web:"unicode/1f578.png?v8",spiral_calendar:"unicode/1f5d3.png?v8",spiral_notepad:"unicode/1f5d2.png?v8",sponge:"unicode/1f9fd.png?v8",spoon:"unicode/1f944.png?v8",squid:"unicode/1f991.png?v8",sri_lanka:"unicode/1f1f1-1f1f0.png?v8",st_barthelemy:"unicode/1f1e7-1f1f1.png?v8",st_helena:"unicode/1f1f8-1f1ed.png?v8",st_kitts_nevis:"unicode/1f1f0-1f1f3.png?v8",st_lucia:"unicode/1f1f1-1f1e8.png?v8",st_martin:"unicode/1f1f2-1f1eb.png?v8",st_pierre_miquelon:"unicode/1f1f5-1f1f2.png?v8",st_vincent_grenadines:"unicode/1f1fb-1f1e8.png?v8",stadium:"unicode/1f3df.png?v8",standing_man:"unicode/1f9cd-2642.png?v8",standing_person:"unicode/1f9cd.png?v8",standing_woman:"unicode/1f9cd-2640.png?v8",star:"unicode/2b50.png?v8",star2:"unicode/1f31f.png?v8",star_and_crescent:"unicode/262a.png?v8",star_of_david:"unicode/2721.png?v8",star_struck:"unicode/1f929.png?v8",stars:"unicode/1f320.png?v8",station:"unicode/1f689.png?v8",statue_of_liberty:"unicode/1f5fd.png?v8",steam_locomotive:"unicode/1f682.png?v8",stethoscope:"unicode/1fa7a.png?v8",stew:"unicode/1f372.png?v8",stop_button:"unicode/23f9.png?v8",stop_sign:"unicode/1f6d1.png?v8",stopwatch:"unicode/23f1.png?v8",straight_ruler:"unicode/1f4cf.png?v8",strawberry:"unicode/1f353.png?v8",stuck_out_tongue:"unicode/1f61b.png?v8",stuck_out_tongue_closed_eyes:"unicode/1f61d.png?v8",stuck_out_tongue_winking_eye:"unicode/1f61c.png?v8",student:"unicode/1f9d1-1f393.png?v8",studio_microphone:"unicode/1f399.png?v8",stuffed_flatbread:"unicode/1f959.png?v8",sudan:"unicode/1f1f8-1f1e9.png?v8",sun_behind_large_cloud:"unicode/1f325.png?v8",sun_behind_rain_cloud:"unicode/1f326.png?v8",sun_behind_small_cloud:"unicode/1f324.png?v8",sun_with_face:"unicode/1f31e.png?v8",sunflower:"unicode/1f33b.png?v8",sunglasses:"unicode/1f60e.png?v8",sunny:"unicode/2600.png?v8",sunrise:"unicode/1f305.png?v8",sunrise_over_mountains:"unicode/1f304.png?v8",superhero:"unicode/1f9b8.png?v8",superhero_man:"unicode/1f9b8-2642.png?v8",superhero_woman:"unicode/1f9b8-2640.png?v8",supervillain:"unicode/1f9b9.png?v8",supervillain_man:"unicode/1f9b9-2642.png?v8",supervillain_woman:"unicode/1f9b9-2640.png?v8",surfer:"unicode/1f3c4.png?v8",surfing_man:"unicode/1f3c4-2642.png?v8",surfing_woman:"unicode/1f3c4-2640.png?v8",suriname:"unicode/1f1f8-1f1f7.png?v8",sushi:"unicode/1f363.png?v8",suspect:"suspect.png?v8",suspension_railway:"unicode/1f69f.png?v8",svalbard_jan_mayen:"unicode/1f1f8-1f1ef.png?v8",swan:"unicode/1f9a2.png?v8",swaziland:"unicode/1f1f8-1f1ff.png?v8",sweat:"unicode/1f613.png?v8",sweat_drops:"unicode/1f4a6.png?v8",sweat_smile:"unicode/1f605.png?v8",sweden:"unicode/1f1f8-1f1ea.png?v8",sweet_potato:"unicode/1f360.png?v8",swim_brief:"unicode/1fa72.png?v8",swimmer:"unicode/1f3ca.png?v8",swimming_man:"unicode/1f3ca-2642.png?v8",swimming_woman:"unicode/1f3ca-2640.png?v8",switzerland:"unicode/1f1e8-1f1ed.png?v8",symbols:"unicode/1f523.png?v8",synagogue:"unicode/1f54d.png?v8",syria:"unicode/1f1f8-1f1fe.png?v8",syringe:"unicode/1f489.png?v8","t-rex":"unicode/1f996.png?v8",taco:"unicode/1f32e.png?v8",tada:"unicode/1f389.png?v8",taiwan:"unicode/1f1f9-1f1fc.png?v8",tajikistan:"unicode/1f1f9-1f1ef.png?v8",takeout_box:"unicode/1f961.png?v8",tamale:"unicode/1fad4.png?v8",tanabata_tree:"unicode/1f38b.png?v8",tangerine:"unicode/1f34a.png?v8",tanzania:"unicode/1f1f9-1f1ff.png?v8",taurus:"unicode/2649.png?v8",taxi:"unicode/1f695.png?v8",tea:"unicode/1f375.png?v8",teacher:"unicode/1f9d1-1f3eb.png?v8",teapot:"unicode/1fad6.png?v8",technologist:"unicode/1f9d1-1f4bb.png?v8",teddy_bear:"unicode/1f9f8.png?v8",telephone:"unicode/260e.png?v8",telephone_receiver:"unicode/1f4de.png?v8",telescope:"unicode/1f52d.png?v8",tennis:"unicode/1f3be.png?v8",tent:"unicode/26fa.png?v8",test_tube:"unicode/1f9ea.png?v8",thailand:"unicode/1f1f9-1f1ed.png?v8",thermometer:"unicode/1f321.png?v8",thinking:"unicode/1f914.png?v8",thong_sandal:"unicode/1fa74.png?v8",thought_balloon:"unicode/1f4ad.png?v8",thread:"unicode/1f9f5.png?v8",three:"unicode/0033-20e3.png?v8",thumbsdown:"unicode/1f44e.png?v8",thumbsup:"unicode/1f44d.png?v8",ticket:"unicode/1f3ab.png?v8",tickets:"unicode/1f39f.png?v8",tiger:"unicode/1f42f.png?v8",tiger2:"unicode/1f405.png?v8",timer_clock:"unicode/23f2.png?v8",timor_leste:"unicode/1f1f9-1f1f1.png?v8",tipping_hand_man:"unicode/1f481-2642.png?v8",tipping_hand_person:"unicode/1f481.png?v8",tipping_hand_woman:"unicode/1f481-2640.png?v8",tired_face:"unicode/1f62b.png?v8",tm:"unicode/2122.png?v8",togo:"unicode/1f1f9-1f1ec.png?v8",toilet:"unicode/1f6bd.png?v8",tokelau:"unicode/1f1f9-1f1f0.png?v8",tokyo_tower:"unicode/1f5fc.png?v8",tomato:"unicode/1f345.png?v8",tonga:"unicode/1f1f9-1f1f4.png?v8",tongue:"unicode/1f445.png?v8",toolbox:"unicode/1f9f0.png?v8",tooth:"unicode/1f9b7.png?v8",toothbrush:"unicode/1faa5.png?v8",top:"unicode/1f51d.png?v8",tophat:"unicode/1f3a9.png?v8",tornado:"unicode/1f32a.png?v8",tr:"unicode/1f1f9-1f1f7.png?v8",trackball:"unicode/1f5b2.png?v8",tractor:"unicode/1f69c.png?v8",traffic_light:"unicode/1f6a5.png?v8",train:"unicode/1f68b.png?v8",train2:"unicode/1f686.png?v8",tram:"unicode/1f68a.png?v8",transgender_flag:"unicode/1f3f3-26a7.png?v8",transgender_symbol:"unicode/26a7.png?v8",triangular_flag_on_post:"unicode/1f6a9.png?v8",triangular_ruler:"unicode/1f4d0.png?v8",trident:"unicode/1f531.png?v8",trinidad_tobago:"unicode/1f1f9-1f1f9.png?v8",tristan_da_cunha:"unicode/1f1f9-1f1e6.png?v8",triumph:"unicode/1f624.png?v8",trolleybus:"unicode/1f68e.png?v8",trollface:"trollface.png?v8",trophy:"unicode/1f3c6.png?v8",tropical_drink:"unicode/1f379.png?v8",tropical_fish:"unicode/1f420.png?v8",truck:"unicode/1f69a.png?v8",trumpet:"unicode/1f3ba.png?v8",tshirt:"unicode/1f455.png?v8",tulip:"unicode/1f337.png?v8",tumbler_glass:"unicode/1f943.png?v8",tunisia:"unicode/1f1f9-1f1f3.png?v8",turkey:"unicode/1f983.png?v8",turkmenistan:"unicode/1f1f9-1f1f2.png?v8",turks_caicos_islands:"unicode/1f1f9-1f1e8.png?v8",turtle:"unicode/1f422.png?v8",tuvalu:"unicode/1f1f9-1f1fb.png?v8",tv:"unicode/1f4fa.png?v8",twisted_rightwards_arrows:"unicode/1f500.png?v8",two:"unicode/0032-20e3.png?v8",two_hearts:"unicode/1f495.png?v8",two_men_holding_hands:"unicode/1f46c.png?v8",two_women_holding_hands:"unicode/1f46d.png?v8",u5272:"unicode/1f239.png?v8",u5408:"unicode/1f234.png?v8",u55b6:"unicode/1f23a.png?v8",u6307:"unicode/1f22f.png?v8",u6708:"unicode/1f237.png?v8",u6709:"unicode/1f236.png?v8",u6e80:"unicode/1f235.png?v8",u7121:"unicode/1f21a.png?v8",u7533:"unicode/1f238.png?v8",u7981:"unicode/1f232.png?v8",u7a7a:"unicode/1f233.png?v8",uganda:"unicode/1f1fa-1f1ec.png?v8",uk:"unicode/1f1ec-1f1e7.png?v8",ukraine:"unicode/1f1fa-1f1e6.png?v8",umbrella:"unicode/2614.png?v8",unamused:"unicode/1f612.png?v8",underage:"unicode/1f51e.png?v8",unicorn:"unicode/1f984.png?v8",united_arab_emirates:"unicode/1f1e6-1f1ea.png?v8",united_nations:"unicode/1f1fa-1f1f3.png?v8",unlock:"unicode/1f513.png?v8",up:"unicode/1f199.png?v8",upside_down_face:"unicode/1f643.png?v8",uruguay:"unicode/1f1fa-1f1fe.png?v8",us:"unicode/1f1fa-1f1f8.png?v8",us_outlying_islands:"unicode/1f1fa-1f1f2.png?v8",us_virgin_islands:"unicode/1f1fb-1f1ee.png?v8",uzbekistan:"unicode/1f1fa-1f1ff.png?v8",v:"unicode/270c.png?v8",vampire:"unicode/1f9db.png?v8",vampire_man:"unicode/1f9db-2642.png?v8",vampire_woman:"unicode/1f9db-2640.png?v8",vanuatu:"unicode/1f1fb-1f1fa.png?v8",vatican_city:"unicode/1f1fb-1f1e6.png?v8",venezuela:"unicode/1f1fb-1f1ea.png?v8",vertical_traffic_light:"unicode/1f6a6.png?v8",vhs:"unicode/1f4fc.png?v8",vibration_mode:"unicode/1f4f3.png?v8",video_camera:"unicode/1f4f9.png?v8",video_game:"unicode/1f3ae.png?v8",vietnam:"unicode/1f1fb-1f1f3.png?v8",violin:"unicode/1f3bb.png?v8",virgo:"unicode/264d.png?v8",volcano:"unicode/1f30b.png?v8",volleyball:"unicode/1f3d0.png?v8",vomiting_face:"unicode/1f92e.png?v8",vs:"unicode/1f19a.png?v8",vulcan_salute:"unicode/1f596.png?v8",waffle:"unicode/1f9c7.png?v8",wales:"unicode/1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png?v8",walking:"unicode/1f6b6.png?v8",walking_man:"unicode/1f6b6-2642.png?v8",walking_woman:"unicode/1f6b6-2640.png?v8",wallis_futuna:"unicode/1f1fc-1f1eb.png?v8",waning_crescent_moon:"unicode/1f318.png?v8",waning_gibbous_moon:"unicode/1f316.png?v8",warning:"unicode/26a0.png?v8",wastebasket:"unicode/1f5d1.png?v8",watch:"unicode/231a.png?v8",water_buffalo:"unicode/1f403.png?v8",water_polo:"unicode/1f93d.png?v8",watermelon:"unicode/1f349.png?v8",wave:"unicode/1f44b.png?v8",wavy_dash:"unicode/3030.png?v8",waxing_crescent_moon:"unicode/1f312.png?v8",waxing_gibbous_moon:"unicode/1f314.png?v8",wc:"unicode/1f6be.png?v8",weary:"unicode/1f629.png?v8",wedding:"unicode/1f492.png?v8",weight_lifting:"unicode/1f3cb.png?v8",weight_lifting_man:"unicode/1f3cb-2642.png?v8",weight_lifting_woman:"unicode/1f3cb-2640.png?v8",western_sahara:"unicode/1f1ea-1f1ed.png?v8",whale:"unicode/1f433.png?v8",whale2:"unicode/1f40b.png?v8",wheel_of_dharma:"unicode/2638.png?v8",wheelchair:"unicode/267f.png?v8",white_check_mark:"unicode/2705.png?v8",white_circle:"unicode/26aa.png?v8",white_flag:"unicode/1f3f3.png?v8",white_flower:"unicode/1f4ae.png?v8",white_haired_man:"unicode/1f468-1f9b3.png?v8",white_haired_woman:"unicode/1f469-1f9b3.png?v8",white_heart:"unicode/1f90d.png?v8",white_large_square:"unicode/2b1c.png?v8",white_medium_small_square:"unicode/25fd.png?v8",white_medium_square:"unicode/25fb.png?v8",white_small_square:"unicode/25ab.png?v8",white_square_button:"unicode/1f533.png?v8",wilted_flower:"unicode/1f940.png?v8",wind_chime:"unicode/1f390.png?v8",wind_face:"unicode/1f32c.png?v8",window:"unicode/1fa9f.png?v8",wine_glass:"unicode/1f377.png?v8",wink:"unicode/1f609.png?v8",wolf:"unicode/1f43a.png?v8",woman:"unicode/1f469.png?v8",woman_artist:"unicode/1f469-1f3a8.png?v8",woman_astronaut:"unicode/1f469-1f680.png?v8",woman_beard:"unicode/1f9d4-2640.png?v8",woman_cartwheeling:"unicode/1f938-2640.png?v8",woman_cook:"unicode/1f469-1f373.png?v8",woman_dancing:"unicode/1f483.png?v8",woman_facepalming:"unicode/1f926-2640.png?v8",woman_factory_worker:"unicode/1f469-1f3ed.png?v8",woman_farmer:"unicode/1f469-1f33e.png?v8",woman_feeding_baby:"unicode/1f469-1f37c.png?v8",woman_firefighter:"unicode/1f469-1f692.png?v8",woman_health_worker:"unicode/1f469-2695.png?v8",woman_in_manual_wheelchair:"unicode/1f469-1f9bd.png?v8",woman_in_motorized_wheelchair:"unicode/1f469-1f9bc.png?v8",woman_in_tuxedo:"unicode/1f935-2640.png?v8",woman_judge:"unicode/1f469-2696.png?v8",woman_juggling:"unicode/1f939-2640.png?v8",woman_mechanic:"unicode/1f469-1f527.png?v8",woman_office_worker:"unicode/1f469-1f4bc.png?v8",woman_pilot:"unicode/1f469-2708.png?v8",woman_playing_handball:"unicode/1f93e-2640.png?v8",woman_playing_water_polo:"unicode/1f93d-2640.png?v8",woman_scientist:"unicode/1f469-1f52c.png?v8",woman_shrugging:"unicode/1f937-2640.png?v8",woman_singer:"unicode/1f469-1f3a4.png?v8",woman_student:"unicode/1f469-1f393.png?v8",woman_teacher:"unicode/1f469-1f3eb.png?v8",woman_technologist:"unicode/1f469-1f4bb.png?v8",woman_with_headscarf:"unicode/1f9d5.png?v8",woman_with_probing_cane:"unicode/1f469-1f9af.png?v8",woman_with_turban:"unicode/1f473-2640.png?v8",woman_with_veil:"unicode/1f470-2640.png?v8",womans_clothes:"unicode/1f45a.png?v8",womans_hat:"unicode/1f452.png?v8",women_wrestling:"unicode/1f93c-2640.png?v8",womens:"unicode/1f6ba.png?v8",wood:"unicode/1fab5.png?v8",woozy_face:"unicode/1f974.png?v8",world_map:"unicode/1f5fa.png?v8",worm:"unicode/1fab1.png?v8",worried:"unicode/1f61f.png?v8",wrench:"unicode/1f527.png?v8",wrestling:"unicode/1f93c.png?v8",writing_hand:"unicode/270d.png?v8",x:"unicode/274c.png?v8",yarn:"unicode/1f9f6.png?v8",yawning_face:"unicode/1f971.png?v8",yellow_circle:"unicode/1f7e1.png?v8",yellow_heart:"unicode/1f49b.png?v8",yellow_square:"unicode/1f7e8.png?v8",yemen:"unicode/1f1fe-1f1ea.png?v8",yen:"unicode/1f4b4.png?v8",yin_yang:"unicode/262f.png?v8",yo_yo:"unicode/1fa80.png?v8",yum:"unicode/1f60b.png?v8",zambia:"unicode/1f1ff-1f1f2.png?v8",zany_face:"unicode/1f92a.png?v8",zap:"unicode/26a1.png?v8",zebra:"unicode/1f993.png?v8",zero:"unicode/0030-20e3.png?v8",zimbabwe:"unicode/1f1ff-1f1fc.png?v8",zipper_mouth_face:"unicode/1f910.png?v8",zombie:"unicode/1f9df.png?v8",zombie_man:"unicode/1f9df-2642.png?v8",zombie_woman:"unicode/1f9df-2640.png?v8",zzz:"unicode/1f4a4.png?v8"}};function jn(e,t){return e.replace(/<(code|pre|script|template)[^>]*?>[\s\S]+?<\/(code|pre|script|template)>/g,function(e){return e.replace(/:/g,"__colon__")}).replace(//g,function(e){return e.replace(/:/g,"__colon__")}).replace(/([a-z]{2,}:)?\/\/[^\s'">)]+/gi,function(e){return e.replace(/:/g,"__colon__")}).replace(/:([a-z0-9_\-+]+?):/g,function(e,n){return i=e,o=n,e=t,n=Rn.data[o],i,i=n?e&&/unicode/.test(n)?''+n.replace("unicode/","").replace(/\.png.*/,"").split("-").map(function(e){return"&#x"+e+";"}).join("‍").concat("︎")+"":''+o+'':i;var i,o}).replace(/__colon__/g,":")}function On(e){var o={};return{str:e=(e=void 0===e?"":e)&&e.replace(/^('|")/,"").replace(/('|")$/,"").replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g,function(e,n,i){return-1===n.indexOf(":")?(o[n]=i&&i.replace(/"/g,"")||!0,""):e}).trim(),config:o}}function Ln(e){return(e=void 0===e?"":e).replace(/(<\/?a.*?>)/gi,"")}var qn,Pn=be(function(e){var u,f,p,d,n,g=function(u){var i=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,n=0,e={},T={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof C?new C(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=r.reach);m+=_.value.length,_=_.next){var b=_.value;if(i.length>n.length)return;if(!(b instanceof C)){var k,w=1;if(l){if(!(k=R(h,m,n,s))||k.index>=n.length)break;var y=k.index,x=k.index+k[0].length,S=m;for(S+=_.value.length;S<=y;)_=_.next,S+=_.value.length;if(S-=_.value.length,m=S,_.value instanceof C)continue;for(var A=_;A!==i.tail&&(Sr.reach&&(r.reach=E);b=_.prev;z&&(b=j(i,b,z),m+=z.length),O(i,b,w);$=new C(c,g?T.tokenize($,g):$,v,$);_=j(i,b,$),F&&j(i,_,F),1r.reach&&(r.reach=E.reach))}}}}}(e,t,n,t.head,0),function(e){var n=[],i=e.head.next;for(;i!==e.tail;)n.push(i.value),i=i.next;return n}(t)},hooks:{all:{},add:function(e,n){var i=T.hooks.all;i[e]=i[e]||[],i[e].push(n)},run:function(e,n){var i=T.hooks.all[e];if(i&&i.length)for(var o,t=0;o=i[t++];)o(n)}},Token:C};function C(e,n,i,o){this.type=e,this.content=n,this.alias=i,this.length=0|(o||"").length}function R(e,n,i,o){e.lastIndex=n;i=e.exec(i);return i&&o&&i[1]&&(o=i[1].length,i.index+=o,i[0]=i[0].slice(o)),i}function a(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function j(e,n,i){var o=n.next,i={value:i,prev:n,next:o};return n.next=i,o.prev=i,e.length++,i}function O(e,n,i){for(var o=n.next,t=0;t"+t.content+""},!u.document)return u.addEventListener&&(T.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),i=n.language,e=n.code,n=n.immediateClose;u.postMessage(T.highlight(e,T.languages[i],i)),n&&u.close()},!1)),T;var o=T.util.currentScript();function t(){T.manual||T.highlightAll()}return o&&(T.filename=o.src,o.hasAttribute("data-manual")&&(T.manual=!0)),T.manual||("loading"===(e=document.readyState)||"interactive"===e&&o&&o.defer?document.addEventListener("DOMContentLoaded",t):window.requestAnimationFrame?window.requestAnimationFrame(t):window.setTimeout(t,16)),T}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{});e.exports&&(e.exports=g),void 0!==me&&(me.Prism=g),g.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},g.languages.markup.tag.inside["attr-value"].inside.entity=g.languages.markup.entity,g.languages.markup.doctype.inside["internal-subset"].inside=g.languages.markup,g.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(g.languages.markup.tag,"addInlined",{value:function(e,n){var i={};i["language-"+n]={pattern:/(^$)/i,lookbehind:!0,inside:g.languages[n]},i.cdata=/^$/i;i={"included-cdata":{pattern://i,inside:i}};i["language-"+n]={pattern:/[\s\S]+/,inside:g.languages[n]};n={};n[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,function(){return e}),"i"),lookbehind:!0,greedy:!0,inside:i},g.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(g.languages.markup.tag,"addAttribute",{value:function(e,n){g.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[n,"language-"+n],inside:g.languages[n]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),g.languages.html=g.languages.markup,g.languages.mathml=g.languages.markup,g.languages.svg=g.languages.markup,g.languages.xml=g.languages.extend("markup",{}),g.languages.ssml=g.languages.xml,g.languages.atom=g.languages.xml,g.languages.rss=g.languages.xml,function(e){var n=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-](?:[^;{\s]|\s+(?![\s{]))*(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+n.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+n.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+n.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:n,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;e=e.languages.markup;e&&(e.tag.addInlined("style","css"),e.tag.addAttribute("style","css"))}(g),g.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},g.languages.javascript=g.languages.extend("clike",{"class-name":[g.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),g.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,g.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:g.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:g.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:g.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:g.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:g.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),g.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:g.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),g.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),g.languages.markup&&(g.languages.markup.tag.addInlined("script","javascript"),g.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),g.languages.js=g.languages.javascript,void 0!==g&&"undefined"!=typeof document&&(Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),u={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},d="pre[data-src]:not(["+(f="data-src-status")+'="loaded"]):not(['+f+'="'+(p="loading")+'"])',g.hooks.add("before-highlightall",function(e){e.selector+=", "+d}),g.hooks.add("before-sanity-check",function(e){var t,n,i,o,a,r,c=e.element;c.matches(d)&&(e.code="",c.setAttribute(f,p),(t=c.appendChild(document.createElement("CODE"))).textContent="Loading…",i=c.getAttribute("data-src"),"none"===(e=e.language)&&(n=(/\.(\w+)$/.exec(i)||[,"none"])[1],e=u[n]||n),g.util.setLanguage(t,e),g.util.setLanguage(c,e),(n=g.plugins.autoloader)&&n.loadLanguages(e),i=i,o=function(e){c.setAttribute(f,"loaded");var n,i,o=function(e){if(i=/^\s*(\d+)\s*(?:(,)\s*(?:(\d+)\s*)?)?$/.exec(e||"")){var n=Number(i[1]),e=i[2],i=i[3];return e?i?[n,Number(i)]:[n,void 0]:[n,n]}}(c.getAttribute("data-range"));o&&(n=e.split(/\r\n?|\n/g),i=o[0],o=null==o[1]?n.length:o[1],i<0&&(i+=n.length),i=Math.max(0,Math.min(i-1,n.length)),o<0&&(o+=n.length),o=Math.max(0,Math.min(o,n.length)),e=n.slice(i,o).join("\n"),c.hasAttribute("data-start")||c.setAttribute("data-start",String(i+1))),t.textContent=e,g.highlightElement(t)},a=function(e){c.setAttribute(f,"failed"),t.textContent=e},(r=new XMLHttpRequest).open("GET",i,!0),r.onreadystatechange=function(){4==r.readyState&&(r.status<400&&r.responseText?o(r.responseText):400<=r.status?a("✖ Error "+r.status+" while fetching file: "+r.statusText):a("✖ Error: File does not exist or is empty"))},r.send(null))}),n=!(g.plugins.fileHighlight={highlight:function(e){for(var n,i=(e||document).querySelectorAll(d),o=0;n=i[o++];)g.highlightElement(n)}}),g.fileHighlight=function(){n||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),n=!0),g.plugins.fileHighlight.highlight.apply(this,arguments)})});function Mn(e,n){return"___"+e.toUpperCase()+n+"___"}qn=Prism,Object.defineProperties(qn.languages["markup-templating"]={},{buildPlaceholders:{value:function(o,t,e,a){var r;o.language===t&&(r=o.tokenStack=[],o.code=o.code.replace(e,function(e){if("function"==typeof a&&!a(e))return e;for(var n,i=r.length;-1!==o.code.indexOf(n=Mn(t,i));)++i;return r[i]=e,n}),o.grammar=qn.languages.markup)}},tokenizePlaceholders:{value:function(f,p){var d,g;f.language===p&&f.tokenStack&&(f.grammar=qn.languages[p],d=0,g=Object.keys(f.tokenStack),function e(n){for(var i=0;i=g.length);i++){var o,t,a,r,c,u=n[i];"string"==typeof u||u.content&&"string"==typeof u.content?(t=g[d],a=f.tokenStack[t],o="string"==typeof u?u:u.content,c=Mn(p,t),-1<(r=o.indexOf(c))&&(++d,t=o.substring(0,r),a=new qn.Token(p,qn.tokenize(a,f.grammar),"language-"+p,a),r=o.substring(r+c.length),c=[],t&&c.push.apply(c,e([t])),c.push(a),r&&c.push.apply(c,e([r])),"string"==typeof u?n.splice.apply(n,[i,1].concat(c)):u.content=c)):u.content&&e(u.content)}return n}(f.tokens))}}});function In(t,e){var a=this;this.config=t,this.router=e,this.cacheTree={},this.toc=[],this.cacheTOC={},this.linkTarget=t.externalLinkTarget||"_blank",this.linkRel="_blank"===this.linkTarget?t.externalLinkRel||"noopener":"",this.contentBase=e.getBasePath();var n=this._initRenderer();this.heading=n.heading;var r=o(e=t.markdown||{})?e(Sn,n):(Sn.setOptions(m(e,{renderer:m(n,e.renderer)})),Sn);this._marked=r,this.compile=function(i){var o=!0,e=c(function(e){o=!1;var n="";return i&&(n=f(i)?r(i):r.parser(i),n=t.noEmoji?n:jn(n,t.nativeEmoji),Cn.clear(),n)})(i),n=a.router.parse().file;return o?a.toc=a.cacheTOC[n]:a.cacheTOC[n]=[].concat(a.toc),e}}var Nn={},Hn={markdown:function(e){return{url:e}},mermaid:function(e){return{url:e}},iframe:function(e,n){return{html:'"}},video:function(e,n){return{html:'"}},audio:function(e,n){return{html:'"}},code:function(e,n){var i=e.match(/\.(\w+)$/);return{url:e,lang:i="md"===(i=n||i&&i[1])?"markdown":i}}};In.prototype.compileEmbed=function(e,n){var i,o,t=On(n),a=t.str,t=t.config;if(n=a,t.include)return T(e)||(e=q(this.contentBase,R(this.router.getCurrentPath()),e)),t.type&&(o=Hn[t.type])?(i=o.call(this,e,n)).type=t.type:(o="code",/\.(md|markdown)/.test(e)?o="markdown":/\.mmd/.test(e)?o="mermaid":/\.html?/.test(e)?o="iframe":/\.(mp4|ogg)/.test(e)?o="video":/\.mp3/.test(e)&&(o="audio"),(i=Hn[o].call(this,e,n)).type=o),i.fragment=t.fragment,i},In.prototype._matchNotCompileLink=function(e){for(var n=this.config.noCompileLinks||[],i=0;i/g.test(o)&&(o=o.replace("\x3c!-- {docsify-ignore} --\x3e",""),e.title=Ln(o),e.ignoreSubHeading=!0),/{docsify-ignore}/g.test(o)&&(o=o.replace("{docsify-ignore}",""),e.title=Ln(o),e.ignoreSubHeading=!0),//g.test(o)&&(o=o.replace("\x3c!-- {docsify-ignore-all} --\x3e",""),e.title=Ln(o),e.ignoreAllSubs=!0),/{docsify-ignore-all}/g.test(o)&&(o=o.replace("{docsify-ignore-all}",""),e.title=Ln(o),e.ignoreAllSubs=!0);i=Cn(t.id||o),t=a.toURL(a.getCurrentPath(),{id:i});return e.slug=t,g.toc.push(e),"'+o+""},t.code={renderer:e}.renderer.code=function(e,n){var i=Pn.languages[n=void 0===n?"markup":n]||Pn.languages.markup;return'
    '+Pn.highlight(e.replace(/@DOCSIFY_QM@/g,"`"),i,n)+"
    "},t.link=(i=(n={renderer:e,router:a,linkTarget:n,linkRel:i,compilerClass:g}).renderer,c=n.router,u=n.linkTarget,n.linkRel,f=n.compilerClass,i.link=function(e,n,i){var o=[],t=On(n=void 0===n?"":n),a=t.str,t=t.config;return u=t.target||u,r="_blank"===u?f.config.externalLinkRel||"noopener":"",n=a,T(e)||f._matchNotCompileLink(e)||t.ignore?(T(e)||"./"!==e.slice(0,2)||(e=document.URL.replace(/\/(?!.*\/).*/,"/").replace("#/./","")+e),o.push(0===e.indexOf("mailto:")?"":'target="'+u+'"'),o.push(0!==e.indexOf("mailto:")&&""!==r?' rel="'+r+'"':"")):(e===f.config.homepage&&(e="README"),e=c.toURL(e,null,c.getCurrentPath())),t.crossorgin&&"_self"===u&&"history"===f.config.routerMode&&-1===f.config.crossOriginLinks.indexOf(e)&&f.config.crossOriginLinks.push(e),t.disabled&&(o.push("disabled"),e="javascript:void(0)"),t.class&&o.push('class="'+t.class+'"'),t.id&&o.push('id="'+t.id+'"'),n&&o.push('title="'+n+'"'),'"+i+""}),t.paragraph={renderer:e}.renderer.paragraph=function(e){e=/^!>/.test(e)?$n("tip",e):/^\?>/.test(e)?$n("warn",e):"

    "+e+"

    ";return e},t.image=(o=(i={renderer:e,contentBase:o,router:a}).renderer,p=i.contentBase,d=i.router,o.image=function(e,n,i){var o=e,t=[],a=On(n),r=a.str,a=a.config;return n=r,a["no-zoom"]&&t.push("data-no-zoom"),n&&t.push('title="'+n+'"'),a.size&&(n=(r=a.size.split("x"))[0],(r=r[1])?t.push('width="'+n+'" height="'+r+'"'):t.push('width="'+n+'"')),a.class&&t.push('class="'+a.class+'"'),a.id&&t.push('id="'+a.id+'"'),T(e)||(o=q(p,R(d.getCurrentPath()),e)),0":''+i+'"}),t.list={renderer:e}.renderer.list=function(e,n,i){n=n?"ol":"ul";return"<"+n+" "+[/
  • /.test(e.split('class="task-list"')[0])?'class="task-list"':"",i&&1"+e+""},t.listitem={renderer:e}.renderer.listitem=function(e){return/^(]*>)/.test(e)?'
  • ":"
  • "+e+"
  • "},e.origin=t,e},In.prototype.sidebar=function(e,n){var i=this.toc,o=this.router.getCurrentPath(),t="";if(e)t=this.compile(e);else{for(var a=0;a{inner}");this.cacheTree[o]=n}return t},In.prototype.subSidebar=function(e){if(e){var n=this.router.getCurrentPath(),i=this.cacheTree,o=this.toc;o[0]&&o[0].ignoreAllSubs&&o.splice(0),o[0]&&1===o[0].level&&o.shift();for(var t=0;t\n'+e+"\n
    "}]).links={}:(n=[{type:"html",text:e}]).links={}),a({token:t,embedToken:n}),++u>=c&&a({})}}(n);n.embed.url?X(n.embed.url).then(o):o(n.embed.html)}}({compile:i,embedTokens:c,fetch:n},function(e){var n,i=e.embedToken,e=e.token;e?(n=e.index,p.forEach(function(e){n>e.start&&(n+=e.length)}),m(f,i.links),r=r.slice(0,n).concat(i,r.slice(n+1)),p.push({start:n,length:i.length-1})):(Bn[t]=r.concat(),r.links=Bn[t].links=f,o(r))})}function Yn(e,n,i){var o,t,a,r;return n="function"==typeof i?i(n):"string"==typeof i?(a=[],r=0,(o=i).replace(V,function(n,e,i){a.push(o.substring(r,i-1)),r=i+=n.length+1,a.push(t&&t[n]||function(e){return("00"+("string"==typeof Y[n]?e[Y[n]]():Y[n](e))).slice(-n.length)})}),r!==o.length&&a.push(o.substring(r)),function(e){for(var n="",i=0,o=e||new Date;i404 - Not found","Vue"in window)for(var a=0,r=k(".markdown-section > *").filter(n);ascript").filter(function(e){return!/template/.test(e.type)})[0])||(e=e.innerText.trim())&&new Function(e)()),"Vue"in window){var u,f,p=[],d=Object.keys(i.vueComponents||{});2===t&&d.length&&d.forEach(function(e){window.Vue.options.components[e]||window.Vue.component(e,i.vueComponents[e])}),!Un&&i.vueGlobalOptions&&"function"==typeof i.vueGlobalOptions.data&&(Un=i.vueGlobalOptions.data()),p.push.apply(p,Object.keys(i.vueMounts||{}).map(function(e){return[b(o,e),i.vueMounts[e]]}).filter(function(e){var n=e[0];e[1];return n})),(i.vueGlobalOptions||d.length)&&(u=/{{2}[^{}]*}{2}/,f=/<[^>/]+\s([@:]|v-)[\w-:.[\]]+[=>\s]/,p.push.apply(p,k(".markdown-section > *").filter(function(i){return!p.some(function(e){var n=e[0];e[1];return n===i})}).filter(function(e){return e.tagName.toLowerCase()in(i.vueComponents||{})||e.querySelector(d.join(",")||null)||u.test(e.outerHTML)||f.test(e.outerHTML)}).map(function(e){var n=m({},i.vueGlobalOptions||{});return Un&&(n.data=function(){return Un}),[e,n]})));for(var g=0,s=p;g([^<]*?)

    $'))&&("color"===n[2]?o.style.background=n[1]+(n[3]||""):(e=n[1],S(o,"add","has-mask"),T(n[1])||(e=q(this.router.getBasePath(),n[1])),o.style.backgroundImage="url("+e+")",o.style.backgroundSize="cover",o.style.backgroundPosition="center center"),i=i.replace(n[0],"")),this._renderTo(".cover-main",i),K()):S(o,"remove","show")},n.prototype._updateRender=function(){var e,n,i,o;e=this,n=l(".app-name-link"),i=e.config.nameLink,o=e.route.path,n&&(f(e.config.nameLink)?n.setAttribute("href",i):"object"==typeof i&&(e=Object.keys(i).filter(function(e){return-1':"")),e.coverpage&&(f+=(o=", 100%, 85%",'
    \x3c!--cover--\x3e
    ')),e.logo&&(o=/^data:image/.test(e.logo),n=/(?:http[s]?:)?\/\//.test(e.logo),i=/^\./.test(e.logo),o||n||i||(e.logo=q(this.router.getBasePath(),e.logo))),f+=(i=(n=e).name||"","
    "+('
    ')+'
    \x3c!--main--\x3e
    '),this._renderTo(u,f,!0)):this.rendered=!0,e.mergeNavbar&&s?p=b(".sidebar"):(c.classList.add("app-nav"),e.repo||c.classList.add("no-badge")),e.loadNavbar&&y(p,c),e.themeColor&&(v.head.appendChild(w("div","").firstElementChild),a=e.themeColor,window.CSS&&window.CSS.supports&&window.CSS.supports("(--v:red)")||(e=k("style:not(.inserted),link"),[].forEach.call(e,function(e){"STYLE"===e.nodeName?Q(e,a):"LINK"===e.nodeName&&(e=e.getAttribute("href"),/\.css$/.test(e)&&X(e).then(function(e){e=w("style",e);_.appendChild(e),Q(e,a)}))}))),this._updateRender(),S(h,"ready")},n}(function(e){function n(){e.apply(this,arguments)}return e&&(n.__proto__=e),((n.prototype=Object.create(e&&e.prototype)).constructor=n).prototype.routes=function(){return this.config.routes||{}},n.prototype.matchVirtualRoute=function(t){var a=this.routes(),r=Object.keys(a),c=function(){return null};function u(){var e=r.shift();if(!e)return c(null);var n=A(o=(i="^",0===(o=e).indexOf(i)?o:"^"+o),"$")?o:o+"$",i=t.match(n);if(!i)return u();var o=a[e];if("string"==typeof o)return c(o);if("function"!=typeof o)return u();n=o,e=Xn(),o=e[0];return(0,e[1])(function(e){return"string"==typeof e?c(e):!1===e?c(null):u()}),n.length<=2?o(n(t,i)):n(t,i,o)}return{then:function(e){c=e,u()}}},n}(function(i){function e(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];i.apply(this,e),this.route={}}return i&&(e.__proto__=i),((e.prototype=Object.create(i&&i.prototype)).constructor=e).prototype.updateRender=function(){this.router.normalize(),this.route=this.router.parse(),h.setAttribute("data-page",this.route.file)},e.prototype.initRouter=function(){var n=this,e=this.config,e=new("history"===(e.routerMode||"hash")&&t?D:H)(e);this.router=e,this.updateRender(),U=this.route,e.onchange(function(e){n.updateRender(),n._updateRender(),U.path!==n.route.path?(n.$fetch(d,n.$resetEvents.bind(n,e.source)),U=n.route):n.$resetEvents(e.source)})},e}(function(e){function n(){e.apply(this,arguments)}return e&&(n.__proto__=e),((n.prototype=Object.create(e&&e.prototype)).constructor=n).prototype.initLifecycle=function(){var i=this;this._hooks={},this._lifecycle={},["init","mounted","beforeEach","afterEach","doneEach","ready"].forEach(function(e){var n=i._hooks[e]=[];i._lifecycle[e]=function(e){return n.push(e)}})},n.prototype.callHook=function(e,t,a){void 0===a&&(a=d);var r=this._hooks[e],c=this.config.catchPluginErrors,u=function(n){var e=r[n];if(n>=r.length)a(t);else if("function"==typeof e){var i="Docsify plugin error";if(2===e.length)try{e(t,function(e){t=e,u(n+1)})}catch(e){if(!c)throw e;console.error(i,e),u(n+1)}else try{var o=e(t);t=void 0===o?t:o,u(n+1)}catch(e){if(!c)throw e;console.error(i,e),u(n+1)}}else u(n+1)};u(0)},n}(we))))))));function Kn(e,n,i){return Qn&&Qn.abort&&Qn.abort(),Qn=X(e,!0,i)}window.Docsify={util:Me,dom:n,get:X,slugify:Cn,version:"4.13.0"},window.DocsifyCompiler=In,window.marked=Sn,window.Prism=Pn,e(function(e){return new Jn})}(); \ No newline at end of file diff --git a/sa-token-doc/static/donate/donate-fun.js b/sa-token-doc/static/donate/donate-fun.js new file mode 100644 index 000000000..e766fefe9 --- /dev/null +++ b/sa-token-doc/static/donate/donate-fun.js @@ -0,0 +1,185 @@ + +// --------------------- 工具方法 --------------------- + +// 打开 loading +loadingIcon = function(msg) { + layer.closeAll(); // 开始前先把所有弹窗关了 + return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); +}; + +// 隐藏 loading +hideLoadingIcon = function() { + layer.closeAll(); +}; + + +// --------------------- 渲染赞助者名单 --------------------- + +// 返回赞助者名单副本 +function getCopyDonateList() { + var arr = []; + for (var i = 0; i < donateList.length; i++) { + var item = donateList[i]; + // 时间转时间戳,方便排序 + item.dateT = new Date(item.date).getTime(); + // 金额补 .0 + item.moneyS = item.money + ''; + if(item.moneyS.indexOf('.') == -1) { + item.moneyS = item.moneyS + '.0'; + } + arr.push(item); + } + return arr; +} +// 返回赞助者名单副本,根据日期倒叙排列 +function getCopyDonateListByDateSort() { + var arr = getCopyDonateList(); + arr.sort(function(a, b){ + var value = b.dateT - a.dateT; + if(value == 0) { + value = -1; + } + return value; + }) + return arr; +} +// 返回赞助者名单副本,根据赞助金额倒叙排列 +function getCopyDonateListByMoneySort() { + var arr = getCopyDonateList(); + arr.sort(function(a, b){ + var value = b.money - a.money; + if(value == 0) { + value = b.dateT - a.dateT; + } + return value; + }) + return arr; +} +// console.log(getCopyDonateListByMoneySort()); + + +// 赞助配置 +var zzCfg = { + curr: 1, // 当前页 + size: 15, // 页大小 + pageCount: 0, // 页总数 + dataCount: 0, // 数据总数 + sort: 1, // 排序方式(1=按照日期倒叙,2=按照金额倒叙) +} + +// 将赞助者名单渲染到页面上 +function renderDonateTable() { + // 先清空旧数据 + $('.zanzhu-table tbody').empty(); + + // 拼接 tr 字符串 + var trArrStr = ''; + var arr = zzCfg.sort == 1 ? getCopyDonateListByDateSort() : getCopyDonateListByMoneySort(); + + // 按照页参数进行遍历 + let index = (zzCfg.curr - 1) * zzCfg.size; // 起始索引 + let end = index + zzCfg.size; // 结束索引 + if(end > arr.length) { + end = arr.length; + } + zzCfg.pageCount = parseInt(arr.length / zzCfg.size); // 页总数 + if(arr.length % zzCfg.size != 0) { + zzCfg.pageCount++; + } + zzCfg.dataCount = arr.length; // 数据总数 + + // 开始拼接字符串 + for (let i = index; i < end; i++) { + // console.log(item); + let item = arr[i]; + let name = item.name; + if(item.link) { + name = '
    ' + name + '' + } + var trStr = ` + + ${name} + ¥ ${item.moneyS} + ${item.msg} + ${item.date} + + `; + trArrStr += trStr; + } + + // 渲染到 table 里 + $('.zanzhu-table tbody').html(trArrStr); + + // 重置分页信息 + const pageInfo = `第 ${zzCfg.curr}/${zzCfg.pageCount} 页(共${zzCfg.dataCount}位)`; + $('.zz-pageInfo').text(pageInfo); +} +// 带动画的渲染 +function renderDonateTable2() { + // 模拟ajax的延时 + loadingIcon('努力加载中...'); + setTimeout(function() { + hideLoadingIcon(); // 隐藏掉转圈圈 + renderDonateTable(); + }, 300); +} +// renderDonateTable(); + +// 上一页 +function prevPageRDT(){ + if(zzCfg.curr <= 1) { + return layer.msg('达咩,不能再往前了'); + } + zzCfg.curr--; + renderDonateTable2(); +} +// 下一页 +function nextPageRDT(){ + if(zzCfg.curr >= zzCfg.pageCount) { + return layer.msg('嘿,到底了'); + } + zzCfg.curr++; + renderDonateTable2(); +} + +// 绑定事件:切换排序 +function onZanzhuSortClick(){ + $('.zanzhu-sort-btn').click(function(){ + // 切换 class + $('.zz-sort-native').removeClass('zz-sort-native'); + $(this).addClass('zz-sort-native'); + + // 切换数据 + zzCfg.curr = 1; // 重置为第1页 + zzCfg.sort = parseInt($(this).attr('sort-value')); + renderDonateTable2(); + }) +} +onZanzhuSortClick(); + +// 读取 sa-token-donate 页数据为 json +function readDataToJson() { + var arr = []; + var trList = $('.zanzhu-box table tbody tr'); + for (let tr of trList) { + var tdArr = $(tr).find('td'); + var item = { + name: $(tdArr[0]).text(), + link: $(tdArr[0]).find('a').attr('href') || '', + money: parseFloat($(tdArr[1]).text().replaceAll('¥', '')), + msg: $(tdArr[2]).html(), + date: $(tdArr[3]).text(), + }; + arr.push(item); + } + return arr; +} +function readDataToJsonStr() { + var arr = readDataToJson(); + var str = ''; + for (let item of arr) { + str = JSON.stringify(item) + ',' + str; + } + return str; +} + diff --git a/sa-token-doc/static/donate/donate-list.js b/sa-token-doc/static/donate/donate-list.js new file mode 100644 index 000000000..9b1fd16bb --- /dev/null +++ b/sa-token-doc/static/donate/donate-list.js @@ -0,0 +1,1633 @@ +// 赞助者名单 +var donateList = [ + { + "name": "省长", + "link": "https://gitee.com/click33", + "money": 10, + "msg": "java中最好用的权限认证框架!", + "date": "2020-12-15" + }, + { + "name": "知知", + "link": "https://gitee.com/double_zhi", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2020-12-15" + }, + { + "name": "zhangjiaxiaozhuo", + "link": "https://gitee.com/zhangjiaxiaozhuo", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2020-12-15" + }, + { + "name": "RockMan", + "link": "https://gitee.com/njx33", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2020-12-17" + }, + { + "name": "whcrow", + "link": "https://gitee.com/whcrow", + "money": 20, + "msg": "军师加油!", + "date": "2021-03-16" + }, + { + "name": "xue1992wz", + "link": "https://gitee.com/xue1992wz", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2021-03-16" + }, + { + "name": "萧瑟", + "link": "https://gitee.com/fengduidui", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2021-03-16" + }, + { + "name": "二范先生", + "link": "https://gitee.com/mr-er-fan", + "money": 20, + "msg": "省长加油啊 喝杯茶", + "date": "2021-03-16" + }, + { + "name": "Wizzer", + "link": "https://gitee.com/wizzer", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2021-05-22" + }, + { + "name": "孔孔的空空", + "link": "https://gitee.com/kongmr", + "money": 500, + "msg": "感谢您的开源项目!", + "date": "2021-07-30" + }, + { + "name": "xiaoyan", + "link": "https://gitee.com/l-yun", + "money": 50, + "msg": "be better", + "date": "2021-07-31" + }, + { + "name": "xiaoyan", + "link": "https://gitee.com/l-yun", + "money": 200, + "msg": "好的作者理应被认可", + "date": "2021-08-24" + }, + { + "name": "苏永晓", + "link": "https://gitee.com/suyongxiao", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2021-09-01" + }, + { + "name": "永夜", + "link": "https://gitee.com/cn-src", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2021-09-18" + }, + { + "name": "apifox001", + "link": "https://gitee.com/apifox001", + "money": 200, + "msg": "Apifox:API 文档、API 调试、API Mock、API 自动化测试", + "date": "2021-10-15" + }, + { + "name": "xiaoyan", + "link": "https://gitee.com/l-yun", + "money": 200, + "msg": "节日快乐", + "date": "2021-10-24" + }, + { + "name": "ithorns", + "link": "https://gitee.com/ithorns", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2021-10-25" + }, + { + "name": "songfazhun", + "link": "https://gitee.com/fzsong", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2021-10-28" + }, + { + "name": "孔孔的空空", + "link": "https://gitee.com/kongmr", + "money": 100, + "msg": "感谢您的开源项目!", + "date": "2021-11-02" + }, + { + "name": "铂赛东", + "link": "https://gitee.com/bryan31", + "money": 20, + "msg": "开源加油!", + "date": "2021-11-08" + }, + { + "name": "公子骏", + "link": "https://gitee.com/dt_flys", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2021-11-08" + }, + { + "name": "Taller", + "link": "https://gitee.com/evilatom", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2021-11-13" + }, + { + "name": "万声鹉", + "link": "https://gitee.com/wanshengwu", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2021-11-15" + }, + { + "name": "yijunzhao", + "link": "https://gitee.com/yijunzhao", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2021-11-21" + }, + { + "name": "xiaoyan", + "link": "https://gitee.com/l-yun", + "money": 200, + "msg": "感谢您的开源项目!", + "date": "2021-11-26" + }, + { + "name": "luyuan", + "link": "https://gitee.com/meitesi", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2021-11-29" + }, + { + "name": "图灵谷", + "link": "https://gitee.com/stephenson37", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2021-11-30" + }, + { + "name": "fuhouyin", + "link": "https://gitee.com/fuhouyin", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2021-12-01" + }, + { + "name": "liu", + "link": "https://gitee.com/liuliuliu123456", + "money": 50, + "msg": "感谢您的开源项目!", + "date": "2021-12-15" + }, + { + "name": "duyiliu", + "link": "https://gitee.com/duyiliu", + "money": 10, + "msg": "化繁为简,是门艺术。", + "date": "2021-12-16" + }, + { + "name": "MrXionGe", + "link": "https://gitee.com/MrXionGe", + "money": 10, + "msg": "SA加油~~", + "date": "2021-12-17" + }, + { + "name": "周周周杨", + "link": "https://gitee.com/ChaoGeWanJiu", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2021-12-18" + }, + { + "name": "网络小渣渣", + "link": "https://gitee.com/a9777", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2021-12-24" + }, + { + "name": "刚子 (微信打赏)", + "link": "", + "money": 50, + "msg": "微信打赏", + "date": "2021-12-27" + }, + { + "name": "两岁 (微信打赏)", + "link": "", + "money": 188, + "msg": "微信打赏", + "date": "2021-12-27" + }, + { + "name": "前世男友", + "link": "https://gitee.com/lanbaba666", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-02-17" + }, + { + "name": "赵津 (微信打赏)", + "link": "", + "money": 16, + "msg": "微信打赏", + "date": "2022-02-20" + }, + { + "name": "老杨", + "link": "https://gitee.com/yangs914", + "money": 6.66, + "msg": "感谢您的开源项目!", + "date": "2022-03-01" + }, + { + "name": "晓辉", + "link": "https://gitee.com/zxhShow", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-03-07" + }, + { + "name": "Charles7c", + "link": "https://gitee.com/Charles7c", + "money": 20, + "msg": "感谢您的开源项目!希望 SSO 模块越来越好!", + "date": "2022-03-17" + }, + { + "name": "黎子豪 (微信打赏)", + "link": "", + "money": 18.88, + "msg": "请你喝杯咖啡", + "date": "2022-03-21" + }, + { + "name": "秦政 (微信打赏)", + "link": "", + "money": 6.66, + "msg": "微信打赏", + "date": "2022-03-22" + }, + { + "name": "秦政 (微信打赏)", + "link": "", + "money": 20, + "msg": "微信打赏", + "date": "2022-03-22" + }, + { + "name": "刘嘉威", + "link": "https://gitee.com/liu_jiawei", + "money": 6.66, + "msg": "真滴好用~", + "date": "2022-03-23" + }, + { + "name": "Robin Tin (微信打赏)", + "link": "", + "money": 28.88, + "msg": "微信打赏", + "date": "2022-03-24" + }, + { + "name": "lele", + "link": "https://gitee.com/lelez", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-03-29" + }, + { + "name": "alkinn", + "link": "https://gitee.com/alkinn", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-03-29" + }, + { + "name": "yukihane", + "link": "https://gitee.com/yukihane", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-04-07" + }, + { + "name": "xq584", + "link": "https://gitee.com/xq584", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-04-08" + }, + { + "name": "行长 (微信打赏)", + "link": "", + "money": 20, + "msg": "微信打赏", + "date": "2022-04-15" + }, + { + "name": "阿文", + "link": "https://gitee.com/qq921124136", + "money": 20, + "msg": "很好的框架,在开发文档里学到了很多知识点", + "date": "2022-04-21" + }, + { + "name": "Horatio201", + "link": "https://gitee.com/horatio201", + "money": 20, + "msg": "太牛了!", + "date": "2022-04-25" + }, + { + "name": "乡村阿土哥", + "link": "https://gitee.com/895995040", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-04-29" + }, + { + "name": "李洪星", + "link": "https://gitee.com/li_hong_xing", + "money": 10, + "msg": "解决了很多之前项目中遇到的问题。感谢您的开源项目!", + "date": "2022-04-29" + }, + { + "name": "别处理", + "link": "https://gitee.com/zshnb", + "money": 10, + "msg": "非常好的项目,希望能一直做下去", + "date": "2022-05-01" + }, + { + "name": "cray", + "link": "https://gitee.com/hyy6300", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-05-10" + }, + { + "name": "LZ", + "link": "https://gitee.com/FUNKBOY", + "money": 6.66, + "msg": "感谢您的开源项目!顺便踩一脚Spring Security,sa加油!", + "date": "2022-05-18" + }, + { + "name": "sun_2020", + "link": "https://gitee.com/sun-two-thousand-and-twenty", + "money": 50, + "msg": "感谢您的开源项目!", + "date": "2022-06-08" + }, + { + "name": "yuncai929", + "link": "https://gitee.com/null_448_5562", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-06-10" + }, + { + "name": "刘时立", + "link": "https://gitee.com/liu-shili", + "money": 10, + "msg": "非常棒的开源项目!", + "date": "2022-06-13" + }, + { + "name": "qiuyue", + "link": "https://gitee.com/bmlt", + "money": 10, + "msg": "satoken牛逼", + "date": "2022-06-16" + }, + { + "name": "风如歌", + "link": "https://gitee.com/the-wind-is-like-a-song", + "money": 10, + "msg": "这个框架简直满足了我所有对于安全框架的需求,赞一个,加油sa-token加油中国开源!", + "date": "2022-06-17" + }, + { + "name": "zhihong", + "link": "https://gitee.com/zzh13520704819", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2022-06-20" + }, + { + "name": "jwc_gitee", + "link": "https://gitee.com/jwc-gitee", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-07-07" + }, + { + "name": "小北宸呀", + "link": "https://gitee.com/a_aas", + "money": 10, + "msg": "感谢您的开源项目!我就喜欢你这种把我当白痴的官方文档", + "date": "2022-07-08" + }, + { + "name": "jerrydo", + "link": "https://gitee.com/jerrydo", + "money": 10, + "msg": "感谢您的开源项目!很强大!", + "date": "2022-08-10" + }, + { + "name": "邱道长", + "link": "https://gitee.com/qiudaozhang", + "money": 20, + "msg": "优秀的项目,赞", + "date": "2022-09-09" + }, + { + "name": "BlueRose", + "link": "https://gitee.com/Bluerose_2", + "money": 20, + "msg": "感谢您的付出,项目非常棒!", + "date": "2022-09-22" + }, + { + "name": "西东", + "link": "https://gitee.com/noear_admin", + "money": 99, + "msg": "感谢您的开源项目!", + "date": "2022-10-05" + }, + { + "name": "xueshize", + "link": "https://gitee.com/xueshize", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2022-10-12" + }, + { + "name": "feyong", + "link": "https://gitee.com/feyong", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-10-18" + }, + { + "name": "王文博", + "link": "https://gitee.com/rl520", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2022-10-24" + }, + { + "name": "就眠儀式", + "link": "https://gitee.com/Jmysy", + "money": 50, + "msg": "感谢您的开源项目!", + "date": "2022-10-26" + }, + { + "name": "laruui", + "link": "https://gitee.com/laruui", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-10-28" + }, + { + "name": "feel", + "link": "https://gitee.com/xujiahuim", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-11-17" + }, + { + "name": "IlovePea", + "link": "https://gitee.com/IlovePea", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-11-22" + }, + { + "name": "ThatYear", + "link": "https://gitee.com/wangmuqing", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2022-11-24" + }, + { + "name": "时间很快", + "link": "https://gitee.com/frsimple", + "money": 50, + "msg": "感谢您的开源项目!", + "date": "2022-11-29" + }, + { + "name": "刘涛", + "link": "https://gitee.com/doILike", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-12-13" + }, + { + "name": "ken", + "link": "https://gitee.com/affction", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-12-19" + }, + { + "name": "Peter Z", + "link": "https://gitee.com/zj1995", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2022-12-26" + }, + { + "name": "SWmachine", + "link": "https://gitee.com/SWmachine", + "money": 10, + "msg": "您的开源很好用!", + "date": "2023-01-07" + }, + { + "name": "tsing", + "link": "https://gitee.com/tsing666", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2023-01-08" + }, + { + "name": "不问烟雨", + "link": "https://gitee.com/xiaominfagui", + "money": 10, + "msg": "牛", + "date": "2023-01-12" + }, + { + "name": "熊孩子", + "link": "https://gitee.com/xhz1230", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-02-17" + }, + { + "name": "陈乾", + "link": "https://gitee.com/qianpou", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-03-05" + }, + { + "name": "陈乾", + "link": "https://gitee.com/qianpou", + "money": 50, + "msg": "感谢您的开源项目!", + "date": "2023-03-07" + }, + { + "name": "李一博", + "link": "https://gitee.com/haust_lyb", + "money": 8.88, + "msg": "感谢您的开源项目!", + "date": "2023-03-07" + }, + { + "name": "空空(微信打赏)", + "link": "", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2023-03-08" + }, + { + "name": "Java_小生", + "link": "https://gitee.com/zhang_hanzhe", + "money": 10, + "msg": "感谢Sa-Token让我不用去B站肯几十个小时的教程,框架很优秀文档更优秀", + "date": "2023-03-09" + }, + { + "name": "zhou", + "link": "https://gitee.com/mrzhou1", + "money": 50, + "msg": "感谢答疑", + "date": "2023-03-29" + }, + { + "name": "F(微信打赏)", + "link": "", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-04-09" + }, + { + "name": "王宁波", + "link": "https://gitee.com/wang-ningbo", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-04-10" + }, + { + "name": "Admin", + "link": "https://gitee.com/jinan-jimeng-network_0", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-04-12" + }, + { + "name": "李广龙", + "link": "https://gitee.com/ak47-b", + "money": 20, + "msg": "跟大哥学习一辈子学不完", + "date": "2023-04-14" + }, + { + "name": "hurumo", + "link": "https://gitee.com/hurumo", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-04-17" + }, + { + "name": "c(微信打赏)", + "link": "", + "money": 100, + "msg": "感谢您的开源项目!", + "date": "2023-04-17" + }, + { + "name": "bootx", + "link": "https://gitee.com/bootx", + "money": 100, + "msg": "Bootx-Platform:支付收单、三方对接、后端基于 Spring Boot、Spring Cloud 应用脚手架", + "date": "2023-04-18" + }, + { + "name": "gdl", + "link": "https://gitee.com/gdl97", + "money": 20, + "msg": "感谢您的开源项目!作者牛逼!", + "date": "2023-04-29" + }, + { + "name": "SummerHy", + "link": "https://gitee.com/hurumo", + "money": 10, + "msg": "国产,就是棒,:)", + "date": "2023-05-07" + }, + { + "name": "BeckJin", + "link": "https://gitee.com/beckjin666", + "money": 100, + "msg": "明道云-零代码开发平台,快速响应业务需求。从“IT背锅侠”,变成“IT英雄”。", + "date": "2023-05-08" + }, + { + "name": "xc_Moving", + "link": "https://gitee.com/fireZhang", + "money": 20, + "msg": "感谢您的开源项目!感谢SA-token帮我度过项目的难关", + "date": "2023-05-11" + }, + { + "name": "砰嚓嚓(QQ打赏)", + "link": "", + "money": 20, + "msg": "一点打赏不成敬意", + "date": "2023-05-15" + }, + { + "name": "dyjgitdyjgit", + "link": "https://gitee.com/qtinfogit", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-05-22" + }, + { + "name": "javahuang", + "link": "https://gitee.com/javahrp", + "money": 200, + "msg": "SurveyKing:功能最强大的调查问卷系统和考试系统,开源", + "date": "2023-06-08" + }, + { + "name": "SP", + "link": "https://gitee.com/LSP1999", + "money": 10, + "msg": "就是需要这种简单上手的项目", + "date": "2023-06-15" + }, + { + "name": "Dear胜哥", + "link": "https://gitee.com/DearShengGe", + "money": 10, + "msg": "有幸在摸鱼时间认真看完了全文档,感觉很是不错。开源不易,望作者继续扩展该框架功能!", + "date": "2023-06-30" + }, + { + "name": "吴其敏(微信打赏)", + "link": "", + "money": 200, + "msg": "CAT 是基于 Java 开发的实时应用监控平台,为美团点评提供了全面的实时监控告警服务。", + "date": "2023-07-11" + }, + { + "name": "mikeinshanghai", + "link": "https://gitee.com/mikeinshanghai", + "money": 50, + "msg": "Sa-Token, MeterSphere共成长, 共辉煌! ", + "date": "2023-07-14" + }, + { + "name": "张兆伟", + "link": "https://gitee.com/zhang865700", + "money": 50, + "msg": "感谢您的开源项目!", + "date": "2023-07-24" + }, + { + "name": "XiaoYi", + "link": "https://gitee.com/getianit", + "money": 100, + "msg": "亚洲云深圳BGP云服务器", + "date": "2023-07-24" + }, + { + "name": "好心肠的老哥", + "link": "https://gitee.com/ntdm", + "money": 10, + "msg": "非常好的开源项目,希望越来越好!", + "date": "2023-08-02" + }, + { + "name": "结弦奏(微信打赏)", + "link": "", + "money": 50, + "msg": "感谢您的开源项目!", + "date": "2023-08-07" + }, + { + "name": "失败女神", + "link": "https://gitee.com/failedgoddess", + "money": 50, + "msg": "感谢您的开源项目!", + "date": "2023-08-03" + }, + { + "name": "快快乐乐小码农", + "link": "https://gitee.com/happy-little-farmer", + "money": 1, + "msg": "感谢您的开源项目!", + "date": "2023-08-17" + }, + { + "name": "刘斌", + "link": "https://gitee.com/xuanfather", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-08-17" + }, + { + "name": "Meteor", + "link": "https://gitee.com/meteoroc", + "money": 2.5, + "msg": "感谢您的开源项目!", + "date": "2023-08-23" + }, + { + "name": "上下求索(微信打赏)", + "link": "", + "money": 12, + "msg": "明天请你吃个早餐吧", + "date": "2023-08-31" + }, + { + "name": "T_T", + "link": "https://gitee.com/wm26hua", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-09-07" + }, + { + "name": "huni", + "link": "https://gitee.com/simin_sizi", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2023-09-11" + }, + { + "name": "lostyue", + "link": "https://gitee.com/lostyue", + "money": 20, + "msg": "感谢您的开源项目!", + "date": "2023-09-14" + }, + { + "name": "shenlicao", + "link": "https://gitee.com/shenlicao", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2023-09-15" + }, + { + "name": "明道云", + "link": "https://gitee.com/lunan-yn", + "money": 200, + "msg": "明道云2023年伙伴大会,报名链接", + "date": "2023-09-25" + }, + { + "name": "yang", + "link": "https://gitee.com/hansdm", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2023-09-27" + }, + { + "name": "lee", + "link": "https://gitee.com/cngeeklee", + "money": 10, + "msg": "真正的轻量级权限安全框架,希望继续更新", + "date": "2023-10-06" + }, + { + "name": "yangs2w", + "link": "https://gitee.com/yangs2w", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2023-10-10" + }, + { + "name": "老马(微信打赏)", + "link": "", + "money": 99, + "msg": "我使用过的开源项目,作者我都给过红包了。请收下", + "date": "2023-10-16" + }, + { + "name": "ly-chn", + "link": "https://gitee.com/ly-chn", + "money": 99, + "msg": "一定的资金支持有助于开源项目走的更加长远", + "date": "2023-10-17" + }, + { + "name": "PotatoLoofah", + "link": "https://gitee.com/PotatoLoofah", + "money": 10, + "msg": "感谢您的开源项目!", + "date": "2023-10-27" + }, + { + "name": "立秋", + "link": "https://gitee.com/code_wh", + "money": 2.5, + "msg": "感谢您的开源项目!", + "date": "2023-10-27" + }, + { + "name": "时间很快", + "link": "https://gitee.com/frsimple", + "money": 220, + "msg": "感谢您的开源项目!", + "date": "2023-10-27" + }, + { + "name": "flydongdong", + "link": "https://gitee.com/flydongdong", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2023-10-31" + }, + + + { + "name": "MetaLowCode", + "link": "https://gitee.com/meta_low_code_admin", + "money": 220.0, + "msg": '可能是最适合Java程序员的低代码平台 -- 美乐低代码 https://melecode.com/', + "date": "2023-11-23" + }, + { + "name": "rednettle", + "link": "https://gitee.com/rednettle", + "money": 5.0, + "msg": "感谢您的开源项目!", + "date": "2023-11-24" + }, + { + "name": "郑志强", + "link": "https://gitee.com/zhi_qiang_zheng", + "money": 20.0, + "msg": "感谢您的开源项目!", + "date": "2023-12-01" + }, + { + "name": "Justin Chia", + "link": "https://gitee.com/justin-chia", + "money": 218.0, + "msg": '可以二开的国产低代码表单 https://vform666.com/', + "date": "2023-12-05" + }, + { + "name": "asalan570", + "link": "https://gitee.com/asalan570", + "money": 2.0, + "msg": "感谢您的开源项目!", + "date": "2023-12-12" + }, + { + "name": "guwq", + "link": "https://gitee.com/guweiqiang2016", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2023-12-14" + }, + { + "name": "少年", + "link": "https://gitee.com/tingfengBlog", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2024-01-10" + }, + { + "name": "mshk", + "link": "https://gitee.com/yueguangshuiyan", + "money": 50.0, + "msg": "Thank you for your open source repository!", + "date": "2024-02-21" + }, + { + "name": "CSpy", + "link": "https://gitee.com/cspy", + "money": 10.0, + "msg": "希望在线文档网站能有个“我已点赞”的跳过按钮,互相尊重一下,谢谢。", + "date": "2024-03-07" + }, + { + "name": "EtSKY", + "link": "https://gitee.com/ecoiyun", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-08" + }, + { + "name": "李富康", + "link": "https://gitee.com/li-fukang0719", + "money": 5.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-15" + }, + + + { + "name": "Jacky", + "link": "https://gitee.com/jackywjj", + "money": 50.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-20" + }, + { + "name": "yuluo", + "link": "https://gitee.com/hlzha", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-20" + }, + { + "name": "ai稞", + "link": "https://gitee.com/bbpla", + "money": 300.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-20" + }, + { + "name": "Smile丶掩饰", + "link": "https://gitee.com/smile_gjy", + "money": 50.0, + "msg": "感谢您的开源项目!加油!", + "date": "2024-03-20" + }, + { + "name": "小雪纷飞", + "link": "https://gitee.com/wujiangwu", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-20" + }, + { + "name": "dengyuanke", + "link": "https://gitee.com/dengyuanke", + "money": 10.0, + "msg": "感谢", + "date": "2024-03-20" + }, + { + "name": "Brath", + "link": "https://gitee.com/Guoqing-Li", + "money": 230.0, + "msg": '感谢SaToken开源!荔知AI是一款优秀的AI网站,地址:https://www.brath.cn', + "date": "2024-03-20" + }, + { + "name": "厉军", + "link": "https://gitee.com/shlijun", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-20" + }, + { + "name": "Blue", + "link": "https://gitee.com/my-blue", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-20" + }, + { + "name": "Cole Xu(微信打赏)", + "link": "", + "money": 50.0, + "msg": "一直在使用satoken,感谢你的付出", + "date": "2024-03-20" + }, + { + "name": "cy42", + "link": "https://gitee.com/third-party-framework", + "money": 50.0, + "msg": "感谢您的开源项目!", + "date": "2024-03-26" + }, + { + "name": "YaeMivo(微信打赏)", + "link": "", + "money": 20.0, + "msg": "祝越做越好", + "date": "2024-03-29" + }, + { + "name": "炮孩子", + "link": "https://gitee.com/paohaizi", + "money": 10.0, + "msg": "拳打Apach shiro,脚踢 Spring Security。", + "date": "2024-03-30" + }, + { + "name": "孤独的造梦者", + "link": "https://gitee.com/dpxz", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2024-04-01" + }, + { + "name": "HiSin", + "link": "https://gitee.com/HisinLx", + "money": 20.0, + "msg": "感谢您的开源项目!", + "date": "2024-05-07" + }, + { + "name": "INS6", + "link": "https://gitee.com/feiyuchuixue", + "money": 188.0, + "msg": '感谢Sa-Token开源!Sz-Admin一个轻量化RBAC开源框架。', + "date": "2024-06-05" + }, + { + "name": "Zongyy", + "link": "https://gitee.com/zongyY11", + "money": 10.0, + "msg": "感谢您的开源项目!", + "date": "2024-06-05" + }, + { + "name": "驰骋BPM", + "link": "https://gitee.com/chichengsoft", + "money": 100.0, + "msg": '感谢开源, 欢迎下载:驰骋低代码BPM https://gitee.com/opencc/JFlow', + "date": "2024-06-11" + }, + { + "name": "flydongdong", + "link": "https://gitee.com/flydongdong", + "money": 10.0, + "msg": '感谢您的开源项目!', + "date": "2024-06-18" + }, + { + "name": "驰骋BPM", + "link": "https://gitee.com/chichengsoft", + "money": 100.0, + "msg": '感谢您的开源项目!欢迎了解驰骋BPM低代码. https://gitee.com/opencc/JFlow', + "date": "2024-06-20" + }, + { + "name": "Mall4j商城系统", + "link": "https://gitee.com/gz-yami_admin", + "money": 218.0, + "msg": '感谢开源!Mall4j商城系统: https://gitee.com/gz-yami/mall4j', + "date": "2024-06-21" + }, + { + "name": "FlyFlow", + "link": "https://gitee.com/junyue", + "money": 200.0, + "msg": '感谢开源!FlyFlow工作流: https://gitee.com/junyue/flyflow', + "date": "2024-06-25" + }, + { + "name": "immortal", + "link": "https://gitee.com/immortal-wang", + "money": 10.0, + "msg": '感谢您的开源项目,内部项目鉴权框架参考了您的部分设计思想(用户会话和令牌会话)。', + "date": "2024-07-20" + }, + { + "name": "张磊", + "link": "https://gitee.com/zl18282425038", + "money": 1.0, + "msg": '感谢您的开源项目!', + "date": "2024-08-03" + }, + { + "name": "老黄H", + "link": "https://gitee.com/lao-huang-h", + "money": 1.0, + "msg": '感谢您的开源项目我是王攀', + "date": "2024-08-04" + }, + { + "name": "Chat2DB", + "link": "https://gitee.com/jipengfei001", + "money": 10.0, + "msg": 'https://github.com/chat2db/Chat2DB/ 数据库客户端', + "date": "2024-08-05" + }, + + { + "name": "gentleman", + "link": "https://gitee.com/guoweiweigege", + "money": 10.0, + "msg": '设计简单 功能多且强大 我杜伟坤为你代言', + "date": "2024-08-09" + }, + { + "name": "june", + "link": "https://gitee.com/june_home", + "money": 50.0, + "msg": '非常方便简单易用,感谢您的开源项目!', + "date": "2024-08-14" + }, + { + "name": "kaka", + "link": "https://gitee.com/blueair", + "money": 10.0, + "msg": '感谢您的开源项目!', + "date": "2024-08-30" + }, + { + "name": "有锦", + "link": "https://gitee.com/mushi00", + "money": 1.0, + "msg": '好厉害的项目啊 我郭威虽然没什么钱但是我郭威还是捐赠一下我郭威真的很认可这个项目,我郭威太崇拜了', + "date": "2024-09-03" + }, + { + "name": "zhangboyang", + "link": "https://gitee.com/zhangboyangos", + "money": 10.0, + "msg": '感谢您的开源项目!', + "date": "2024-09-04" + }, + { + "name": "读钓", + "link": "https://gitee.com/songyinyin", + "money": 50.0, + "msg": '感谢您的开源项目!致敬用爱发电', + "date": "2024-09-14" + }, + { + "name": "sswiki", + "link": "https://gitee.com/sswiki", + "money": 50.0, + "msg": '感谢开源!私有化部署的企业知识库:https://doc.zyplayer.com', + "date": "2024-09-24" + }, + { + "name": "坚持就是胜利", + "link": "https://gitee.com/insistppp", + "money": 1.0, + "msg": '感谢您的开源项目!', + "date": "2024-09-27" + }, + { + "name": "StrawberryerBlue", + "link": "https://gitee.com/strawberryerblue", + "money": 50.0, + "msg": '感谢您的开源项目!', + "date": "2024-10-14" + }, + { + "name": "qing", + "link": "https://gitee.com/haomao1", + "money": 20.0, + "msg": '非常好用,感谢您的开源项目!', + "date": "2024-10-15" + }, + { + "name": "厉飞雨", + "link": "https://gitee.com/david666a", + "money": 58.0, + "msg": '感谢道友,深有启发。', + "date": "2024-10-16" + }, + { + "name": "李嘉辉", + "link": "https://gitee.com/lee_kiahwee", + "money": 10.0, + "msg": '感谢您的开源项目!', + "date": "2024-10-17" + }, + { + "name": "不问烟雨", + "link": "https://gitee.com/xiaominfagui", + "money": 20.0, + "msg": '加油', + "date": "2024-11-04" + }, + { + "name": "zonglinjiang", + "link": "https://gitee.com/jiang-zonglin0427", + "money": 5.0, + "msg": '已经在至少两个商业项目里面使用了 ,非常好用,感谢作者的开源精神', + "date": "2024-11-05" + }, + { + "name": "当下", + "link": "https://gitee.com/carl1974", + "money": 10.0, + "msg": '感谢您的开源项目!', + "date": "2024-11-18" + }, + { + "name": "唐醋鱼(微信打赏)", + "link": "", + "money": 8.8, + "msg": '小小心意,群主请受纳', + "date": "2024-11-19" + }, + { + "name": "cunyun", + "link": "https://gitee.com/cunyun", + "money": 1.0, + "msg": '感谢您的开源项目!', + "date": "2024-11-27" + }, + { + "name": "guwq", + "link": "https://gitee.com/guweiqiang2016", + "money": 10.0, + "msg": '感谢您的开源项目!', + "date": "2024-12-05" + }, + { + "name": "kingkick", + "link": "https://gitee.com/kingkick", + "money": 10.0, + "msg": '文档真好!学习到不止是 Sa-Token 框架本身,更是绝大多数场景下权限设计的最佳实践。', + "date": "2024-12-12" + }, + { + "name": "JavaBean", + "link": "https://gitee.com/DearShengGe", + "money": 6.6, + "msg": '跟着Sa的文档一点点理解仿佛有位老师在带领着一步步去学,尤其是SSO单点登录部分!好东西不能被埋没!', + "date": "2024-12-19" + }, + { + "name": "焱枫", + "link": "https://gitee.com/dellibrunaway", + "money": 10.0, + "msg": '开心快乐每一天', + "date": "2024-12-20" + }, + { + "name": "dmyi", + "link": "https://gitee.com/dmyi", + "money": 20.0, + "msg": '感谢您的开源项目!', + "date": "2025-01-03" + }, + { + "name": "费雷", + "link": "https://gitee.com/feileier", + "money": 20.0, + "msg": '感谢您的开源项目!', + "date": "2025-01-09" + }, + { + "name": "苏俊", + "link": "https://gitee.com/fareuwell", + "money": 50.0, + "msg": '感谢您的开源项目!', + "date": "2025-01-10" + }, + { + "name": "阡陌兮", + "link": "https://gitee.com/i_kang", + "money": 9.9, + "msg": '感谢您的开源项目!', + "date": "2025-01-16" + }, + { + "name": "main", + "link": "https://gitee.com/zgx1179399522", + "money": 50.0, + "msg": '感谢您的开源项目!', + "date": "2025-01-22" + }, + { + "name": "shalixiaohu", + "link": "https://gitee.com/jiaruozhi", + "money": 10.0, + "msg": '感谢您的开源项目!', + "date": "2025-02-06" + }, + { + "name": "林佳奇", + "link": "https://gitee.com/ljq1307", + "money": 20.0, + "msg": '感谢您的开源项目!', + "date": "2025-02-15" + }, + { + "name": "AAA方一翻(微信打赏)", + "link": "", + "money": 28.8, + "msg": '请你喝杯奶茶', + "date": "2025-04-07" + }, + { + "name": "16群群友 错别字先生(微信打赏)", + "link": "", + "money": 50, + "msg": '感谢您的开源项目!', + "date": "2025-05-27" + }, + { + "name": "李猛(微信打赏)", + "link": "", + "money": 100, + "msg": '小小红包不成敬意,就当支持 satoken 的社区了哈', + "date": "2025-09-17" + }, + { + "name": "Owen(微信打赏)", + "link": "", + "money": 66.0, + "msg": '感谢您的开源项目!', + "date": "2025-09-19" + }, + { + "name": "Linex(赞赏码)", + "link": "", + "money": 0.01, + "msg": '感谢您的开源项目!', + "date": "2026-01-27" + }, + { + "name": "Json.张(赞赏码)", + "link": "", + "money": 1.0, + "msg": '小小心意', + "date": "2026-01-27" + }, + { + "name": "Nafil-鱼泡直聘运营(微信打赏)", + "link": "", + "money": 18.88, + "msg": '感谢您的开源项目!', + "date": "2026-01-29" + }, + { + "name": "偶T啊M(赞赏码)", + "link": "", + "money": 5.0, + "msg": '感谢您的开源项目!', + "date": "2026-02-06" + }, + { + "name": "马潮毅(赞赏码)", + "link": "", + "money": 50.0, + "msg": '希望 sa-token 社区越做越好', + "date": "2026-02-08" + }, + { + "name": "秋末-(赞赏码)", + "link": "", + "money": 29.9, + "msg": '非常棒的项目,加油', + "date": "2026-02-09" + }, + { + "name": "飘飘(赞赏码)", + "link": "", + "money": 88.88, + "msg": '做的很好,不白嫖,希望收费', + "date": "2026-02-25" + }, + { + "name": "Nafil-鱼泡直聘运营(微信打赏)", + "link": "", + "money": 16.88, + "msg": '感谢您的开源项目!', + "date": "2026-02-26" + }, + { + "name": "美人鱼(微信打赏)", + "link": "", + "money": 18.88, + "msg": '感谢您的开源项目!', + "date": "2026-03-3" + }, + { + "name": "G_M(赞赏码)", + "link": "", + "money": 3, + "msg": '感谢,确实很好用', + "date": "2026-03-13" + }, + { + "name": "Li(赞赏码)", + "link": "", + "money": 20, + "msg": '感谢您的开源项目,加油', + "date": "2026-03-17" + }, + { + "name": "123(赞赏码)", + "link": "", + "money": 18.88, + "msg": '感谢您开源的项目,中文文档太棒了!', + "date": "2026-03-17" + }, + { + "name": "石嘉懿(赞赏码)", + "link": "", + "money": 20, + "msg": '23级赞助', + "date": "2026-03-18" + }, + { + "name": "Ma.chine_M(微信打赏)", + "link": "", + "money": 20.00, + "msg": '喝杯咖啡!', + "date": "2026-03-20" + }, + { + "name": "四云(赞赏码)", + "link": "", + "money": 200, + "msg": '感谢您的开源项目', + "date": "2026-03-27" + }, + { + "name": "小火(赞赏码)", + "link": "", + "money": 0.05, + "msg": '感谢您的开源项目', + "date": "2026-03-29" + }, + + { + "name": "Right(赞赏码)", + "link": "", + "money": 1.0, + "msg": '感谢,ZZT.', + "date": "2026-04-09" + }, + { + "name": "糍粑(赞赏码)", + "link": "", + "money": 1.0, + "msg": '加油', + "date": "2026-04-28" + }, + { + "name": "jerry(赞赏码)", + "link": "", + "money": 42.9, + "msg": '非常棒的项目!', + "date": "2026-04-29" + }, + { + "name": "七(赞赏码)", + "link": "", + "money": 1.11, + "msg": '没啥钱的正在工作的毕业生,表示一下', + "date": "2026-05-08" + }, + { + "name": "朱毛毛(赞赏码)", + "link": "", + "money": 10.00, + "msg": '感谢您的开源', + "date": "2026-05-13" + }, + { + "name": "森林雨(赞赏码)", + "link": "", + "money": 6.66, + "msg": '加油,看好你们', + "date": "2026-05-17" + }, + { + "name": "黄桃(赞赏码)", + "link": "", + "money": 16.68, + "msg": '给16群多发点红包', + "date": "2026-06-02" + }, + { + "name": "null(赞赏码)", + "link": "", + "money": 1, + "msg": '感谢开源', + "date": "2026-06-02" + }, + { + "name": "三哥(赞赏码)", + "link": "", + "money": 9.9, + "msg": '好框架,中国造,加油', + "date": "2026-06-06" + }, + { + "name": "陈利(赞赏码)", + "link": "", + "money": 20.00, + "msg": '感谢您的开源项目!', + "date": "2026-06-10" + }, + { + "name": "拾壹(赞赏码)", + "link": "", + "money": 1.00, + "msg": '感谢您的开源项目!', + "date": "2026-06-12" + }, + { + "name": "willSleep(赞赏码)", + "link": "", + "money": 1.00, + "msg": '感谢您的开源项目!', + "date": "2026-07-16" + }, + { + "name": "燕南(赞赏码)", + "link": "", + "money": 1, + "msg": '嘻嘻', + "date": "2026-07-31" + }, + { + "name": "Leung(赞赏码)", + "link": "", + "money": 1, + "msg": '感谢您的开源项目!', + "date": "2026-08-06" + }, + { + "name": "null(赞赏码)", + "link": "", + "money": 1, + "msg": '我来试试', + "date": "2026-08-07" + }, +] \ No newline at end of file diff --git a/sa-token-doc/static/icon/code.svg b/sa-token-doc/static/icon/code.svg new file mode 100644 index 000000000..da1bafb4b --- /dev/null +++ b/sa-token-doc/static/icon/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sa-token-doc/static/icon/dati.svg b/sa-token-doc/static/icon/dati.svg new file mode 100644 index 000000000..c11f157c6 --- /dev/null +++ b/sa-token-doc/static/icon/dati.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sa-token-doc/static/icon/theme.svg b/sa-token-doc/static/icon/theme.svg new file mode 100644 index 000000000..3cf96c2b3 --- /dev/null +++ b/sa-token-doc/static/icon/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sa-token-doc/static/icon/video.svg b/sa-token-doc/static/icon/video.svg new file mode 100644 index 000000000..af1ee2020 --- /dev/null +++ b/sa-token-doc/static/icon/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sa-token-doc/static/index.css b/sa-token-doc/static/index.css new file mode 100644 index 000000000..bd6fd124c --- /dev/null +++ b/sa-token-doc/static/index.css @@ -0,0 +1,436 @@ +/* ================================== 内容 ====================================== */ +/* 总 */ +*{margin: 0px; padding: 0px;} +body{font-size: 16px; color: #34495E; font-family: "Source Sans Pro","Helvetica Neue","Arial,sans-serif";} +.z-div{} + +.s-width{width: 1000px; margin: auto;} +/* 栏目标题 */ +.s-title{color: #000; margin-top: 90px; margin-bottom: 50px;} +/* 解释性文字 */ +.re-text{color: #4e6e8e;} +/* 分割线 */ +.s-fenge{width: 80%; margin: auto; border-top: 1px #ddd solid;} + +/* ------- 头部样式 ------- */ +.doc-header{position: fixed; top: 0; z-index: 1000; width: 100%; height: 60px; line-height: 60px;} +.doc-header{background-color: hsla(0,0%,100%,0.97); box-shadow: 0 1px 3px rgba(26,26,26,0.1);} + +/* 左边logo */ +.nav-left{display: inline-block; float: left;} +.nav-left > a{text-decoration: none; color: inherit;} +.logo-box {display: inline-block; cursor: pointer; color: #000; padding-left: 24px; height: 60px; line-height: 60px;} +.logo-box img {width: 50px; height: 50px; vertical-align: middle; position: relative; top: -1px;} +.logo-box .logo-text {display: inline-block; margin: 0; padding: 0; padding-left: 5px; vertical-align: middle; font-size: 22px;/* font-weight: 700; */} + +/* + * 语言下拉:与文档页同一套样式 + * 用 #translateSelectLanguage 写死,切换语种重绘也不丢样式 + */ +#translateSelectLanguage{ + background-color: transparent !important; + display: inline-block !important; + vertical-align: middle; + margin-left: 10px !important; + border: 1px solid #999 !important; + color: #666 !important; + outline: 0; + cursor: pointer; + transition: all 0.2s; + background-color: #FFF; + font-size: 12px !important; + padding: 2px 2px !important; + position: relative; + top: 2px; + appearance: auto; + -webkit-appearance: menulist; + height: 22px !important; + line-height: 16px !important; + box-sizing: border-box; +} +#translateSelectLanguage:hover{box-shadow: 0 0 10px #aaa;} +/* 语言框单独限宽:长语种名别把顶栏撑爆 */ +#translateSelectLanguage{ + width: 88px !important; + max-width: 88px !important; +} +.nav-left .select-language-box{display: contents;} + +/* 右边导航 */ +.doc-header .nav-right{margin: 0; float: right; line-height: 60px; padding-right: 4em; white-space: nowrap; } +.doc-header .nav-right>*{padding: 0px; margin: 0 9px;} +.doc-header .nav-right>*:last-child{position: relative; z-index: 1002; } + +.nav-right a{color: #34495E; text-decoration: none; transition: all 0.2s;} +.nav-right a:hover{color: #42B983;} +.doc-header .nav-right .wzi{font-size: 14px; line-height: 61px; transition: color 0.2s; position: relative; text-decoration: none;} +.doc-header .nav-right .wzi::after{ + content: ''; + position: absolute; + left: 0; + right: 0; + top: calc(50% + 0.8em); + height: 2px; + background-color: #42B983; + transform: scaleX(0); + transform-origin: center; + transition: transform 0.25s ease; +} +.doc-header .nav-right .wzi:hover::after, +.doc-header .nav-right .wzi.nav-active::after{transform: scaleX(1);} + +/* 小章鱼 */ +.github-corner svg{color: #fff; fill: var(--theme-color, #42b983); height: 80px; width: 80px; z-index: 1001 !important;} + + +/* -------- 海报部分 --------- */ +.main-box{width: 100%; /* min-height: 70vh; */ /* height: 80vh; */ text-align: center; } +.main-box{display: flex; align-items: center; text-align: center; } +.fenge{min-height: 90px;} +.content-box{color: #000; flex: 1; padding: 120px 1em 70px;} +.content-box h1{font-size: 100px; font-weight: 400; position: relative; margin-top: 40px; /* margin-top: 15vh; */} +.content-box h1 small{font-size: 18px; position: absolute; bottom: 10px; margin-left: 5px; font-weight: 100;} +/* .title-logo{width: 221px; cursor: pointer; transition: all 0.2s;} +.title-logo:hover{transform: scale(1.2, 1.2);} */ + +.sub-title{font-size: 22px; font-weight: 400; margin-top: 30px; margin-bottom: 25px; color: #6a8bad; color: #234;} +/* .content-box p{line-height: 30px; padding: 0px 1em;} */ +/* 角标位置修复 */ +.badge-box a:nth-child(-n+2) img{position: relative; top: 1px;} +/* 模拟副标题的光标闪烁 */ +.gb-cursor {display: inline-block;width: 2px;height: 22px;position: relative;top: 4px;left: -4px;background-color: black;animation: blink 0.7s infinite alternate;} +@keyframes blink { from {opacity: 0;} to {opacity: 1;} } + +.main-box{background-image: url(/big-file/index/home-bg3.jpg); background-size: 120% 100%;} +.main-box{animation: changes 30s 0.2s linear infinite normal; /* background-attachment: ; */} /* normal | alternate */ +@keyframes changes { + from {background-position: 0vw 0%;} + to {background-position: -20vw 0%;} +} + +/* 几个按钮 */ +.btn-box{margin-top: 50px; margin-bottom: 40px;} +.btn-box a{border: 1px #1e8f5c solid; border-radius: 2em; box-sizing: border-box; color: #1e8f5c; display: inline-block;transition: all 0.1s;} +.btn-box a{font-size: 14px; background-color: rgba(0,255,0,0.06); letter-spacing: 1px; padding: 1em 2em; margin: 0 0.5em; margin-bottom: 14px; text-decoration: none; } +.btn-box a:hover{/* transform: scale(1.05, 1.05); */padding: 1em 2.3em; margin-left: 0.2em; margin-right: 0.2em;} +/* 最后一个加深底色 */ +.btn-box .doc-btn {color: #fff; background-color: #42B983; border: 1px green solid;} + +/* 按钮发光动画 */ +.btn-box .doc-btn{animation: bganimation 3s infinite;} +@keyframes bganimation{ + 0%{box-shadow: 0 0 1px #42B983;} + 50%{box-shadow: 0 0 20px #42B983;} + 100%{box-shadow: 0 0 20px #FFF;} +} + +/* 其它平台链接 */ +.qt-pt-box{margin-top: 40px;} +.qt-pt-box a{text-decoration: none; margin-right: 20px;} +.qt-pt-box>a img{height: 40px;} +.qt-pt-box a img{/* width: 130px; */ /* height: 40px; */ transition: all 0.2s !important;} +.qt-pt-box a img:hover{transform: scale(1.1, 1.1);} +/* .img-gitcode{width: 140px;} */ + +/* 多媒体平台 */ +.qt-pt-box .dmt-link{ } +.qt-pt-box .dmt-img{width: 40px; height: 40px; } +.qt-pt-box .dmt-tips{ vertical-align: 100%; color: #888; font-size: 12px; margin-left: 5px; } +.dmt-detail h4{padding-top: 10px; padding-bottom: 20px;} +/* 悬浮时展开 */ +.dmt-link{display: inline-block; position: relative;} +.dmt-detail{position: absolute; transform: translate(-300px, 0px); background-color: #FFF; overflow: hidden; display: none; transition: all 0.2s;} +.dmt-link:hover .dmt-detail{display: block; } +.dmt-detail{padding: 30px; padding-top: 15px; border: 1px #ccc solid;} +.dmt-item-box{display: flex; width: 600px;} +.dmt-detail .dmt-item{width: 200px; flex: 1; text-align: center; cursor: pointer;} +.dmt-item .dmt-qr-img{ max-width: 180px; } +.dmt-item .dmt-logo-img{ max-width: 180px; max-height: 50px; margin-top: 10px;} +.dmt-item-douyin,.dmt-item-wxsph{ margin-top: 15px; } +.dmt-item-bilibili .dmt-logo-img{margin-top: 20px;} +.dmt-item-wxsph .dmt-logo-img{margin-top: 15px;} + +/* 微信二维码 */ +.wx-qr-box{margin-top: 50px;} +.qr-item{display: inline-block;} +.qr-item p{font-size: 12px; padding: 0 0.5em;} +/* .qr-item a{color: #42B983;} */ +.wx-qr{width: 150px;} +.wx-qr-box p{margin-top: 10px; color: #666; margin-bottom: 20px;} +.wx-qr,.dro-qr{cursor: pointer;} + + +/* -------- 支持特性 --------- */ +.feature-z{padding: 0em 1em; padding-top: 0px; padding-bottom: 60px; text-align: center; color: #000;} +.feature-z .s-title{font-size: 30px; font-weight: 400; margin-top: 70px; margin-bottom: 40px; line-height: 1.5;} +.feature-z{color: rgb(128, 128, 128); text-align: center; box-sizing: border-box; line-height: 24px; font-size: 16px;} + +.feature-box{margin-top: 10px; margin-bottom: 70px; display: flex; flex-wrap: wrap; justify-content: space-between; /* justify-content: flex-start; */} +.feature{border: 0px #000 solid; flex: 0 0 33%; text-align: left; padding: 1.8em 1.2em; box-sizing: border-box;} +.feature h2{font-size: 22px; color: #000; font-weight: 400;} +.feature p{margin-top: 14px; font-size: 16px; color: #4e6e8e;} + +.sa-token-jss-img{ width: 100%; } + +/* 功能结构图介绍 */ +/* .sa-token-js-box{margin-bottom: 50px; transition: all 0.2s;} +.sa-token-js-box img{max-width: 100%;} +.sa-token-js-box:hover{cursor: pointer; box-shadow: 0 0 20px #ccc;} */ + +.re-text{padding: 0 1em;} +.re-text a{color: #0969da; text-decoration: none;} +.re-text a:hover{border-bottom: 1px #0969da solid;} + +/* 渐变色文字(兼容搜狗等 WebKit 内核浏览器) */ +.s-gradient-text{ + display: inline-block; + line-height: 1.5; + background-image: linear-gradient(to right, #44f, #bd34fe); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; +} +@supports not ((-webkit-background-clip: text) or (background-clip: text)) { + .s-gradient-text{ + background: none; + -webkit-text-fill-color: #7b3fe4; + color: #7b3fe4; + } +} + +/* -------- 集成案例 --------- */ +.s-case-box{justify-content: space-between;} +.s-case{border: 1px #e5e5e5 solid; flex: 0 0 31.5%; margin-top: 30px; text-align: left; box-sizing: border-box; padding-bottom: 16px; overflow: hidden;} +.s-case{position: relative; transition: all 0.2s; background-color: #FFF;} +.s-case-link{display: block; width: 100%; height: 0px; padding-bottom: 50%; position: relative; overflow: hidden;} +.s-case-link img{width: 100%; height: 100%; object-fit: cover; object-position: center; position: absolute;} +.s-case-title,.s-case-intro{padding: 0 16px;} +.s-case-title{margin-top: 20px; font-size: 18px; font-weight: 400; color: #333; font-family: "microsoft yahei";} +.s-case-intro{margin-top: 15px; font-size: 14px; line-height: 20px; color: #777; word-break:break-all;} +.s-author{color: #ff5722; border: 1px #ff5722 solid; position: absolute; right: 20px; display: inline-block;} +.s-author{padding: 0 5px; font-size: 12px; transform: translate(0, -25px);} + + +/* 悬浮动画 */ +.s-case:hover{box-shadow: 0 0 20px #ccc; border-color: rgba(22, 119, 255, 0.25);} +.s-case:hover img{transform: scale(1.3, 1.3); } +.s-case-link img{transition: transform 0.3s !important;} +.s-case img:hover{cursor: pointer;} +.s-case:hover .s-case-link:after {background-color: rgba(0, 0, 0, .35); color: #FFF;} +.s-case .s-case-link:after { + content: "详情"; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0); + transition: all .4s; + text-align: center; + line-height: 10; + color: rgba(0,0,0,0); + cursor: pointer; +} + +/* 边框流光环绕(悬浮触发,参考阿里云云服务器卡片) */ +@property --s-case-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} +.s-case::before{ + content: ""; + position: absolute; + inset: 0; + padding: 1.5px; + /* 头实尾虚:亮头在前,拖尾渐隐 */ + background: conic-gradient( + from var(--s-case-angle), + transparent 0%, + transparent 66%, + rgba(22, 119, 255, 0.15) 78%, + rgba(105, 192, 255, 0.45) 88%, + #4dabff 95%, + #1677ff 100% + ); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + pointer-events: none; + z-index: 3; + transition: opacity 0.25s ease; + animation: s-case-border-spin 7s linear infinite; + animation-play-state: paused; +} +.s-case:hover::before{ + opacity: 1; + animation-play-state: running; +} +@keyframes s-case-border-spin{ + to{ --s-case-angle: 360deg; } +} + + +/* -------- 赞助者名单 --------- */ +.zanzhu-box{font-size: 14px;} +.zanzhu-table{width: 100%; margin-top: 20px; /* border-color: #fff; */ text-align: left; color: #333;} +.zanzhu-table th,.zanzhu-table td{padding: 5px 10px;} +.zanzhu-table tr:nth-child(even){background: #F8F8F8;} +.zanzhu-table .zanzhu-money{color: red;} +.zanzhu-table .zanzhu-name a{text-decoration: none; color: #333;} +.zanzhu-table .zanzhu-name a:hover{text-decoration: underline; color: blue;} +/* 赞助排序盒子 */ +.zanzhu-sort-box{font-size: 14px; margin-top: -10px;} +.zanzhu-sort-box .zanzhu-sort-btn{text-decoration: none; color: #999; cursor: pointer;} +.zanzhu-sort-box .zanzhu-sort-btn:hover{text-decoration: underline; color: #55a;} +.zanzhu-sort-box .zanzhu-sort-btn.zz-sort-native{text-decoration: underline; color: #55a;} +/* 底部按钮盒子 */ +.zz-btn-box{text-align: center; margin-top: 20px; font-size: 14px;} +.zz-btn-box button{padding: 5px 10px; cursor: pointer; border: 1px #ccc solid; color: #999;} +.zz-btn-box button:hover{box-shadow: 0 0 10px #ddd;} + + + +/* -------- 使用公司 --------- */ +.com-box-f{padding: 1em 1em; padding-bottom: 30px; text-align: center;} +.com-box-f h2{font-size: 30px; color: #000; font-weight: 400;} +.com-box{display: flex; flex-wrap: wrap; width: 100%; margin-bottom: 50px; justify-content: flex-start;} +.com-box a{display: block; flex: 0 0 13%; margin: 5px; cursor: pointer; border: 0px #ddd solid;} +.com-box a{line-height: 75px;} +.com-box a img{transition: transform 0.2s !important; vertical-align: middle; min-width: 60%; max-width: 100%; max-height: 100%;} +.com-box a img:hover{transform: scale(1.05, 1.05);} +.com-box-you a img:hover{transform: none;} + +/* -------- 友情链接 --------- */ +.com-box-you a{flex: 0 0 14.5%; line-height: 60px; height: 60px; margin: 10px;} +.com-box-you a img{min-width: 60%; max-width: 85%; vertical-align: middle; max-height: 100%;} +.you-coop-tip{text-align: center; margin-top: 16px; font-size: 14px;} +.you-coop-tip a{color: #999; text-decoration: none;} +.you-coop-tip a:hover{color: #666; text-decoration: underline;} + +/* -------- Dromara 成员项目 --------- */ +.table-show-pj{border: 1px #d5d5d5 solid; border-width: 1px 0 0 1px ;} +.table-show-pj a{flex: 0 0 16.5%; border: 1px #d5d5d5 solid; margin: 0; padding: 7px 0; overflow: hidden;} +.table-show-pj a{border-width: 0 1px 1px 0px;} +.table-show-pj a img{min-width: 60%; max-width: 70%; } + +/* -------- 底部 - 连接 --------- */ +#footer{background-color: #181818;} +#footer h3{font-weight: 400; font-size: 16px; color: #ccc; margin-top: 20px; margin-bottom: 20px;} +#footer{border-top: 1px #666 solid;} +.footer-r-b{display: flex; padding: 40px 0;} +.ss-box{display: inline-block; flex: 1; color: #595959; margin: 0 50px; font-size: 14px;} +.ss-box a{color: #595959; text-decoration: none;} +.ss-box a:hover{color: #EEE; text-decoration: underline;} +.ss-box ul{margin: 0; padding: 0;} +.ss-box li{list-style: none; line-height: 28px;} + +/* -------- 底部 - 版权 --------- */ +.foot-box{background-color: #000; color: #ddd; padding: 2em 0px; line-height: 28px; overflow: hidden; position: relative; z-index: 100;} +.foot-box{border-top: 0px #666 solid;} +.foot-box p{text-indent: 1em;} +.foot-box b{font-size: 1.1em;} +.foot-box a{color: #ddd; font-size: 0.9em;} +.foot-box a:hover{text-decoration: underline;} + + + +/* -------- 自适应 --------- */ +/* 一般的笔记本 */ +@media screen and (max-width: 1700px) { + .content-box{padding-top: 100px;} + .content-box h1{font-size: 80px;} + + /* 支持特性部分的间距 */ + .s-title.s-title-tx{margin-top: 50px; margin-bottom: 20px; /* display: none; */} +} + +/* 一般的手机 */ +@media screen and (max-width: 800px) { + + .s-title{padding: 0 16px;} + .s-width{width: 100%;} + + .logo-box .logo-text,.copyright {display: none;} + .main-box{ height: auto;} + .content-box{padding-top: 100px;} + .content-box h1{font-size: 50px;} + + .feature-z{padding: 0em; padding-bottom: 50px;} + .feature{min-width: 100%;} + + .com-box-f{padding: 0em;} + .com-box{justify-content: space-around;} + .com-box a{flex: 0 0 90%;} + + .s-case-box{justify-content: space-around;} + .s-case{flex: 0 0 90%;} + + .footer-r-b{display: block;} + .ss-box{display: block; text-align: center; width: 90%; margin: 0px; padding-left: 1.5em;} + + /* .s-header{position: static;} */ + footer{position: static; line-height: 40px;} + + /* 手机端不显示广告,和一些其它东西 */ + .wwads-cn,.p-none{display:none!important} +} + +/* 手机端不显示广告,和一些其它东西 */ +/* @media (max-width: 576px) {.wwads-cn,.p-none{display:none!important}} */ + + + +/* 工具栏超链接 展开、收缩div */ +.zk-box{display: inline-block; padding-right: 0px; margin-right: -25px;} +/* 外层盒 */ +.zk-box .zk-context{max-height: 0px; position: absolute; overflow: hidden;} +.zk-box:hover .zk-context{max-height: 400px;} +/* 内层盒 */ +.zk-context>div{padding: 1em 0.5em 1em 1em; border: 1px #ccc solid; border-radius: 2px; background-color: #FFF; font-size: 12px; transition: all 0.2s; opacity: 0;} +.zk-box:hover .zk-context>div{opacity: 1;} +/* 小链接 */ +.zk-box .zk-context a{font-size: 14px; display: block; line-height: 32px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;} +.zk-box .zk-context a{text-align: left; padding: 0 1.5em 0 1em;} +.zk-box .zk-context .zk-fengexian{border-bottom: 1px #d9d9d9 solid; margin: 10px 0;} + +/* 下三角小图标 */ +.zk-icon{display: inline-block; width: 0px; height: 0px; position: relative;top: 3px; margin-left: 4px;} +.zk-icon{border-style: solid; border-width: 5px; border-color: #aaa transparent transparent transparent; } + + +/* ------------- 背景色相关 ------------- */ +/* 侧边栏需要透明 */ +.sidebar-toggle{background-color: transparent !important;} +.sidebar{background-color: transparent !important;} + +/* 变色的动画 */ +.doc-header,body{transition: all 0.5s !important;} + +/* 调色按钮 */ +.theme-btn{width: 25px; height: 25px; line-height: 60px; vertical-align: middle; position: relative; top: -1px;} +.theme-box{width: 160px; text-align: left; line-height: 20px; margin-top: -20px; white-space: normal;} +.theme-box span{ + display: inline-block; + width: 20px; + height: 20px; + margin: 1px 2px; + border: 1px #ccc solid; + cursor: pointer; + border-radius: 1px; + box-sizing: border-box; +} + + + +/* ajax加载时的转圈圈样式 */ +.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} +.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} +.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } + +/* 公安备案 */ +.foot-beian-gongan{display:inline-flex;align-items:center;gap:4px;vertical-align:middle;} +.foot-beian-gongan-icon{width:16px;height:16px;flex-shrink:0;} +#main footer a.footer-beian img{width:14px;height:14px;vertical-align:-2px;margin-right:3px;} diff --git a/sa-token-doc/static/is-fill-in-wj-plugin.js b/sa-token-doc/static/is-fill-in-wj-plugin.js new file mode 100644 index 000000000..d9def0069 --- /dev/null +++ b/sa-token-doc/static/is-fill-in-wj-plugin.js @@ -0,0 +1,114 @@ +// + +// 声明 docsify 插件 +var isFillInWjPlugin = function(hook, vm) { + + // 钩子函数:解析之前执行 + hook.beforeEach(function(content) { + return content; + }); + + // 钩子函数:每次路由切换时,解析内容之后执行 + hook.afterEach(function(html) { + return html; + }); + + // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 + hook.doneEach(function() { + isFillIn(vm); + }); + + // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 + hook.ready(function() { + + }); + +} + + +// 检查成功后,多少天不再检查 +const wjAllowDisparity = 1000 * 60 * 60 * 24 * 30 * 3; +// const allowDisparity = 1000 * 10; + + +// 判断当前是否已填写 +function isFillIn(vm) { + // 非PC端不检查 + if(document.body.offsetWidth < 800) { + console.log('small screen ... wj '); + return; + } + + // 白名单路由不判断 + const whiteList = ['/', '/more/link', '/more/demand-commit', '/more/join-group', '/more/sa-token-donate', '/more/wenjuan', + '/sso/sso-pro', '/more/update-log', '/more/common-questions', '/fun/sa-token-test', '/fun/issue-template']; + if(whiteList.indexOf(vm.route.path) >= 0) { + console.log('white route ... wj'); + return; + } + + // 判断是否近期已经判断过了 + try{ + const isFillIn = localStorage.isFillIn; + if(isFillIn) { + // 记录 star 的时间,和当前时间的差距 + const disparity = new Date().getTime() - parseInt(isFillIn); + + // 差距小于一月,不再检测,大于一月,再检测一下 + if(disparity < wjAllowDisparity) { + console.log('checked ... wj '); + return; + } + } + }catch(e){ + console.error(e); + } + + // 本次打开页面的内存内已经弹出了的话,也不再弹了 + if(window.isYtcXsjfkasjda) { + return; + } + window.isYtcXsjfkasjda = true; + + // 弹出弹框,邀请填写 + const tipStr = ` +
    +

    + 嗨,同学你好! +

    +

    + 我们想以运营一款产品的心态来运营一个开源框架,所以我们迫切希望您能够填写这份问卷,这有 6 道选择题, + 应该只会略微占用您 1~3 分钟的时间。 +

    +

    问卷地址:https://wj.qq.com/s2/14587150/b5b4/

    +

    Sa-Token 将会非常重视每一位粉丝的宝贵意见!😇😇😇

    +
    + `; + + const index = layer.confirm(tipStr, { + title: '问卷调查填写邀请', + btn: ['我已填写 (1月内不再弹出)', '暂时不要 (1天内不再弹出)'], + // btn: ['同意授权检测', '暂时不要,我先看看文档'], + area: '480px', + offset: '30%' + }, + // 点击确定 + function(index) { + layer.close(index); + localStorage.isFillIn = new Date().getTime(); + + layer.msg('感谢你的支持,Sa-Token 将努力变得更加完善! ❤️ ❤️ ❤️ ') + }, + // 点击取消 + function(){ + // 一天内不再检查 + const ygTime = allowDisparity - (1000 * 60 * 60 * 24); + localStorage.isFillIn = new Date().getTime() - ygTime; + + layer.alert('你可以随时在右上角 [ 相关资源 -> 问卷调查 ] 处找到问卷链接', function(index) { + layer.close(index); + }) + } + ); +} + diff --git a/sa-token-doc/static/is-star-plugin.js b/sa-token-doc/static/is-star-plugin.js new file mode 100644 index 000000000..c04b26ab4 --- /dev/null +++ b/sa-token-doc/static/is-star-plugin.js @@ -0,0 +1,259 @@ +// + +// 声明 docsify 插件 +var isStarPlugin = function(hook, vm) { + + // 钩子函数:解析之前执行 + hook.beforeEach(function(content) { + return content; + }); + + // 钩子函数:每次路由切换时,解析内容之后执行 + hook.afterEach(function(html) { + return html; + }); + + // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 + hook.doneEach(function() { + //isStarRepo(vm); + }); + + // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 + hook.ready(function() { + + }); + +} + +// 应用参数 +const client_id = '0cc618beb08db99bff50e500e38c2144d95ada9abb51c00c44592726ecd583f4'; +const client_secret = 'xxx'; +const redirect_uri = 'https://sa-token.com/doc.html'; +const docDomain = 'sa-token.com'; +// const redirect_uri = 'http://127.0.0.1:8848/sa-token-doc/doc.html'; +// const docDomain = '127.0.0.1:8848'; + +// 检查成功后,多少天不再检查 +const allowDisparity = 1000 * 60 * 60 * 24 * 30 * 3; +// const allowDisparity = 1000 * 10; + + +// 判断当前是否已 star +function isStarRepo(vm) { + // 非PC端不检查 + if(document.body.offsetWidth < 800) { + console.log('small screen ...'); + return; + } + + // 判断是否在主域名下 + if(location.host !== docDomain) { + console.log('not domain, no check...'); + return; + } + + // 判断是否近期已经判断过了 + try{ + const isStarRepo = localStorage.isStarRepo; + if(isStarRepo) { + // 记录 star 的时间,和当前时间的差距 + const disparity = new Date().getTime() - parseInt(isStarRepo); + + // 差距小于一月,不再检测,大于一月,再检测一下 + if(disparity < allowDisparity) { + console.log('checked ...'); + return; + } + } + }catch(e){ + console.error(e); + } + + // 白名单路由不判断 + const whiteList = ['/a', '/more/link', '/more/demand-commit', '/more/join-group', '/more/sa-token-donate', + '/sso/sso-pro', '/more/update-log', '/more/common-questions', '/fun/sa-token-test', '/fun/issue-template']; + if(whiteList.indexOf(vm.route.path) >= 0 && getParam('code') === null) { + console.log('white route ...'); + return; + } + + // 开始获取 code + $('body').css({'overflow': 'hidden'}); + getCode(); +} + +// 去请求授权 +function getCode() { + + // 检查url中是否有code + const code = getParam('code'); + if(code) { + // 有 code,进一步去请求 access_token + getAccessToken(code); + } else { + // 不存在code,弹窗提示询问 + confirmStar(); + } +} + +// 弹窗提示点 star +function confirmStar() { + + // 弹窗提示文字 + const tipStr = ` +
    +

    嗨,同学,来支持一下 Sa-Token 吧,为项目点个 star !

    +
    仅需两步即可完成:
    +
    1、打开 Sa-Token 开源仓库主页,在右上角点个 star 。
    +
    2、点击下方 [ 同意授权检测 ] 按钮,同意 Sa-Token 获取 API 权限进行检测。
    +
    +

    本章节文档将在 star 后正常开放展示。

    +

    开源不易,希望您不吝支持,激励开源项目走的更加长远 😇😇😇

    +
    + `; + + const index = layer.confirm(tipStr, { + title: '提示', + btn: ['同意授权检测'], + // btn: ['同意授权检测', '暂时不要,我先看看文档'], + area: '460px', + offset: '25%', + closeBtn: false + }, + function(index) { + // + layer.close(index); + // 用户点了确认,去 gitee 官方请求授权获取 + goAuth(); + } + ); + + // 源码注释提示 + const closeLayer = + ` + + `; + $('#layui-layer' + index).prepend(closeLayer) +} + + +// 跳转到 gitee 授权界面 +function goAuth() { + const authUrl = "https://gitee.com/oauth/authorize" + + "?client_id=" + client_id + + "&redirect_uri=" + redirect_uri + + "&response_type=code"; + location.href = authUrl; +} + + +// 获取 access_token +function getAccessToken(code) { + // 根据 code 获取 access_token + $.ajax({ + url: 'https://sa-token.com/server/oauth/token', + method: 'post', + data: { + grant_type: 'authorization_code', + code: code, + client_id: client_id, + redirect_uri: redirect_uri, + client_secret: client_secret, + }, + success: function(res) { + // 如果返回的不是 200 + if(res.code !== 200) { + return layer.alert(res.msg, {closeBtn: false}, function(){ + // 刷新url,去掉 code 参数 + location.href = 'doc.html'; + }); + } + + // 拿到 access_token + const access_token = res.access_token; + + // 根据 access_token 判断是否 star 了仓库 + $.ajax({ + url: 'https://gitee.com/api/v5/user/starred/dromara/sa-token', + method: 'get', + data: { + access_token: access_token + }, + success: function(res) { + // success 回调即代表已经 star,gitee API 请求体不返回任何数据 + console.log('-> stared ...'); + // 记录本次检查时间 + localStorage.isStarRepo = new Date().getTime(); + // + layer.alert('感谢你的支持 ❤️ ❤️ ❤️ ,Sa-Token 将努力变得更加完善!', function(index) { + layer.close(index); + // 刷新url,去掉 code 参数 + location.href = location.href.replace("?code=" + code, ''); + }) + }, + error: function(e) { + // console.log('ff请求错误 ', e); + // 如下返回,代表没有 star + if(e.statusText = 'Not Found'){ + console.log('not star ...'); + layer.alert('未检测到 star 数据...', {closeBtn: false}, function() { + // 刷新url,去掉 code 参数 + location.href = location.href.replace("?code=" + code, ''); + }); + } + } + }); + + }, + error: function(e) { + console.log('请求错误 ', e); + // 如果请求地址有错,可能是服务器宕机了,暂停一天检测 + if(e.status === 0 || e.status === 502) { + return layer.alert(JSON.stringify(e), {closeBtn: false}, function(){ + // 一天内不再检查 + const ygTime = allowDisparity - (1000 * 60 * 60 * 24); + localStorage.isStarRepo = new Date().getTime() - ygTime; + // 刷新 url,去掉 code 参数 + location.href = location.href.replace("?code=" + code, ''); + }); + } + + // 无效授权,可能是 code 无效 + const errorMsg = (e.responseJSON && e.responseJSON.error) || JSON.stringify(e); + if(errorMsg == 'invalid_grant') { + console.log('无效code', code); + } + layer.alert('check error... ' + errorMsg, function(index) { + layer.close(index); + // 刷新url,去掉 code 参数 + let url = location.href.replace("?code=" + code, ''); + url = url.replace("&code=" + code, ''); + location.href = url; + }); + } + }) +} + +// 疑问 +function authDetails() { + const str = "用于检测的凭证信息将仅保存你的浏览器本地,Sa-Token 文档已完整开源,源码可查"; + alert(str); +} + +// 获取 url 携带的参数 +function getParam(name, defaultValue){ + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i settings.failure_limit) { + return false; + } + } + }); + + } + + if(options) { + /* Maintain BC for a couple of versions. */ + if (undefined !== options.failurelimit) { + options.failure_limit = options.failurelimit; + delete options.failurelimit; + } + if (undefined !== options.effectspeed) { + options.effect_speed = options.effectspeed; + delete options.effectspeed; + } + + $.extend(settings, options); + } + + /* Cache container as jQuery as object. */ + $container = (settings.container === undefined || + settings.container === window) ? $window : $(settings.container); + + /* Fire one scroll event per scroll. Not one scroll event per image. */ + if (0 === settings.event.indexOf("scroll")) { + $container.bind(settings.event, function() { + return update(); + }); + } + + this.each(function() { + var self = this; + var $self = $(self); + + self.loaded = false; + + /* If no src attribute given use data:uri. */ + if ($self.attr("src") === undefined || $self.attr("src") === false) { + if ($self.is("img")) { + $self.attr("src", settings.placeholder); + } + } + + /* When appear is triggered load original image. */ + $self.one("appear", function() { + if (!this.loaded) { + if (settings.appear) { + var elements_left = elements.length; + settings.appear.call(self, elements_left, settings); + } + $("") + .bind("load", function() { + + var original = $self.attr("data-" + settings.data_attribute); + $self.hide(); + if ($self.is("img")) { + $self.attr("src", original); + } else { + $self.css("background-image", "url('" + original + "')"); + } + $self[settings.effect](settings.effect_speed); + + self.loaded = true; + + /* Remove image from array so it is not looped next time. */ + var temp = $.grep(elements, function(element) { + return !element.loaded; + }); + elements = $(temp); + + if (settings.load) { + var elements_left = elements.length; + settings.load.call(self, elements_left, settings); + } + }) + .attr("src", $self.attr("data-" + settings.data_attribute)); + } + }); + + /* When wanted event is triggered load original image */ + /* by triggering appear. */ + if (0 !== settings.event.indexOf("scroll")) { + $self.bind(settings.event, function() { + if (!self.loaded) { + $self.trigger("appear"); + } + }); + } + }); + + /* Check if something appears when window is resized. */ + $window.bind("resize", function() { + update(); + }); + + /* With IOS5 force loading images when navigating with back button. */ + /* Non optimal workaround. */ + if ((/(?:iphone|ipod|ipad).*os 5/gi).test(navigator.appVersion)) { + $window.bind("pageshow", function(event) { + if (event.originalEvent && event.originalEvent.persisted) { + elements.each(function() { + $(this).trigger("appear"); + }); + } + }); + } + + /* Force initial check if images should appear. */ + $(document).ready(function() { + update(); + }); + + return this; + }; + + /* Convenience methods in jQuery namespace. */ + /* Use as $.belowthefold(element, {threshold : 100, container : window}) */ + + $.belowthefold = function(element, settings) { + var fold; + + if (settings.container === undefined || settings.container === window) { + fold = (window.innerHeight ? window.innerHeight : $window.height()) + $window.scrollTop(); + } else { + fold = $(settings.container).offset().top + $(settings.container).height(); + } + + return fold <= $(element).offset().top - settings.threshold; + }; + + $.rightoffold = function(element, settings) { + var fold; + + if (settings.container === undefined || settings.container === window) { + fold = $window.width() + $window.scrollLeft(); + } else { + fold = $(settings.container).offset().left + $(settings.container).width(); + } + + return fold <= $(element).offset().left - settings.threshold; + }; + + $.abovethetop = function(element, settings) { + var fold; + + if (settings.container === undefined || settings.container === window) { + fold = $window.scrollTop(); + } else { + fold = $(settings.container).offset().top; + } + + return fold >= $(element).offset().top + settings.threshold + $(element).height(); + }; + + $.leftofbegin = function(element, settings) { + var fold; + + if (settings.container === undefined || settings.container === window) { + fold = $window.scrollLeft(); + } else { + fold = $(settings.container).offset().left; + } + + return fold >= $(element).offset().left + settings.threshold + $(element).width(); + }; + + $.inviewport = function(element, settings) { + return !$.rightoffold(element, settings) && !$.leftofbegin(element, settings) && + !$.belowthefold(element, settings) && !$.abovethetop(element, settings); + }; + + /* Custom selectors for your convenience. */ + /* Use as $("img:below-the-fold").something() or */ + /* $("img").filter(":below-the-fold").something() which is faster */ + + $.extend($.expr[":"], { + "below-the-fold" : function(a) { return $.belowthefold(a, {threshold : 0}); }, + "above-the-top" : function(a) { return !$.belowthefold(a, {threshold : 0}); }, + "right-of-screen": function(a) { return $.rightoffold(a, {threshold : 0}); }, + "left-of-screen" : function(a) { return !$.rightoffold(a, {threshold : 0}); }, + "in-viewport" : function(a) { return $.inviewport(a, {threshold : 0}); }, + /* Maintain BC for couple of versions. */ + "above-the-fold" : function(a) { return !$.belowthefold(a, {threshold : 0}); }, + "right-of-fold" : function(a) { return $.rightoffold(a, {threshold : 0}); }, + "left-of-fold" : function(a) { return !$.rightoffold(a, {threshold : 0}); } + }); + +})(jQuery, window, document); \ No newline at end of file diff --git a/sa-token-doc/static/jquery.min.js b/sa-token-doc/static/jquery.min.js new file mode 100644 index 000000000..a1c07fd80 --- /dev/null +++ b/sa-token-doc/static/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 00;n--)if("interactive"===t[n].readyState){e=t[n].src;break}return e||t[i].src}();return e.substring(0,e.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"],getStyle:function(t,i){var n=t.currentStyle?t.currentStyle:e.getComputedStyle(t,null);return n[n.getPropertyValue?"getPropertyValue":"getAttribute"](i)},link:function(t,i,n){if(r.path){var a=document.getElementsByTagName("head")[0],s=document.createElement("link");"string"==typeof i&&(n=i);var l=(n||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,document.getElementById(f)||a.appendChild(s),"function"==typeof i&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(o.getStyle(document.getElementById(f),"width"))?i():setTimeout(u,100))}()}}},r={v:"3.1.1",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):o.link("theme/"+e.extend),this):this},ready:function(e){var t="layer",i="",n=(a?"modules/layer/":"theme/")+"default/layer.css?v="+r.v+i;return a?layui.addcss(n,e,t):o.link(n,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:210},n))}},s=function(e){var t=this;t.index=++r.index,t.config=i.extend({},t.config,o.config,e),document.body?t.creat():setTimeout(function(){t.creat()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim-00","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'
    '+(f?r.title[0]:r.title)+"
    ":"";return r.zIndex=s,t([r.shade?'
    ':"",'
    '+(e&&2!=r.type?"":u)+'
    '+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
    '+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
    '+e+"
    "}():"")+(r.resize?'':"")+"
    "],u,i('
    ')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content||"http://layer.layui.com","auto"];t.content='';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}if(e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),i("#layui-layer-shade"+e.index).css({"background-color":t.shade[1]||"#000",opacity:t.shade[0]||t.shade}),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]){var u="layer-anim "+l.anim[t.anim];e.layero.addClass(u).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){i(this).removeClass(u)})}t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){var t=this,a=t.config,o=i("#"+l[0]+e);""===a.area[0]&&a.maxWidth>0&&(r.ie&&r.ie<8&&a.btn&&o.width(o.innerWidth()),o.outerWidth()>a.maxWidth&&o.width(a.maxWidth));var s=[o.innerWidth(),o.innerHeight()],f=o.find(l[1]).outerHeight()||0,c=o.find("."+l[6]).outerHeight()||0,u=function(e){e=o.find(e),e.height(s[1]-f-c-2*(0|parseFloat(e.css("padding-top"))))};switch(a.type){case 2:u("iframe");break;default:""===a.area[1]?a.maxHeight>0&&o.outerHeight()>a.maxHeight?(s[1]=a.maxHeight,u("."+l[5])):a.fixed&&s[1]>=n.height()&&(s[1]=n.height(),u("."+l[5])):u("."+l[5])}return t},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;af&&(a=f),ou&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass("layer-anim "+a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}},r.closeAll=function(e){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'":function(){return''}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(e){s=e.find(".layui-layer-input"),s.focus(),"function"==typeof f&&f(e)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n="layui-this",a=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,a="";if(e>0)for(a=''+t[0].title+"";i"+t[i].title+"";return a}(),content:'
      '+function(){var e=t.length,i=1,a="";if(e>0)for(a='
    • '+(t[0].content||"no content")+"
    • ";i'+(t[i].content||"no content")+"";return a}()+"
    ",success:function(t){var o=t.find(".layui-layer-title").children(),r=t.find(".layui-layer-tabmain").children();o.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var a=i(this),o=a.index();a.addClass(n).siblings().removeClass(n),r.eq(o).show().siblings().hide(),"function"==typeof e.change&&e.change(o)}),"function"==typeof a&&a(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]'+(u[d].alt||
    '+(u.length>1?'':"")+'
    '+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
    ",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常
    是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.$),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window); \ No newline at end of file diff --git a/sa-token-doc/static/layer-v3.1.1/mobile/layer.js b/sa-token-doc/static/layer-v3.1.1/mobile/layer.js new file mode 100644 index 000000000..f9cf69313 --- /dev/null +++ b/sa-token-doc/static/layer-v3.1.1/mobile/layer.js @@ -0,0 +1,2 @@ +/*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */ + ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?'

    '+(e?n.title[0]:n.title)+"

    ":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),'
    '+e+"
    "):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content='

    '+(n.content||"")+"

    "),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"
    ':"")+'
    "+l+'
    '+n.content+"
    "+c+"
    ",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;odiv{line-height:22px;padding-top:7px;margin-bottom:20px;font-size:14px}.layui-m-layerbtn{display:box;display:-moz-box;display:-webkit-box;width:100%;height:50px;line-height:50px;font-size:0;border-top:1px solid #D0D0D0;background-color:#F2F2F2}.layui-m-layerbtn span{display:block;-moz-box-flex:1;box-flex:1;-webkit-box-flex:1;font-size:14px;cursor:pointer}.layui-m-layerbtn span[yes]{color:#40AFFE}.layui-m-layerbtn span[no]{border-right:1px solid #D0D0D0;border-radius:0 0 0 5px}.layui-m-layerbtn span:active{background-color:#F6F6F6}.layui-m-layerend{position:absolute;right:7px;top:10px;width:30px;height:30px;border:0;font-weight:400;background:0 0;cursor:pointer;-webkit-appearance:none;font-size:30px}.layui-m-layerend::after,.layui-m-layerend::before{position:absolute;left:5px;top:15px;content:'';width:18px;height:1px;background-color:#999;transform:rotate(45deg);-webkit-transform:rotate(45deg);border-radius:3px}.layui-m-layerend::after{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}body .layui-m-layer .layui-m-layer-footer{position:fixed;width:95%;max-width:100%;margin:0 auto;left:0;right:0;bottom:10px;background:0 0}.layui-m-layer-footer .layui-m-layercont{padding:20px;border-radius:5px 5px 0 0;background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn{display:block;height:auto;background:0 0;border-top:none}.layui-m-layer-footer .layui-m-layerbtn span{background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn span[no]{color:#FD482C;border-top:1px solid #c2c2c2;border-radius:0 0 5px 5px}.layui-m-layer-footer .layui-m-layerbtn span[yes]{margin-top:10px;border-radius:5px}body .layui-m-layer .layui-m-layer-msg{width:auto;max-width:90%;margin:0 auto;bottom:-150px;background-color:rgba(0,0,0,.7);color:#fff}.layui-m-layer-msg .layui-m-layercont{padding:10px 20px} \ No newline at end of file diff --git a/sa-token-doc/static/layer-v3.1.1/theme/default/icon-ext.png b/sa-token-doc/static/layer-v3.1.1/theme/default/icon-ext.png new file mode 100644 index 000000000..bbbb669bb Binary files /dev/null and b/sa-token-doc/static/layer-v3.1.1/theme/default/icon-ext.png differ diff --git a/sa-token-doc/static/layer-v3.1.1/theme/default/icon.png b/sa-token-doc/static/layer-v3.1.1/theme/default/icon.png new file mode 100644 index 000000000..3e17da8b1 Binary files /dev/null and b/sa-token-doc/static/layer-v3.1.1/theme/default/icon.png differ diff --git a/sa-token-doc/static/layer-v3.1.1/theme/default/layer.css b/sa-token-doc/static/layer-v3.1.1/theme/default/layer.css new file mode 100644 index 000000000..820b4a99b --- /dev/null +++ b/sa-token-doc/static/layer-v3.1.1/theme/default/layer.css @@ -0,0 +1 @@ +.layui-layer-imgbar,.layui-layer-imgtit a,.layui-layer-tab .layui-layer-title span,.layui-layer-title{text-overflow:ellipsis;white-space:nowrap}html #layuicss-layer{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch;top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;border-radius:2px;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #B2B2B2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-load{background:url(loading-1.gif) center center no-repeat #eee}.layui-layer-ico{background:url(icon.png) no-repeat}.layui-layer-btn a,.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a{display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layer-anim{-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-00{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.layui-layer-title{padding:0 80px 0 20px;height:42px;line-height:42px;border-bottom:1px solid #eee;font-size:14px;color:#333;overflow:hidden;background-color:#F8F8F8;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:15px;font-size:0;line-height:initial}.layui-layer-setwin a{position:relative;width:16px;height:16px;margin-left:10px;font-size:12px;_overflow:hidden}.layui-layer-setwin .layui-layer-min cite{position:absolute;width:14px;height:2px;left:0;top:50%;margin-top:-1px;background-color:#2E2D3C;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2D93CA}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{background-position:1px -40px;cursor:pointer}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;width:30px;height:30px;margin-left:0;background-position:-149px -31px;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;padding:0 15px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:28px;line-height:28px;margin:5px 5px 0;padding:0 15px;border:1px solid #dedede;background-color:#fff;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:#1E9FFF;background-color:#1E9FFF;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:260px}.layui-layer-dialog .layui-layer-content{position:relative;padding:20px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{position:absolute;top:16px;left:15px;_left:-40px;width:30px;height:30px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{border:6px solid #8D8D8D;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #D3D4D3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-content{padding:12px 25px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:20px 20px 20px 55px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:60px;height:24px;background:url(loading-0.gif) no-repeat}.layui-layer-loading .layui-layer-loading1{width:37px;height:37px;background:url(loading-1.gif) no-repeat}.layui-layer-ico16,.layui-layer-loading .layui-layer-loading2{width:32px;height:32px;background:url(loading-2.gif) no-repeat}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:8px 15px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:5px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{background:#4476A7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;text-align:right;border-top:1px solid #E9E7E7}.layui-layer-lan .layui-layer-btn a{background:#fff;border-color:#E9E7E7;color:#333}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#C9C5C5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95;border-color:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92B8B1}.layui-layer-iconext{background:url(icon-ext.png) no-repeat}.layui-layer-prompt .layui-layer-input{display:block;width:230px;height:36px;margin:0 auto;line-height:30px;padding-left:10px;border:1px solid #e6e6e6;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px;padding:6px 10px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;float:left;min-width:80px;max-width:260px;padding:0 20px;text-align:center;overflow:hidden;cursor:pointer}.layui-layer-tab .layui-layer-title span.layui-this{height:43px;border-left:1px solid #eee;border-right:1px solid #eee;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.layui-this{display:block}.layui-layer-photos{-webkit-animation-duration:.8s;animation-duration:.8s}.layui-layer-photos .layui-layer-content{overflow:hidden;text-align:center}.layui-layer-photos .layui-layer-phimg img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-imgbar,.layui-layer-imguide{display:none}.layui-layer-imgnext,.layui-layer-imgprev{position:absolute;top:50%;width:27px;_width:44px;height:44px;margin-top:-22px;outline:0;blr:expression(this.onFocus=this.blur())}.layui-layer-imgprev{left:10px;background-position:-5px -5px;_background-position:-70px -5px}.layui-layer-imgprev:hover{background-position:-33px -5px;_background-position:-120px -5px}.layui-layer-imgnext{right:10px;_right:8px;background-position:-5px -50px;_background-position:-70px -50px}.layui-layer-imgnext:hover{background-position:-33px -50px;_background-position:-120px -50px}.layui-layer-imgbar{position:absolute;left:0;bottom:0;width:100%;height:32px;line-height:32px;background-color:rgba(0,0,0,.8);background-color:#000\9;filter:Alpha(opacity=80);color:#fff;overflow:hidden;font-size:0}.layui-layer-imgtit *{display:inline-block;*display:inline;*zoom:1;vertical-align:top;font-size:12px}.layui-layer-imgtit a{max-width:65%;overflow:hidden;color:#fff}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} \ No newline at end of file diff --git a/sa-token-doc/static/layer-v3.1.1/theme/default/loading-0.gif b/sa-token-doc/static/layer-v3.1.1/theme/default/loading-0.gif new file mode 100644 index 000000000..6f3c9539a Binary files /dev/null and b/sa-token-doc/static/layer-v3.1.1/theme/default/loading-0.gif differ diff --git a/sa-token-doc/static/layer-v3.1.1/theme/default/loading-1.gif b/sa-token-doc/static/layer-v3.1.1/theme/default/loading-1.gif new file mode 100644 index 000000000..db3a483e4 Binary files /dev/null and b/sa-token-doc/static/layer-v3.1.1/theme/default/loading-1.gif differ diff --git a/sa-token-doc/static/layer-v3.1.1/theme/default/loading-2.gif b/sa-token-doc/static/layer-v3.1.1/theme/default/loading-2.gif new file mode 100644 index 000000000..5bb90fd6a Binary files /dev/null and b/sa-token-doc/static/layer-v3.1.1/theme/default/loading-2.gif differ diff --git a/sa-token-doc/static/prism-java.min.js b/sa-token-doc/static/prism-java.min.js new file mode 100644 index 000000000..079c8d4e7 --- /dev/null +++ b/sa-token-doc/static/prism-java.min.js @@ -0,0 +1 @@ +!function(e){var t=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|null|open|opens|package|private|protected|provides|public|record|requires|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,a=/\b[A-Z](?:\w*[a-z]\w*)?\b/;e.languages.java=e.languages.extend("clike",{"class-name":[a,/\b[A-Z]\w*(?=\s+\w+\s*[;,=())])/],keyword:t,function:[e.languages.clike.function,{pattern:/(\:\:)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x[\da-f_]*\.?[\da-f_p+-]+\b|(?:\b\d[\d_]*\.?[\d_]*|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0}}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"}}),e.languages.insertBefore("java","class-name",{annotation:{alias:"punctuation",pattern:/(^|[^.])@\w+/,lookbehind:!0},namespace:{pattern:/(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)[a-z]\w*(?:\.[a-z]\w*)+/,lookbehind:!0,inside:{punctuation:/\./}},generics:{pattern:/<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<[\w\s,.&?]*>)*>)*>)*>/,inside:{"class-name":a,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}}})}(Prism); \ No newline at end of file diff --git a/sa-token-doc/static/prism/prism-gradle.min.js b/sa-token-doc/static/prism/prism-gradle.min.js new file mode 100644 index 000000000..96aae9aa0 --- /dev/null +++ b/sa-token-doc/static/prism/prism-gradle.min.js @@ -0,0 +1 @@ +!function(e){var n={pattern:/((?:^|[^\\$])(?:\\{2})*)\$(?:\w+|\{[^{}]*\})/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{?|\}$/,alias:"punctuation"},expression:{pattern:/[\s\S]+/,inside:null}}};e.languages.gradle=e.languages.extend("clike",{string:{pattern:/'''(?:[^\\]|\\[\s\S])*?'''|'(?:\\.|[^\\'\r\n])*'/,greedy:!0},keyword:/\b(?:apply|def|dependencies|else|if|implementation|import|plugin|plugins|project|repositories|repository|sourceSets|tasks|val)\b/,number:/\b(?:0b[01_]+|0x[\da-f_]+(?:\.[\da-f_p\-]+)?|[\d_]+(?:\.[\d_]+)?(?:e[+-]?\d+)?)[glidf]?\b/i,operator:{pattern:/(^|[^.])(?:~|==?~?|\?[.:]?|\*(?:[.=]|\*=?)?|\.[@&]|\.\.<|\.\.(?!\.)|-[-=>]?|\+[+=]?|!=?|<(?:<=?|=>?)?|>(?:>>?=?|=)?|&[&=]?|\|[|=]?|\/=?|\^=?|%=?)/,lookbehind:!0},punctuation:/\.+|[{}[\];(),:$]/}),e.languages.insertBefore("gradle","string",{shebang:{pattern:/#!.+/,alias:"comment",greedy:!0},"interpolation-string":{pattern:/"""(?:[^\\]|\\[\s\S])*?"""|(["/])(?:\\.|(?!\1)[^\\\r\n])*\1|\$\/(?:[^/$]|\$(?:[/$]|(?![/$]))|\/(?!\$))*\/\$/,greedy:!0,inside:{interpolation:n,string:/[\s\S]+/}}}),e.languages.insertBefore("gradle","punctuation",{"spock-block":/\b(?:and|cleanup|expect|given|setup|then|when|where):/}),e.languages.insertBefore("gradle","function",{annotation:{pattern:/(^|[^.])@\w+/,lookbehind:!0,alias:"punctuation"}}),n.inside.expression.inside=e.languages.gradle}(Prism); \ No newline at end of file diff --git a/sa-token-doc/static/prism/prism-java.min.js b/sa-token-doc/static/prism/prism-java.min.js new file mode 100644 index 000000000..9b90cf3cc --- /dev/null +++ b/sa-token-doc/static/prism/prism-java.min.js @@ -0,0 +1 @@ +!function(e){var n=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,t="(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*",s={pattern:RegExp("(^|[^\\w.])"+t+"[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"/,lookbehind:!0,greedy:!0},"class-name":[s,{pattern:RegExp("(^|[^\\w.])"+t+"[A-Z]\\w*(?=\\s+\\w+\\s*[;,=()]|\\s*(?:\\[[\\s,]*\\]\\s*)?::\\s*new\\b)"),lookbehind:!0,inside:s.inside},{pattern:RegExp("(\\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\\s+)"+t+"[A-Z]\\w*\\b"),lookbehind:!0,inside:s.inside}],keyword:n,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\b[A-Z][A-Z_\d]+\b/}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"},char:{pattern:/'(?:\\.|[^'\\\r\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":s,keyword:n,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp("(\\bimport\\s+)"+t+"(?:[A-Z]\\w*|\\*)(?=\\s*;)"),lookbehind:!0,inside:{namespace:s.inside.namespace,punctuation:/\./,operator:/\*/,"class-name":/\w+/}},{pattern:RegExp("(\\bimport\\s+static\\s+)"+t+"(?:\\w+|\\*)(?=\\s*;)"),lookbehind:!0,alias:"static",inside:{namespace:s.inside.namespace,static:/\b\w+$/,punctuation:/\./,operator:/\*/,"class-name":/\w+/}}],namespace:{pattern:RegExp("(\\b(?:exports|import(?:\\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\\s+)(?!)[a-z]\\w*(?:\\.[a-z]\\w*)*\\.?".replace(//g,(function(){return n.source}))),lookbehind:!0,inside:{punctuation:/\./}}})}(Prism); \ No newline at end of file diff --git a/sa-token-doc/static/prism/prism-properties.min.js b/sa-token-doc/static/prism/prism-properties.min.js new file mode 100644 index 000000000..5976d4765 --- /dev/null +++ b/sa-token-doc/static/prism/prism-properties.min.js @@ -0,0 +1 @@ +Prism.languages.properties={comment:/^[ \t]*[#!].*$/m,value:{pattern:/(^[ \t]*(?:\\(?:\r\n|[\s\S])|[^\\\s:=])+(?: *[=:] *(?! )| ))(?:\\(?:\r\n|[\s\S])|[^\\\r\n])+/m,lookbehind:!0,alias:"attr-value"},key:{pattern:/^[ \t]*(?:\\(?:\r\n|[\s\S])|[^\\\s:=])+(?= *[=:]| )/m,alias:"attr-name"},punctuation:/[=:]/}; \ No newline at end of file diff --git a/sa-token-doc/static/prism/prism-yaml.min.js b/sa-token-doc/static/prism/prism-yaml.min.js new file mode 100644 index 000000000..5cd16d3d7 --- /dev/null +++ b/sa-token-doc/static/prism/prism-yaml.min.js @@ -0,0 +1 @@ +!function(e){var n=/[*&][^\s[\]{},]+/,r=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,t="(?:"+r.source+"(?:[ \t]+"+n.source+")?|"+n.source+"(?:[ \t]+"+r.source+")?)",a="(?:[^\\s\\x00-\\x08\\x0e-\\x1f!\"#%&'*,\\-:>?@[\\]`{|}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*".replace(//g,(function(){return"[^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]"})),d="\"(?:[^\"\\\\\r\n]|\\\\.)*\"|'(?:[^'\\\\\r\n]|\\\\.)*'";function o(e,n){n=(n||"").replace(/m/g,"")+"m";var r="([:\\-,[{]\\s*(?:\\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\\]|\\}|(?:[\r\n]\\s*)?#))".replace(/<>/g,(function(){return t})).replace(/<>/g,(function(){return e}));return RegExp(r,n)}e.languages.yaml={scalar:{pattern:RegExp("([\\-:]\\s*(?:\\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\\S[^\r\n]*(?:\\2[^\r\n]+)*)".replace(/<>/g,(function(){return t}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp("((?:^|[:\\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\\s*:\\s)".replace(/<>/g,(function(){return t})).replace(/<>/g,(function(){return"(?:"+a+"|"+d+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:o("\\d{4}-\\d\\d?-\\d\\d?(?:[tT]|[ \t]+)\\d\\d?:\\d{2}:\\d{2}(?:\\.\\d*)?(?:[ \t]*(?:Z|[-+]\\d\\d?(?::\\d{2})?))?|\\d{4}-\\d{2}-\\d{2}|\\d\\d?:\\d{2}(?::\\d{2}(?:\\.\\d*)?)?"),lookbehind:!0,alias:"number"},boolean:{pattern:o("false|true","i"),lookbehind:!0,alias:"important"},null:{pattern:o("null|~","i"),lookbehind:!0,alias:"important"},string:{pattern:o(d),lookbehind:!0,greedy:!0},number:{pattern:o("[+-]?(?:0x[\\da-f]+|0o[0-7]+|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?|\\.inf|\\.nan)","i"),lookbehind:!0},tag:r,important:n,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(Prism); \ No newline at end of file diff --git a/sa-token-doc/static/sa-translate.js b/sa-token-doc/static/sa-translate.js new file mode 100644 index 000000000..409a34aa7 --- /dev/null +++ b/sa-token-doc/static/sa-translate.js @@ -0,0 +1,89 @@ +/** + * Sa-Token 多语言切换(基于 translate.js) + * + * 用法: + * 1. 页面放容器: + * 2. 引入 translate.js + 本文件 + * 3. 文档页:挂到 Docsify plugins → saTranslatePlugin + * 4. 首页等普通页:直接调 initSaTranslate() + * + * 官方文档:https://translate.zvo.cn + * 仓库:https://gitee.com/mail_osc/translate + */ +(function (global) { + 'use strict'; + + var inited = false; + + /** 初始化翻译配置,并执行第一次翻译 */ + function initSaTranslate() { + if (inited) { + return; + } + if (!global.translate) { + console.warn('[sa-translate] 未加载 translate.js,跳过多语言初始化'); + return; + } + inited = true; + + var translate = global.translate; + + // 当前网页原文语种 + translate.language.setLocal('chinese_simplified'); + + // 免费通道:走浏览器端 Edge 翻译(无需 API Key) + translate.service.use('client.edge'); + + // 顶部下拉只展示这几种语言(可按需增减) + // 语种 ID 见:http://translate.zvo.cn/support_language.html + // 德语官方码是 deutsch(不是 german) + translate.selectLanguageTag.languages = [ + 'chinese_simplified', + 'chinese_traditional', + 'english', + 'japanese', + 'korean', + 'russian', + 'vietnamese', + 'spanish', + 'deutsch', + 'french', + 'indonesian' + ].join(','); + + // 代码别被翻坏:忽略 code / pre + translate.ignore.tag.push('code'); + translate.ignore.tag.push('pre'); + + // 动态改 DOM 时也能跟上(Docsify 切页、首页轮播文案等) + translate.listener.start(); + + // 开始翻译(会同时在 #translate 里生成 select) + // 样式靠 CSS 选择器 #translateSelectLanguage,切换语种重绘也不丢 + translate.execute(); + } + + /** + * Docsify 插件: + * - ready:文档框架就绪后初始化 + * - doneEach:每次切换章节后再翻一遍,避免新内容漏翻 + */ + function saTranslatePlugin(hook) { + if (!global.translate) { + console.warn('[sa-translate] 未加载 translate.js,跳过多语言初始化'); + return; + } + + hook.ready(function () { + initSaTranslate(); + }); + hook.doneEach(function () { + if (inited) { + global.translate.execute(); + } + }); + } + + global.initSaTranslate = initSaTranslate; + global.saTranslatePlugin = saTranslatePlugin; +})(typeof window !== 'undefined' ? window : this); diff --git a/sa-token-doc/static/search-icon.svg b/sa-token-doc/static/search-icon.svg new file mode 100644 index 000000000..03d83913e --- /dev/null +++ b/sa-token-doc/static/search-icon.svg @@ -0,0 +1 @@ + diff --git a/sa-token-doc/static/search.min.js b/sa-token-doc/static/search.min.js new file mode 100644 index 000000000..74f7fbc5c --- /dev/null +++ b/sa-token-doc/static/search.min.js @@ -0,0 +1 @@ +!function(){var u={},m={EXPIRE_KEY:"docsify.search.expires",INDEX_KEY:"docsify.search.index"};function p(e){var n={"&":"&","<":"<",">":">",'"':""","'":"'"};return String(e).replace(/[&<>"']/g,function(e){return n[e]})}function h(e){return e.text||"table"!==e.type||(e.cells.unshift(e.header),e.text=e.cells.map(function(e){return e.join(" | ")}).join(" |\n ")),e.text}function f(e){return e.text||"list"!==e.type||(e.text=e.raw),e.text}function g(i,e,r,o){void 0===e&&(e="");var s,e=window.marked.lexer(e),c=window.Docsify.slugify,d={},l="";return e.forEach(function(e,n){if("heading"===e.type&&e.depth<=o){var t=function(e){void 0===e&&(e="");var a={};return{str:e=e&&e.replace(/^('|")/,"").replace(/('|")$/,"").replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g,function(e,n,t){return-1===n.indexOf(":")?(a[n]=t&&t.replace(/"/g,"")||!0,""):e}).trim(),config:a}}(e.text),a=t.str,t=t.config;s=t.id?r.toURL(i,{id:c(t.id)}):r.toURL(i,{id:c(p(e.text))}),a&&(l=a.replace(//,"").replace(/{docsify-ignore}/,"").replace(//,"").replace(/{docsify-ignore-all}/,"").trim()),d[s]={slug:s,title:l,body:""}}else{if(0===n&&(s=r.toURL(i),d[s]={slug:s,title:"/"!==i?i.slice(1):"Home Page",body:e.text||""}),!s)return;d[s]?d[s].body?(e.text=h(e),e.text=f(e),d[s].body+="\n"+(e.text||"")):(e.text=h(e),e.text=f(e),d[s].body=d[s].body?d[s].body+e.text:e.text):d[s]={slug:s,title:"",body:""}}}),c.clear(),d}function y(e){return e&&e.normalize?e.normalize("NFD").replace(/[\u0300-\u036f]/g,""):e}function r(i,r){var t,a,n,e,o="auto"===i.paths,s=o?(t=r.router,a=[],Docsify.dom.findAll(".sidebar-nav a:not(.section-link):not([data-nosearch])").forEach(function(e){var n=e.href,e=e.getAttribute("href"),n=t.parse(n).path;n&&-1===a.indexOf(n)&&!Docsify.util.isAbsolutePath(e)&&a.push(n)}),a):i.paths,c="";s.length&&o&&i.pathNamespaces?(n=s[0],Array.isArray(i.pathNamespaces)?c=i.pathNamespaces.filter(function(e){return n.slice(0,e.length)===e})[0]||c:i.pathNamespaces instanceof RegExp&&((d=n.match(i.pathNamespaces))&&(c=d[0])),e=-1===s.indexOf(c+"/"),d=-1===s.indexOf(c+"/README"),e&&d&&s.unshift(c+"/")):-1===s.indexOf("/")&&-1===s.indexOf("/README")&&s.unshift("/");var d,l=((d=i.namespace)?m.EXPIRE_KEY+"/"+d:m.EXPIRE_KEY)+c,p=((d=i.namespace)?m.INDEX_KEY+"/"+d:m.INDEX_KEY)+c,c=localStorage.getItem(l)l.length&&(a=l.length),t="..."+c.substring(n,a).replace(t,function(e){return''+e+""})+"...",o+=t)}),0\n

    '+e.title+"

    \n

    "+e.content+"

    \n\n
    "}),t.classList.add("show"),a.classList.add("show"),t.innerHTML=r||'

    '+s+"

    ",o.hideOtherSidebarContent&&(i&&i.classList.add("hide"),n&&n.classList.add("hide"))}function d(e){o=e}function l(e,n){var t,a,i=n.router.parse().query.s;d(e),Docsify.dom.style("\n.sidebar {\n padding-top: 0;\n}\n\n.search {\n margin-bottom: 20px;\n padding: 6px;\n border-bottom: 1px solid #eee;\n}\n\n.search .input-wrap {\n display: flex;\n align-items: center;\n}\n\n.search .results-panel {\n display: none;\n}\n\n.search .results-panel.show {\n display: block;\n}\n\n.search input {\n outline: none;\n border: none;\n width: 100%;\n padding: 0 7px;\n line-height: 36px;\n font-size: 14px;\n border: 1px solid transparent;\n}\n\n.search input:focus {\n box-shadow: 0 0 5px var(--theme-color, #42b983);\n border: 1px solid var(--theme-color, #42b983);\n}\n\n.search input::-webkit-search-decoration,\n.search input::-webkit-search-cancel-button,\n.search input {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.search .clear-button {\n cursor: pointer;\n width: 36px;\n text-align: right;\n display: none;\n}\n\n.search .clear-button.show {\n display: block;\n}\n\n.search .clear-button svg {\n transform: scale(.5);\n}\n\n.search h2 {\n font-size: 17px;\n margin: 10px 0;\n}\n\n.search a {\n text-decoration: none;\n color: inherit;\n}\n\n.search .matching-post {\n border-bottom: 1px solid #eee;\n}\n\n.search .matching-post:last-child {\n border-bottom: 0;\n}\n\n.search p {\n font-size: 14px;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.search p.empty {\n text-align: center;\n}\n\n.app-name.hide, .sidebar-nav.hide {\n display: none;\n}"),function(e){void 0===e&&(e="");var n='
    \n \n
    \n \n \n \n \n \n
    \n
    \n
    \n
    ',e=Docsify.dom.create("div",n),n=Docsify.dom.find("aside");Docsify.dom.toggleClass(e,"search"),Docsify.dom.before(n,e)}(i),n=Docsify.dom.find("div.search"),a=Docsify.dom.find(n,"input"),e=Docsify.dom.find(n,".input-wrap"),Docsify.dom.on(n,"click",function(e){return-1===["A","H2","P","EM"].indexOf(e.target.tagName)&&e.stopPropagation()}),Docsify.dom.on(a,"input",function(n){clearTimeout(t),t=setTimeout(function(e){return c(n.target.value.trim())},100)}),Docsify.dom.on(e,"click",function(e){"INPUT"!==e.target.tagName&&(a.value="",c())}),i&&setTimeout(function(e){return c(i)},500)}function v(e,n){var t,a,i,r,o;d(e),t=e.placeholder,a=n.route.path,(r=Docsify.dom.getNode('.search input[type="search"]'))&&("string"==typeof t?r.placeholder=t:(i=Object.keys(t).filter(function(e){return-1' + (index + 1) + ''; + }, + }, + // 左右切换按钮 + navigation: { + nextEl: ".swiper-button-next", + prevEl: ".swiper-button-prev", + }, + }); +} + +$(function(){ + initSwiper(); +}) + +// 滚动到 swiper 时,再加载 +// $(document).scroll(function(){ +// // 页面滚动条高度 > ry盒子到顶部距离 + window 视口高度 时,swiper出现 +// if($(document).scrollTop() > $('.ry-kuai').offset().top - $(window).height()) { +// initSwiper(); +// } +// }) \ No newline at end of file diff --git a/sa-token-doc/static/swiper/swiper-bundle.min.css b/sa-token-doc/static/swiper/swiper-bundle.min.css new file mode 100644 index 000000000..22103c80e --- /dev/null +++ b/sa-token-doc/static/swiper/swiper-bundle.min.css @@ -0,0 +1,13 @@ +/** + * Swiper 6.8.4 + * Most modern mobile touch slider and framework with hardware accelerated transitions + * https://swiperjs.com + * + * Copyright 2014-2021 Vladimir Kharlampidi + * + * Released under the MIT License + * + * Released on: August 23, 2021 + */ + +@font-face{font-family:swiper-icons;src:url('data:application/font-woff;charset=utf-8;base64, d09GRgABAAAAAAZgABAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAGRAAAABoAAAAci6qHkUdERUYAAAWgAAAAIwAAACQAYABXR1BPUwAABhQAAAAuAAAANuAY7+xHU1VCAAAFxAAAAFAAAABm2fPczU9TLzIAAAHcAAAASgAAAGBP9V5RY21hcAAAAkQAAACIAAABYt6F0cBjdnQgAAACzAAAAAQAAAAEABEBRGdhc3AAAAWYAAAACAAAAAj//wADZ2x5ZgAAAywAAADMAAAD2MHtryVoZWFkAAABbAAAADAAAAA2E2+eoWhoZWEAAAGcAAAAHwAAACQC9gDzaG10eAAAAigAAAAZAAAArgJkABFsb2NhAAAC0AAAAFoAAABaFQAUGG1heHAAAAG8AAAAHwAAACAAcABAbmFtZQAAA/gAAAE5AAACXvFdBwlwb3N0AAAFNAAAAGIAAACE5s74hXjaY2BkYGAAYpf5Hu/j+W2+MnAzMYDAzaX6QjD6/4//Bxj5GA8AuRwMYGkAPywL13jaY2BkYGA88P8Agx4j+/8fQDYfA1AEBWgDAIB2BOoAeNpjYGRgYNBh4GdgYgABEMnIABJzYNADCQAACWgAsQB42mNgYfzCOIGBlYGB0YcxjYGBwR1Kf2WQZGhhYGBiYGVmgAFGBiQQkOaawtDAoMBQxXjg/wEGPcYDDA4wNUA2CCgwsAAAO4EL6gAAeNpj2M0gyAACqxgGNWBkZ2D4/wMA+xkDdgAAAHjaY2BgYGaAYBkGRgYQiAHyGMF8FgYHIM3DwMHABGQrMOgyWDLEM1T9/w8UBfEMgLzE////P/5//f/V/xv+r4eaAAeMbAxwIUYmIMHEgKYAYjUcsDAwsLKxc3BycfPw8jEQA/gZBASFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTQZBgMAAMR+E+gAEQFEAAAAKgAqACoANAA+AEgAUgBcAGYAcAB6AIQAjgCYAKIArAC2AMAAygDUAN4A6ADyAPwBBgEQARoBJAEuATgBQgFMAVYBYAFqAXQBfgGIAZIBnAGmAbIBzgHsAAB42u2NMQ6CUAyGW568x9AneYYgm4MJbhKFaExIOAVX8ApewSt4Bic4AfeAid3VOBixDxfPYEza5O+Xfi04YADggiUIULCuEJK8VhO4bSvpdnktHI5QCYtdi2sl8ZnXaHlqUrNKzdKcT8cjlq+rwZSvIVczNiezsfnP/uznmfPFBNODM2K7MTQ45YEAZqGP81AmGGcF3iPqOop0r1SPTaTbVkfUe4HXj97wYE+yNwWYxwWu4v1ugWHgo3S1XdZEVqWM7ET0cfnLGxWfkgR42o2PvWrDMBSFj/IHLaF0zKjRgdiVMwScNRAoWUoH78Y2icB/yIY09An6AH2Bdu/UB+yxopYshQiEvnvu0dURgDt8QeC8PDw7Fpji3fEA4z/PEJ6YOB5hKh4dj3EvXhxPqH/SKUY3rJ7srZ4FZnh1PMAtPhwP6fl2PMJMPDgeQ4rY8YT6Gzao0eAEA409DuggmTnFnOcSCiEiLMgxCiTI6Cq5DZUd3Qmp10vO0LaLTd2cjN4fOumlc7lUYbSQcZFkutRG7g6JKZKy0RmdLY680CDnEJ+UMkpFFe1RN7nxdVpXrC4aTtnaurOnYercZg2YVmLN/d/gczfEimrE/fs/bOuq29Zmn8tloORaXgZgGa78yO9/cnXm2BpaGvq25Dv9S4E9+5SIc9PqupJKhYFSSl47+Qcr1mYNAAAAeNptw0cKwkAAAMDZJA8Q7OUJvkLsPfZ6zFVERPy8qHh2YER+3i/BP83vIBLLySsoKimrqKqpa2hp6+jq6RsYGhmbmJqZSy0sraxtbO3sHRydnEMU4uR6yx7JJXveP7WrDycAAAAAAAH//wACeNpjYGRgYOABYhkgZgJCZgZNBkYGLQZtIJsFLMYAAAw3ALgAeNolizEKgDAQBCchRbC2sFER0YD6qVQiBCv/H9ezGI6Z5XBAw8CBK/m5iQQVauVbXLnOrMZv2oLdKFa8Pjuru2hJzGabmOSLzNMzvutpB3N42mNgZGBg4GKQYzBhYMxJLMlj4GBgAYow/P/PAJJhLM6sSoWKfWCAAwDAjgbRAAB42mNgYGBkAIIbCZo5IPrmUn0hGA0AO8EFTQAA') format('woff');font-weight:400;font-style:normal}:root{--swiper-theme-color:#007aff}.swiper-container{margin-left:auto;margin-right:auto;position:relative;overflow:hidden;list-style:none;padding:0;z-index:1}.swiper-container-vertical>.swiper-wrapper{flex-direction:column}.swiper-wrapper{position:relative;width:100%;height:100%;z-index:1;display:flex;transition-property:transform;box-sizing:content-box}.swiper-container-android .swiper-slide,.swiper-wrapper{transform:translate3d(0px,0,0)}.swiper-container-multirow>.swiper-wrapper{flex-wrap:wrap}.swiper-container-multirow-column>.swiper-wrapper{flex-wrap:wrap;flex-direction:column}.swiper-container-free-mode>.swiper-wrapper{transition-timing-function:ease-out;margin:0 auto}.swiper-container-pointer-events{touch-action:pan-y}.swiper-container-pointer-events.swiper-container-vertical{touch-action:pan-x}.swiper-slide{flex-shrink:0;width:100%;height:100%;position:relative;transition-property:transform}.swiper-slide-invisible-blank{visibility:hidden}.swiper-container-autoheight,.swiper-container-autoheight .swiper-slide{height:auto}.swiper-container-autoheight .swiper-wrapper{align-items:flex-start;transition-property:transform,height}.swiper-container-3d{perspective:1200px}.swiper-container-3d .swiper-cube-shadow,.swiper-container-3d .swiper-slide,.swiper-container-3d .swiper-slide-shadow-bottom,.swiper-container-3d .swiper-slide-shadow-left,.swiper-container-3d .swiper-slide-shadow-right,.swiper-container-3d .swiper-slide-shadow-top,.swiper-container-3d .swiper-wrapper{transform-style:preserve-3d}.swiper-container-3d .swiper-slide-shadow-bottom,.swiper-container-3d .swiper-slide-shadow-left,.swiper-container-3d .swiper-slide-shadow-right,.swiper-container-3d .swiper-slide-shadow-top{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:10}.swiper-container-3d .swiper-slide-shadow-left{background-image:linear-gradient(to left,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-container-3d .swiper-slide-shadow-right{background-image:linear-gradient(to right,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-container-3d .swiper-slide-shadow-top{background-image:linear-gradient(to top,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-container-3d .swiper-slide-shadow-bottom{background-image:linear-gradient(to bottom,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-container-css-mode>.swiper-wrapper{overflow:auto;scrollbar-width:none;-ms-overflow-style:none}.swiper-container-css-mode>.swiper-wrapper::-webkit-scrollbar{display:none}.swiper-container-css-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:start start}.swiper-container-horizontal.swiper-container-css-mode>.swiper-wrapper{scroll-snap-type:x mandatory}.swiper-container-vertical.swiper-container-css-mode>.swiper-wrapper{scroll-snap-type:y mandatory}:root{--swiper-navigation-size:44px}.swiper-button-next,.swiper-button-prev{position:absolute;top:50%;width:calc(var(--swiper-navigation-size)/ 44 * 27);height:var(--swiper-navigation-size);margin-top:calc(0px - (var(--swiper-navigation-size)/ 2));z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color,var(--swiper-theme-color))}.swiper-button-next.swiper-button-disabled,.swiper-button-prev.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}.swiper-button-next:after,.swiper-button-prev:after{font-family:swiper-icons;font-size:var(--swiper-navigation-size);text-transform:none!important;letter-spacing:0;text-transform:none;font-variant:initial;line-height:1}.swiper-button-prev,.swiper-container-rtl .swiper-button-next{left:10px;right:auto}.swiper-button-prev:after,.swiper-container-rtl .swiper-button-next:after{content:'prev'}.swiper-button-next,.swiper-container-rtl .swiper-button-prev{right:10px;left:auto}.swiper-button-next:after,.swiper-container-rtl .swiper-button-prev:after{content:'next'}.swiper-button-next.swiper-button-white,.swiper-button-prev.swiper-button-white{--swiper-navigation-color:#ffffff}.swiper-button-next.swiper-button-black,.swiper-button-prev.swiper-button-black{--swiper-navigation-color:#000000}.swiper-button-lock{display:none}.swiper-pagination{position:absolute;text-align:center;transition:.3s opacity;transform:translate3d(0,0,0);z-index:10}.swiper-pagination.swiper-pagination-hidden{opacity:0}.swiper-container-horizontal>.swiper-pagination-bullets,.swiper-pagination-custom,.swiper-pagination-fraction{bottom:10px;left:0;width:100%}.swiper-pagination-bullets-dynamic{overflow:hidden;font-size:0}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transform:scale(.33);position:relative}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev{transform:scale(.33)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next{transform:scale(.33)}.swiper-pagination-bullet{width:8px;height:8px;display:inline-block;border-radius:50%;background:#000;opacity:.2}button.swiper-pagination-bullet{border:none;margin:0;padding:0;box-shadow:none;-webkit-appearance:none;appearance:none}.swiper-pagination-clickable .swiper-pagination-bullet{cursor:pointer}.swiper-pagination-bullet:only-child{display:none!important}.swiper-pagination-bullet-active{opacity:1;background:var(--swiper-pagination-color,var(--swiper-theme-color))}.swiper-container-vertical>.swiper-pagination-bullets{right:10px;top:50%;transform:translate3d(0px,-50%,0)}.swiper-container-vertical>.swiper-pagination-bullets .swiper-pagination-bullet{margin:6px 0;display:block}.swiper-container-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{top:50%;transform:translateY(-50%);width:8px}.swiper-container-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{display:inline-block;transition:.2s transform,.2s top}.swiper-container-horizontal>.swiper-pagination-bullets .swiper-pagination-bullet{margin:0 4px}.swiper-container-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{left:50%;transform:translateX(-50%);white-space:nowrap}.swiper-container-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s left}.swiper-container-horizontal.swiper-container-rtl>.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s right}.swiper-pagination-progressbar{background:rgba(0,0,0,.25);position:absolute}.swiper-pagination-progressbar .swiper-pagination-progressbar-fill{background:var(--swiper-pagination-color,var(--swiper-theme-color));position:absolute;left:0;top:0;width:100%;height:100%;transform:scale(0);transform-origin:left top}.swiper-container-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill{transform-origin:right top}.swiper-container-horizontal>.swiper-pagination-progressbar,.swiper-container-vertical>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite{width:100%;height:4px;left:0;top:0}.swiper-container-horizontal>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite,.swiper-container-vertical>.swiper-pagination-progressbar{width:4px;height:100%;left:0;top:0}.swiper-pagination-white{--swiper-pagination-color:#ffffff}.swiper-pagination-black{--swiper-pagination-color:#000000}.swiper-pagination-lock{display:none}.swiper-scrollbar{border-radius:10px;position:relative;-ms-touch-action:none;background:rgba(0,0,0,.1)}.swiper-container-horizontal>.swiper-scrollbar{position:absolute;left:1%;bottom:3px;z-index:50;height:5px;width:98%}.swiper-container-vertical>.swiper-scrollbar{position:absolute;right:3px;top:1%;z-index:50;width:5px;height:98%}.swiper-scrollbar-drag{height:100%;width:100%;position:relative;background:rgba(0,0,0,.5);border-radius:10px;left:0;top:0}.swiper-scrollbar-cursor-drag{cursor:move}.swiper-scrollbar-lock{display:none}.swiper-zoom-container{width:100%;height:100%;display:flex;justify-content:center;align-items:center;text-align:center}.swiper-zoom-container>canvas,.swiper-zoom-container>img,.swiper-zoom-container>svg{max-width:100%;max-height:100%;object-fit:contain}.swiper-slide-zoomed{cursor:move}.swiper-lazy-preloader{width:42px;height:42px;position:absolute;left:50%;top:50%;margin-left:-21px;margin-top:-21px;z-index:10;transform-origin:50%;animation:swiper-preloader-spin 1s infinite linear;box-sizing:border-box;border:4px solid var(--swiper-preloader-color,var(--swiper-theme-color));border-radius:50%;border-top-color:transparent}.swiper-lazy-preloader-white{--swiper-preloader-color:#fff}.swiper-lazy-preloader-black{--swiper-preloader-color:#000}@keyframes swiper-preloader-spin{100%{transform:rotate(360deg)}}.swiper-container .swiper-notification{position:absolute;left:0;top:0;pointer-events:none;opacity:0;z-index:-1000}.swiper-container-fade.swiper-container-free-mode .swiper-slide{transition-timing-function:ease-out}.swiper-container-fade .swiper-slide{pointer-events:none;transition-property:opacity}.swiper-container-fade .swiper-slide .swiper-slide{pointer-events:none}.swiper-container-fade .swiper-slide-active,.swiper-container-fade .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-container-cube{overflow:visible}.swiper-container-cube .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;visibility:hidden;transform-origin:0 0;width:100%;height:100%}.swiper-container-cube .swiper-slide .swiper-slide{pointer-events:none}.swiper-container-cube.swiper-container-rtl .swiper-slide{transform-origin:100% 0}.swiper-container-cube .swiper-slide-active,.swiper-container-cube .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-container-cube .swiper-slide-active,.swiper-container-cube .swiper-slide-next,.swiper-container-cube .swiper-slide-next+.swiper-slide,.swiper-container-cube .swiper-slide-prev{pointer-events:auto;visibility:visible}.swiper-container-cube .swiper-slide-shadow-bottom,.swiper-container-cube .swiper-slide-shadow-left,.swiper-container-cube .swiper-slide-shadow-right,.swiper-container-cube .swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-container-cube .swiper-cube-shadow{position:absolute;left:0;bottom:0px;width:100%;height:100%;opacity:.6;z-index:0}.swiper-container-cube .swiper-cube-shadow:before{content:'';background:#000;position:absolute;left:0;top:0;bottom:0;right:0;filter:blur(50px)}.swiper-container-flip{overflow:visible}.swiper-container-flip .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1}.swiper-container-flip .swiper-slide .swiper-slide{pointer-events:none}.swiper-container-flip .swiper-slide-active,.swiper-container-flip .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-container-flip .swiper-slide-shadow-bottom,.swiper-container-flip .swiper-slide-shadow-left,.swiper-container-flip .swiper-slide-shadow-right,.swiper-container-flip .swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden} \ No newline at end of file diff --git a/sa-token-doc/static/swiper/swiper-bundle.min.js b/sa-token-doc/static/swiper/swiper-bundle.min.js new file mode 100644 index 000000000..31c4ffa09 --- /dev/null +++ b/sa-token-doc/static/swiper/swiper-bundle.min.js @@ -0,0 +1,14 @@ +/** + * Swiper 6.8.4 + * Most modern mobile touch slider and framework with hardware accelerated transitions + * https://swiperjs.com + * + * Copyright 2014-2021 Vladimir Kharlampidi + * + * Released under the MIT License + * + * Released on: August 23, 2021 + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Swiper=t()}(this,(function(){"use strict";function e(e,t){for(var a=0;a0&&i(e[s],t[s])}))}var s={body:{},addEventListener:function(){},removeEventListener:function(){},activeElement:{blur:function(){},nodeName:""},querySelector:function(){return null},querySelectorAll:function(){return[]},getElementById:function(){return null},createEvent:function(){return{initEvent:function(){}}},createElement:function(){return{children:[],childNodes:[],style:{},setAttribute:function(){},getElementsByTagName:function(){return[]}}},createElementNS:function(){return{}},importNode:function(){return null},location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""}};function r(){var e="undefined"!=typeof document?document:{};return i(e,s),e}var n={document:s,navigator:{userAgent:""},location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""},history:{replaceState:function(){},pushState:function(){},go:function(){},back:function(){}},CustomEvent:function(){return this},addEventListener:function(){},removeEventListener:function(){},getComputedStyle:function(){return{getPropertyValue:function(){return""}}},Image:function(){},Date:function(){},screen:{},setTimeout:function(){},clearTimeout:function(){},matchMedia:function(){return{}},requestAnimationFrame:function(e){return"undefined"==typeof setTimeout?(e(),null):setTimeout(e,0)},cancelAnimationFrame:function(e){"undefined"!=typeof setTimeout&&clearTimeout(e)}};function l(){var e="undefined"!=typeof window?window:{};return i(e,n),e}function o(e){return(o=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function d(e,t){return(d=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function p(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}function u(e,t,a){return(u=p()?Reflect.construct:function(e,t,a){var i=[null];i.push.apply(i,t);var s=new(Function.bind.apply(e,i));return a&&d(s,a.prototype),s}).apply(null,arguments)}function c(e){var t="function"==typeof Map?new Map:void 0;return(c=function(e){if(null===e||(a=e,-1===Function.toString.call(a).indexOf("[native code]")))return e;var a;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,i)}function i(){return u(e,arguments,o(this).constructor)}return i.prototype=Object.create(e.prototype,{constructor:{value:i,enumerable:!1,writable:!0,configurable:!0}}),d(i,e)})(e)}var h=function(e){var t,a;function i(t){var a,i,s;return a=e.call.apply(e,[this].concat(t))||this,i=function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(a),s=i.__proto__,Object.defineProperty(i,"__proto__",{get:function(){return s},set:function(e){s.__proto__=e}}),a}return a=e,(t=i).prototype=Object.create(a.prototype),t.prototype.constructor=t,t.__proto__=a,i}(c(Array));function v(e){void 0===e&&(e=[]);var t=[];return e.forEach((function(e){Array.isArray(e)?t.push.apply(t,v(e)):t.push(e)})),t}function f(e,t){return Array.prototype.filter.call(e,t)}function m(e,t){var a=l(),i=r(),s=[];if(!t&&e instanceof h)return e;if(!e)return new h(s);if("string"==typeof e){var n=e.trim();if(n.indexOf("<")>=0&&n.indexOf(">")>=0){var o="div";0===n.indexOf("0})).length>0},toggleClass:function(){for(var e=arguments.length,t=new Array(e),a=0;a=0;h-=1){var v=c[h];r&&v.listener===r||r&&v.listener&&v.listener.dom7proxy&&v.listener.dom7proxy===r?(u.removeEventListener(d,v.proxyListener,n),c.splice(h,1)):r||(u.removeEventListener(d,v.proxyListener,n),c.splice(h,1))}}return this},trigger:function(){for(var e=l(),t=arguments.length,a=new Array(t),i=0;i0})),p.dispatchEvent(u),p.dom7EventData=[],delete p.dom7EventData}}return this},transitionEnd:function(e){var t=this;return e&&t.on("transitionend",(function a(i){i.target===this&&(e.call(this,i),t.off("transitionend",a))})),this},outerWidth:function(e){if(this.length>0){if(e){var t=this.styles();return this[0].offsetWidth+parseFloat(t.getPropertyValue("margin-right"))+parseFloat(t.getPropertyValue("margin-left"))}return this[0].offsetWidth}return null},outerHeight:function(e){if(this.length>0){if(e){var t=this.styles();return this[0].offsetHeight+parseFloat(t.getPropertyValue("margin-top"))+parseFloat(t.getPropertyValue("margin-bottom"))}return this[0].offsetHeight}return null},styles:function(){var e=l();return this[0]?e.getComputedStyle(this[0],null):{}},offset:function(){if(this.length>0){var e=l(),t=r(),a=this[0],i=a.getBoundingClientRect(),s=t.body,n=a.clientTop||s.clientTop||0,o=a.clientLeft||s.clientLeft||0,d=a===e?e.scrollY:a.scrollTop,p=a===e?e.scrollX:a.scrollLeft;return{top:i.top+d-n,left:i.left+p-o}}return null},css:function(e,t){var a,i=l();if(1===arguments.length){if("string"!=typeof e){for(a=0;at-1)return m([]);if(e<0){var a=t+e;return m(a<0?[]:[this[a]])}return m([this[e]])},append:function(){for(var e,t=r(),a=0;a=0;a-=1)this[t].insertBefore(s.childNodes[a],this[t].childNodes[0])}else if(e instanceof h)for(a=0;a0?e?this[0].nextElementSibling&&m(this[0].nextElementSibling).is(e)?m([this[0].nextElementSibling]):m([]):this[0].nextElementSibling?m([this[0].nextElementSibling]):m([]):m([])},nextAll:function(e){var t=[],a=this[0];if(!a)return m([]);for(;a.nextElementSibling;){var i=a.nextElementSibling;e?m(i).is(e)&&t.push(i):t.push(i),a=i}return m(t)},prev:function(e){if(this.length>0){var t=this[0];return e?t.previousElementSibling&&m(t.previousElementSibling).is(e)?m([t.previousElementSibling]):m([]):t.previousElementSibling?m([t.previousElementSibling]):m([])}return m([])},prevAll:function(e){var t=[],a=this[0];if(!a)return m([]);for(;a.previousElementSibling;){var i=a.previousElementSibling;e?m(i).is(e)&&t.push(i):t.push(i),a=i}return m(t)},parent:function(e){for(var t=[],a=0;a6&&(i=i.split(", ").map((function(e){return e.replace(",",".")})).join(", ")),s=new r.WebKitCSSMatrix("none"===i?"":i)):a=(s=n.MozTransform||n.OTransform||n.MsTransform||n.msTransform||n.transform||n.getPropertyValue("transform").replace("translate(","matrix(1, 0, 0, 1,")).toString().split(","),"x"===t&&(i=r.WebKitCSSMatrix?s.m41:16===a.length?parseFloat(a[12]):parseFloat(a[4])),"y"===t&&(i=r.WebKitCSSMatrix?s.m42:16===a.length?parseFloat(a[13]):parseFloat(a[5])),i||0}function C(e){return"object"==typeof e&&null!==e&&e.constructor&&"Object"===Object.prototype.toString.call(e).slice(8,-1)}function S(e){return"undefined"!=typeof window&&void 0!==window.HTMLElement?e instanceof HTMLElement:e&&(1===e.nodeType||11===e.nodeType)}function M(){for(var e=Object(arguments.length<=0?void 0:arguments[0]),t=["__proto__","constructor","prototype"],a=1;a=0,observer:"MutationObserver"in e||"WebkitMutationObserver"in e,passiveListener:function(){var t=!1;try{var a=Object.defineProperty({},"passive",{get:function(){t=!0}});e.addEventListener("testPassiveListener",null,a)}catch(e){}return t}(),gestures:"ongesturestart"in e}}()),g}function L(e){return void 0===e&&(e={}),b||(b=function(e){var t=(void 0===e?{}:e).userAgent,a=$(),i=l(),s=i.navigator.platform,r=t||i.navigator.userAgent,n={ios:!1,android:!1},o=i.screen.width,d=i.screen.height,p=r.match(/(Android);?[\s\/]+([\d.]+)?/),u=r.match(/(iPad).*OS\s([\d_]+)/),c=r.match(/(iPod)(.*OS\s([\d_]+))?/),h=!u&&r.match(/(iPhone\sOS|iOS)\s([\d_]+)/),v="Win32"===s,f="MacIntel"===s;return!u&&f&&a.touch&&["1024x1366","1366x1024","834x1194","1194x834","834x1112","1112x834","768x1024","1024x768","820x1180","1180x820","810x1080","1080x810"].indexOf(o+"x"+d)>=0&&((u=r.match(/(Version)\/([\d.]+)/))||(u=[0,1,"13_0_0"]),f=!1),p&&!v&&(n.os="android",n.android=!0),(u||h||c)&&(n.os="ios",n.ios=!0),n}(e)),b}function I(){return w||(w=function(){var e,t=l();return{isEdge:!!t.navigator.userAgent.match(/Edge/g),isSafari:(e=t.navigator.userAgent.toLowerCase(),e.indexOf("safari")>=0&&e.indexOf("chrome")<0&&e.indexOf("android")<0),isWebView:/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(t.navigator.userAgent)}}()),w}Object.keys(y).forEach((function(e){Object.defineProperty(m.fn,e,{value:y[e],writable:!0})}));var O={name:"resize",create:function(){var e=this;M(e,{resize:{observer:null,createObserver:function(){e&&!e.destroyed&&e.initialized&&(e.resize.observer=new ResizeObserver((function(t){var a=e.width,i=e.height,s=a,r=i;t.forEach((function(t){var a=t.contentBoxSize,i=t.contentRect,n=t.target;n&&n!==e.el||(s=i?i.width:(a[0]||a).inlineSize,r=i?i.height:(a[0]||a).blockSize)})),s===a&&r===i||e.resize.resizeHandler()})),e.resize.observer.observe(e.el))},removeObserver:function(){e.resize.observer&&e.resize.observer.unobserve&&e.el&&(e.resize.observer.unobserve(e.el),e.resize.observer=null)},resizeHandler:function(){e&&!e.destroyed&&e.initialized&&(e.emit("beforeResize"),e.emit("resize"))},orientationChangeHandler:function(){e&&!e.destroyed&&e.initialized&&e.emit("orientationchange")}}})},on:{init:function(e){var t=l();e.params.resizeObserver&&void 0!==l().ResizeObserver?e.resize.createObserver():(t.addEventListener("resize",e.resize.resizeHandler),t.addEventListener("orientationchange",e.resize.orientationChangeHandler))},destroy:function(e){var t=l();e.resize.removeObserver(),t.removeEventListener("resize",e.resize.resizeHandler),t.removeEventListener("orientationchange",e.resize.orientationChangeHandler)}}},A={attach:function(e,t){void 0===t&&(t={});var a=l(),i=this,s=new(a.MutationObserver||a.WebkitMutationObserver)((function(e){if(1!==e.length){var t=function(){i.emit("observerUpdate",e[0])};a.requestAnimationFrame?a.requestAnimationFrame(t):a.setTimeout(t,0)}else i.emit("observerUpdate",e[0])}));s.observe(e,{attributes:void 0===t.attributes||t.attributes,childList:void 0===t.childList||t.childList,characterData:void 0===t.characterData||t.characterData}),i.observer.observers.push(s)},init:function(){var e=this;if(e.support.observer&&e.params.observer){if(e.params.observeParents)for(var t=e.$el.parents(),a=0;a0||s.isTouched&&s.isMoved))){!!n.noSwipingClass&&""!==n.noSwipingClass&&d.target&&d.target.shadowRoot&&e.path&&e.path[0]&&(p=m(e.path[0]));var u=n.noSwipingSelector?n.noSwipingSelector:"."+n.noSwipingClass,c=!(!d.target||!d.target.shadowRoot);if(n.noSwiping&&(c?function(e,t){return void 0===t&&(t=this),function t(a){return a&&a!==r()&&a!==l()?(a.assignedSlot&&(a=a.assignedSlot),a.closest(e)||t(a.getRootNode().host)):null}(t)}(u,d.target):p.closest(u)[0]))t.allowClick=!0;else if(!n.swipeHandler||p.closest(n.swipeHandler)[0]){o.currentX="touchstart"===d.type?d.targetTouches[0].pageX:d.pageX,o.currentY="touchstart"===d.type?d.targetTouches[0].pageY:d.pageY;var h=o.currentX,v=o.currentY,f=n.edgeSwipeDetection||n.iOSEdgeSwipeDetection,g=n.edgeSwipeThreshold||n.iOSEdgeSwipeThreshold;if(f&&(h<=g||h>=i.innerWidth-g)){if("prevent"!==f)return;e.preventDefault()}if(M(s,{isTouched:!0,isMoved:!1,allowTouchCallbacks:!0,isScrolling:void 0,startMoving:void 0}),o.startX=h,o.startY=v,s.touchStartTime=x(),t.allowClick=!0,t.updateSize(),t.swipeDirection=void 0,n.threshold>0&&(s.allowThresholdMove=!1),"touchstart"!==d.type){var b=!0;p.is(s.focusableElements)&&(b=!1),a.activeElement&&m(a.activeElement).is(s.focusableElements)&&a.activeElement!==p[0]&&a.activeElement.blur();var w=b&&t.allowTouchMove&&n.touchStartPreventDefault;!n.touchStartForcePreventDefault&&!w||p[0].isContentEditable||d.preventDefault()}t.emit("touchStart",d)}}}}function G(e){var t=r(),a=this,i=a.touchEventsData,s=a.params,n=a.touches,l=a.rtlTranslate;if(a.enabled){var o=e;if(o.originalEvent&&(o=o.originalEvent),i.isTouched){if(!i.isTouchEvent||"touchmove"===o.type){var d="touchmove"===o.type&&o.targetTouches&&(o.targetTouches[0]||o.changedTouches[0]),p="touchmove"===o.type?d.pageX:o.pageX,u="touchmove"===o.type?d.pageY:o.pageY;if(o.preventedByNestedSwiper)return n.startX=p,void(n.startY=u);if(!a.allowTouchMove)return a.allowClick=!1,void(i.isTouched&&(M(n,{startX:p,startY:u,currentX:p,currentY:u}),i.touchStartTime=x()));if(i.isTouchEvent&&s.touchReleaseOnEdges&&!s.loop)if(a.isVertical()){if(un.startY&&a.translate>=a.minTranslate())return i.isTouched=!1,void(i.isMoved=!1)}else if(pn.startX&&a.translate>=a.minTranslate())return;if(i.isTouchEvent&&t.activeElement&&o.target===t.activeElement&&m(o.target).is(i.focusableElements))return i.isMoved=!0,void(a.allowClick=!1);if(i.allowTouchCallbacks&&a.emit("touchMove",o),!(o.targetTouches&&o.targetTouches.length>1)){n.currentX=p,n.currentY=u;var c=n.currentX-n.startX,h=n.currentY-n.startY;if(!(a.params.threshold&&Math.sqrt(Math.pow(c,2)+Math.pow(h,2))=25&&(v=180*Math.atan2(Math.abs(h),Math.abs(c))/Math.PI,i.isScrolling=a.isHorizontal()?v>s.touchAngle:90-v>s.touchAngle);if(i.isScrolling&&a.emit("touchMoveOpposite",o),void 0===i.startMoving&&(n.currentX===n.startX&&n.currentY===n.startY||(i.startMoving=!0)),i.isScrolling)i.isTouched=!1;else if(i.startMoving){a.allowClick=!1,!s.cssMode&&o.cancelable&&o.preventDefault(),s.touchMoveStopPropagation&&!s.nested&&o.stopPropagation(),i.isMoved||(s.loop&&a.loopFix(),i.startTranslate=a.getTranslate(),a.setTransition(0),a.animating&&a.$wrapperEl.trigger("webkitTransitionEnd transitionend"),i.allowMomentumBounce=!1,!s.grabCursor||!0!==a.allowSlideNext&&!0!==a.allowSlidePrev||a.setGrabCursor(!0),a.emit("sliderFirstMove",o)),a.emit("sliderMove",o),i.isMoved=!0;var f=a.isHorizontal()?c:h;n.diff=f,f*=s.touchRatio,l&&(f=-f),a.swipeDirection=f>0?"prev":"next",i.currentTranslate=f+i.startTranslate;var g=!0,b=s.resistanceRatio;if(s.touchReleaseOnEdges&&(b=0),f>0&&i.currentTranslate>a.minTranslate()?(g=!1,s.resistance&&(i.currentTranslate=a.minTranslate()-1+Math.pow(-a.minTranslate()+i.startTranslate+f,b))):f<0&&i.currentTranslatei.startTranslate&&(i.currentTranslate=i.startTranslate),a.allowSlidePrev||a.allowSlideNext||(i.currentTranslate=i.startTranslate),s.threshold>0){if(!(Math.abs(f)>s.threshold||i.allowThresholdMove))return void(i.currentTranslate=i.startTranslate);if(!i.allowThresholdMove)return i.allowThresholdMove=!0,n.startX=n.currentX,n.startY=n.currentY,i.currentTranslate=i.startTranslate,void(n.diff=a.isHorizontal()?n.currentX-n.startX:n.currentY-n.startY)}s.followFinger&&!s.cssMode&&((s.freeMode||s.watchSlidesProgress||s.watchSlidesVisibility)&&(a.updateActiveIndex(),a.updateSlidesClasses()),s.freeMode&&(0===i.velocities.length&&i.velocities.push({position:n[a.isHorizontal()?"startX":"startY"],time:i.touchStartTime}),i.velocities.push({position:n[a.isHorizontal()?"currentX":"currentY"],time:x()})),a.updateProgress(i.currentTranslate),a.setTranslate(i.currentTranslate))}}}}}else i.startMoving&&i.isScrolling&&a.emit("touchMoveOpposite",o)}}function B(e){var t=this,a=t.touchEventsData,i=t.params,s=t.touches,r=t.rtlTranslate,n=t.$wrapperEl,l=t.slidesGrid,o=t.snapGrid;if(t.enabled){var d=e;if(d.originalEvent&&(d=d.originalEvent),a.allowTouchCallbacks&&t.emit("touchEnd",d),a.allowTouchCallbacks=!1,!a.isTouched)return a.isMoved&&i.grabCursor&&t.setGrabCursor(!1),a.isMoved=!1,void(a.startMoving=!1);i.grabCursor&&a.isMoved&&a.isTouched&&(!0===t.allowSlideNext||!0===t.allowSlidePrev)&&t.setGrabCursor(!1);var p,u=x(),c=u-a.touchStartTime;if(t.allowClick&&(t.updateClickedSlide(d),t.emit("tap click",d),c<300&&u-a.lastClickTime<300&&t.emit("doubleTap doubleClick",d)),a.lastClickTime=x(),E((function(){t.destroyed||(t.allowClick=!0)})),!a.isTouched||!a.isMoved||!t.swipeDirection||0===s.diff||a.currentTranslate===a.startTranslate)return a.isTouched=!1,a.isMoved=!1,void(a.startMoving=!1);if(a.isTouched=!1,a.isMoved=!1,a.startMoving=!1,p=i.followFinger?r?t.translate:-t.translate:-a.currentTranslate,!i.cssMode)if(i.freeMode){if(p<-t.minTranslate())return void t.slideTo(t.activeIndex);if(p>-t.maxTranslate())return void(t.slides.length1){var h=a.velocities.pop(),v=a.velocities.pop(),f=h.position-v.position,m=h.time-v.time;t.velocity=f/m,t.velocity/=2,Math.abs(t.velocity)150||x()-h.time>300)&&(t.velocity=0)}else t.velocity=0;t.velocity*=i.freeModeMomentumVelocityRatio,a.velocities.length=0;var g=1e3*i.freeModeMomentumRatio,b=t.velocity*g,w=t.translate+b;r&&(w=-w);var y,T,C=!1,S=20*Math.abs(t.velocity)*i.freeModeMomentumBounceRatio;if(wt.minTranslate())i.freeModeMomentumBounce?(w-t.minTranslate()>S&&(w=t.minTranslate()+S),y=t.minTranslate(),C=!0,a.allowMomentumBounce=!0):w=t.minTranslate(),i.loop&&i.centeredSlides&&(T=!0);else if(i.freeModeSticky){for(var M,z=0;z-w){M=z;break}w=-(w=Math.abs(o[M]-w)=i.longSwipesMs)&&(t.updateProgress(),t.updateActiveIndex(),t.updateSlidesClasses())}else{for(var $=0,L=t.slidesSizesGrid[0],I=0;I=l[I]&&p=l[I]&&($=I,L=l[l.length-1]-l[l.length-2])}var A=(p-l[$])/L,D=$i.longSwipesMs){if(!i.longSwipes)return void t.slideTo(t.activeIndex);"next"===t.swipeDirection&&(A>=i.longSwipesRatio?t.slideTo($+D):t.slideTo($)),"prev"===t.swipeDirection&&(A>1-i.longSwipesRatio?t.slideTo($+D):t.slideTo($))}else{if(!i.shortSwipes)return void t.slideTo(t.activeIndex);t.navigation&&(d.target===t.navigation.nextEl||d.target===t.navigation.prevEl)?d.target===t.navigation.nextEl?t.slideTo($+D):t.slideTo($):("next"===t.swipeDirection&&t.slideTo($+D),"prev"===t.swipeDirection&&t.slideTo($))}}}}function H(){var e=this,t=e.params,a=e.el;if(!a||0!==a.offsetWidth){t.breakpoints&&e.setBreakpoint();var i=e.allowSlideNext,s=e.allowSlidePrev,r=e.snapGrid;e.allowSlideNext=!0,e.allowSlidePrev=!0,e.updateSize(),e.updateSlides(),e.updateSlidesClasses(),("auto"===t.slidesPerView||t.slidesPerView>1)&&e.isEnd&&!e.isBeginning&&!e.params.centeredSlides?e.slideTo(e.slides.length-1,0,!1,!0):e.slideTo(e.activeIndex,0,!1,!0),e.autoplay&&e.autoplay.running&&e.autoplay.paused&&e.autoplay.run(),e.allowSlidePrev=s,e.allowSlideNext=i,e.params.watchOverflow&&r!==e.snapGrid&&e.checkOverflow()}}function R(e){var t=this;t.enabled&&(t.allowClick||(t.params.preventClicks&&e.preventDefault(),t.params.preventClicksPropagation&&t.animating&&(e.stopPropagation(),e.stopImmediatePropagation())))}function X(){var e=this,t=e.wrapperEl,a=e.rtlTranslate;if(e.enabled){e.previousTranslate=e.translate,e.isHorizontal()?e.translate=a?t.scrollWidth-t.offsetWidth-t.scrollLeft:-t.scrollLeft:e.translate=-t.scrollTop,-0===e.translate&&(e.translate=0),e.updateActiveIndex(),e.updateSlidesClasses();var i=e.maxTranslate()-e.minTranslate();(0===i?0:(e.translate-e.minTranslate())/i)!==e.progress&&e.updateProgress(a?-e.translate:e.translate),e.emit("setTranslate",e.translate,!1)}}var Y=!1;function V(){}var W={init:!0,direction:"horizontal",touchEventsTarget:"container",initialSlide:0,speed:300,cssMode:!1,updateOnWindowResize:!0,resizeObserver:!1,nested:!1,createElements:!1,enabled:!0,focusableElements:"input, select, option, textarea, button, video, label",width:null,height:null,preventInteractionOnTransition:!1,userAgent:null,url:null,edgeSwipeDetection:!1,edgeSwipeThreshold:20,freeMode:!1,freeModeMomentum:!0,freeModeMomentumRatio:1,freeModeMomentumBounce:!0,freeModeMomentumBounceRatio:1,freeModeMomentumVelocityRatio:1,freeModeSticky:!1,freeModeMinimumVelocity:.02,autoHeight:!1,setWrapperSize:!1,virtualTranslate:!1,effect:"slide",breakpoints:void 0,breakpointsBase:"window",spaceBetween:0,slidesPerView:1,slidesPerColumn:1,slidesPerColumnFill:"column",slidesPerGroup:1,slidesPerGroupSkip:0,centeredSlides:!1,centeredSlidesBounds:!1,slidesOffsetBefore:0,slidesOffsetAfter:0,normalizeSlideIndex:!0,centerInsufficientSlides:!1,watchOverflow:!1,roundLengths:!1,touchRatio:1,touchAngle:45,simulateTouch:!0,shortSwipes:!0,longSwipes:!0,longSwipesRatio:.5,longSwipesMs:300,followFinger:!0,allowTouchMove:!0,threshold:0,touchMoveStopPropagation:!1,touchStartPreventDefault:!0,touchStartForcePreventDefault:!1,touchReleaseOnEdges:!1,uniqueNavElements:!0,resistance:!0,resistanceRatio:.85,watchSlidesProgress:!1,watchSlidesVisibility:!1,grabCursor:!1,preventClicks:!0,preventClicksPropagation:!0,slideToClickedSlide:!1,preloadImages:!0,updateOnImagesReady:!0,loop:!1,loopAdditionalSlides:0,loopedSlides:null,loopFillGroupWithBlank:!1,loopPreventsSlide:!0,allowSlidePrev:!0,allowSlideNext:!0,swipeHandler:null,noSwiping:!0,noSwipingClass:"swiper-no-swiping",noSwipingSelector:null,passiveListeners:!0,containerModifierClass:"swiper-container-",slideClass:"swiper-slide",slideBlankClass:"swiper-slide-invisible-blank",slideActiveClass:"swiper-slide-active",slideDuplicateActiveClass:"swiper-slide-duplicate-active",slideVisibleClass:"swiper-slide-visible",slideDuplicateClass:"swiper-slide-duplicate",slideNextClass:"swiper-slide-next",slideDuplicateNextClass:"swiper-slide-duplicate-next",slidePrevClass:"swiper-slide-prev",slideDuplicatePrevClass:"swiper-slide-duplicate-prev",wrapperClass:"swiper-wrapper",runCallbacksOnInit:!0,_emitClasses:!1},F={modular:{useParams:function(e){var t=this;t.modules&&Object.keys(t.modules).forEach((function(a){var i=t.modules[a];i.params&&M(e,i.params)}))},useModules:function(e){void 0===e&&(e={});var t=this;t.modules&&Object.keys(t.modules).forEach((function(a){var i=t.modules[a],s=e[a]||{};i.on&&t.on&&Object.keys(i.on).forEach((function(e){t.on(e,i.on[e])})),i.create&&i.create.bind(t)(s)}))}},eventsEmitter:{on:function(e,t,a){var i=this;if("function"!=typeof t)return i;var s=a?"unshift":"push";return e.split(" ").forEach((function(e){i.eventsListeners[e]||(i.eventsListeners[e]=[]),i.eventsListeners[e][s](t)})),i},once:function(e,t,a){var i=this;if("function"!=typeof t)return i;function s(){i.off(e,s),s.__emitterProxy&&delete s.__emitterProxy;for(var a=arguments.length,r=new Array(a),n=0;n=0&&t.eventsAnyListeners.splice(a,1),t},off:function(e,t){var a=this;return a.eventsListeners?(e.split(" ").forEach((function(e){void 0===t?a.eventsListeners[e]=[]:a.eventsListeners[e]&&a.eventsListeners[e].forEach((function(i,s){(i===t||i.__emitterProxy&&i.__emitterProxy===t)&&a.eventsListeners[e].splice(s,1)}))})),a):a},emit:function(){var e,t,a,i=this;if(!i.eventsListeners)return i;for(var s=arguments.length,r=new Array(s),n=0;n=0&&(w=parseFloat(w.replace("%",""))/100*r),e.virtualSize=-w,n?p.css({marginLeft:"",marginBottom:"",marginTop:""}):p.css({marginRight:"",marginBottom:"",marginTop:""}),i.slidesPerColumn>1&&(T=Math.floor(u/i.slidesPerColumn)===u/e.params.slidesPerColumn?u:Math.ceil(u/i.slidesPerColumn)*i.slidesPerColumn,"auto"!==i.slidesPerView&&"row"===i.slidesPerColumnFill&&(T=Math.max(T,i.slidesPerView*i.slidesPerColumn)));for(var S,z,P,k=i.slidesPerColumn,$=T/k,L=Math.floor(u/i.slidesPerColumn),I=0;I1){var A=void 0,D=void 0,N=void 0;if("row"===i.slidesPerColumnFill&&i.slidesPerGroup>1){var G=Math.floor(I/(i.slidesPerGroup*i.slidesPerColumn)),B=I-i.slidesPerColumn*i.slidesPerGroup*G,H=0===G?i.slidesPerGroup:Math.min(Math.ceil((u-G*k*i.slidesPerGroup)/k),i.slidesPerGroup);A=(D=B-(N=Math.floor(B/H))*H+G*i.slidesPerGroup)+N*T/k,O.css({"-webkit-box-ordinal-group":A,"-moz-box-ordinal-group":A,"-ms-flex-order":A,"-webkit-order":A,order:A})}else"column"===i.slidesPerColumnFill?(N=I-(D=Math.floor(I/k))*k,(D>L||D===L&&N===k-1)&&(N+=1)>=k&&(N=0,D+=1)):D=I-(N=Math.floor(I/$))*$;O.css(t("margin-top"),0!==N?i.spaceBetween&&i.spaceBetween+"px":"")}if("none"!==O.css("display")){if("auto"===i.slidesPerView){var R=getComputedStyle(O[0]),X=O[0].style.transform,Y=O[0].style.webkitTransform;if(X&&(O[0].style.transform="none"),Y&&(O[0].style.webkitTransform="none"),i.roundLengths)C=e.isHorizontal()?O.outerWidth(!0):O.outerHeight(!0);else{var V=a(R,"width"),W=a(R,"padding-left"),F=a(R,"padding-right"),_=a(R,"margin-left"),q=a(R,"margin-right"),j=R.getPropertyValue("box-sizing");if(j&&"border-box"===j)C=V+_+q;else{var U=O[0],K=U.clientWidth;C=V+W+F+_+q+(U.offsetWidth-K)}}X&&(O[0].style.transform=X),Y&&(O[0].style.webkitTransform=Y),i.roundLengths&&(C=Math.floor(C))}else C=(r-(i.slidesPerView-1)*w)/i.slidesPerView,i.roundLengths&&(C=Math.floor(C)),p[I]&&(p[I].style[t("width")]=C+"px");p[I]&&(p[I].swiperSlideSize=C),v.push(C),i.centeredSlides?(y=y+C/2+E/2+w,0===E&&0!==I&&(y=y-r/2-w),0===I&&(y=y-r/2-w),Math.abs(y)<.001&&(y=0),i.roundLengths&&(y=Math.floor(y)),x%i.slidesPerGroup==0&&c.push(y),h.push(y)):(i.roundLengths&&(y=Math.floor(y)),(x-Math.min(e.params.slidesPerGroupSkip,x))%e.params.slidesPerGroup==0&&c.push(y),h.push(y),y=y+C+w),e.virtualSize+=C+w,E=C,x+=1}}if(e.virtualSize=Math.max(e.virtualSize,r)+m,n&&l&&("slide"===i.effect||"coverflow"===i.effect)&&s.css({width:e.virtualSize+i.spaceBetween+"px"}),i.setWrapperSize)s.css(((z={})[t("width")]=e.virtualSize+i.spaceBetween+"px",z));if(i.slidesPerColumn>1)if(e.virtualSize=(C+i.spaceBetween)*T,e.virtualSize=Math.ceil(e.virtualSize/i.slidesPerColumn)-i.spaceBetween,s.css(((P={})[t("width")]=e.virtualSize+i.spaceBetween+"px",P)),i.centeredSlides){S=[];for(var Z=0;Z1&&c.push(e.virtualSize-r)}if(0===c.length&&(c=[0]),0!==i.spaceBetween){var te,ae=e.isHorizontal()&&n?"marginLeft":t("marginRight");p.filter((function(e,t){return!i.cssMode||t!==p.length-1})).css(((te={})[ae]=w+"px",te))}if(i.centeredSlides&&i.centeredSlidesBounds){var ie=0;v.forEach((function(e){ie+=e+(i.spaceBetween?i.spaceBetween:0)}));var se=(ie-=i.spaceBetween)-r;c=c.map((function(e){return e<0?-f:e>se?se+m:e}))}if(i.centerInsufficientSlides){var re=0;if(v.forEach((function(e){re+=e+(i.spaceBetween?i.spaceBetween:0)})),(re-=i.spaceBetween)1)if(a.params.centeredSlides)a.visibleSlides.each((function(e){i.push(e)}));else for(t=0;ta.slides.length&&!s)break;i.push(n(l))}else i.push(n(a.activeIndex));for(t=0;tr?o:r}r&&a.$wrapperEl.css("height",r+"px")},updateSlidesOffset:function(){for(var e=this.slides,t=0;t=0&&d1&&p<=t.size||d<=0&&p>=t.size)&&(t.visibleSlides.push(l),t.visibleSlidesIndexes.push(n),i.eq(n).addClass(a.slideVisibleClass))}l.progress=s?-o:o}t.visibleSlides=m(t.visibleSlides)}},updateProgress:function(e){var t=this;if(void 0===e){var a=t.rtlTranslate?-1:1;e=t&&t.translate&&t.translate*a||0}var i=t.params,s=t.maxTranslate()-t.minTranslate(),r=t.progress,n=t.isBeginning,l=t.isEnd,o=n,d=l;0===s?(r=0,n=!0,l=!0):(n=(r=(e-t.minTranslate())/s)<=0,l=r>=1),M(t,{progress:r,isBeginning:n,isEnd:l}),(i.watchSlidesProgress||i.watchSlidesVisibility||i.centeredSlides&&i.autoHeight)&&t.updateSlidesProgress(e),n&&!o&&t.emit("reachBeginning toEdge"),l&&!d&&t.emit("reachEnd toEdge"),(o&&!n||d&&!l)&&t.emit("fromEdge"),t.emit("progress",r)},updateSlidesClasses:function(){var e,t=this,a=t.slides,i=t.params,s=t.$wrapperEl,r=t.activeIndex,n=t.realIndex,l=t.virtual&&i.virtual.enabled;a.removeClass(i.slideActiveClass+" "+i.slideNextClass+" "+i.slidePrevClass+" "+i.slideDuplicateActiveClass+" "+i.slideDuplicateNextClass+" "+i.slideDuplicatePrevClass),(e=l?t.$wrapperEl.find("."+i.slideClass+'[data-swiper-slide-index="'+r+'"]'):a.eq(r)).addClass(i.slideActiveClass),i.loop&&(e.hasClass(i.slideDuplicateClass)?s.children("."+i.slideClass+":not(."+i.slideDuplicateClass+')[data-swiper-slide-index="'+n+'"]').addClass(i.slideDuplicateActiveClass):s.children("."+i.slideClass+"."+i.slideDuplicateClass+'[data-swiper-slide-index="'+n+'"]').addClass(i.slideDuplicateActiveClass));var o=e.nextAll("."+i.slideClass).eq(0).addClass(i.slideNextClass);i.loop&&0===o.length&&(o=a.eq(0)).addClass(i.slideNextClass);var d=e.prevAll("."+i.slideClass).eq(0).addClass(i.slidePrevClass);i.loop&&0===d.length&&(d=a.eq(-1)).addClass(i.slidePrevClass),i.loop&&(o.hasClass(i.slideDuplicateClass)?s.children("."+i.slideClass+":not(."+i.slideDuplicateClass+')[data-swiper-slide-index="'+o.attr("data-swiper-slide-index")+'"]').addClass(i.slideDuplicateNextClass):s.children("."+i.slideClass+"."+i.slideDuplicateClass+'[data-swiper-slide-index="'+o.attr("data-swiper-slide-index")+'"]').addClass(i.slideDuplicateNextClass),d.hasClass(i.slideDuplicateClass)?s.children("."+i.slideClass+":not(."+i.slideDuplicateClass+')[data-swiper-slide-index="'+d.attr("data-swiper-slide-index")+'"]').addClass(i.slideDuplicatePrevClass):s.children("."+i.slideClass+"."+i.slideDuplicateClass+'[data-swiper-slide-index="'+d.attr("data-swiper-slide-index")+'"]').addClass(i.slideDuplicatePrevClass)),t.emitSlidesClasses()},updateActiveIndex:function(e){var t,a=this,i=a.rtlTranslate?a.translate:-a.translate,s=a.slidesGrid,r=a.snapGrid,n=a.params,l=a.activeIndex,o=a.realIndex,d=a.snapIndex,p=e;if(void 0===p){for(var u=0;u=s[u]&&i=s[u]&&i=s[u]&&(p=u);n.normalizeSlideIndex&&(p<0||void 0===p)&&(p=0)}if(r.indexOf(i)>=0)t=r.indexOf(i);else{var c=Math.min(n.slidesPerGroupSkip,p);t=c+Math.floor((p-c)/n.slidesPerGroup)}if(t>=r.length&&(t=r.length-1),p!==l){var h=parseInt(a.slides.eq(p).attr("data-swiper-slide-index")||p,10);M(a,{snapIndex:t,realIndex:h,previousIndex:l,activeIndex:p}),a.emit("activeIndexChange"),a.emit("snapIndexChange"),o!==h&&a.emit("realIndexChange"),(a.initialized||a.params.runCallbacksOnInit)&&a.emit("slideChange")}else t!==d&&(a.snapIndex=t,a.emit("snapIndexChange"))},updateClickedSlide:function(e){var t,a=this,i=a.params,s=m(e.target).closest("."+i.slideClass)[0],r=!1;if(s)for(var n=0;nd?d:i&&er?"next":is?"next":i=d.length&&(g=d.length-1),(c||o.initialSlide||0)===(u||0)&&a&&n.emit("beforeSlideChangeStart");var b,w=-d[g];if(n.updateProgress(w),o.normalizeSlideIndex)for(var y=0;y=x&&E=x&&E=x&&(l=y)}if(n.initialized&&l!==c){if(!n.allowSlideNext&&wn.translate&&w>n.maxTranslate()&&(c||0)!==l)return!1}if(b=l>c?"next":l=e&&(h=e)})),void 0!==h&&(p=l.indexOf(h))<0&&(p=i.activeIndex-1),i.slideTo(p,e,t,a)},slideReset:function(e,t,a){return void 0===e&&(e=this.params.speed),void 0===t&&(t=!0),this.slideTo(this.activeIndex,e,t,a)},slideToClosest:function(e,t,a,i){void 0===e&&(e=this.params.speed),void 0===t&&(t=!0),void 0===i&&(i=.5);var s=this,r=s.activeIndex,n=Math.min(s.params.slidesPerGroupSkip,r),l=n+Math.floor((r-n)/s.params.slidesPerGroup),o=s.rtlTranslate?s.translate:-s.translate;if(o>=s.snapGrid[l]){var d=s.snapGrid[l];o-d>(s.snapGrid[l+1]-d)*i&&(r+=s.params.slidesPerGroup)}else{var p=s.snapGrid[l-1];o-p<=(s.snapGrid[l]-p)*i&&(r-=s.params.slidesPerGroup)}return r=Math.max(r,0),r=Math.min(r,s.slidesGrid.length-1),s.slideTo(r,e,t,a)},slideToClickedSlide:function(){var e,t=this,a=t.params,i=t.$wrapperEl,s="auto"===a.slidesPerView?t.slidesPerViewDynamic():a.slidesPerView,r=t.clickedIndex;if(a.loop){if(t.animating)return;e=parseInt(m(t.clickedSlide).attr("data-swiper-slide-index"),10),a.centeredSlides?rt.slides.length-t.loopedSlides+s/2?(t.loopFix(),r=i.children("."+a.slideClass+'[data-swiper-slide-index="'+e+'"]:not(.'+a.slideDuplicateClass+")").eq(0).index(),E((function(){t.slideTo(r)}))):t.slideTo(r):r>t.slides.length-s?(t.loopFix(),r=i.children("."+a.slideClass+'[data-swiper-slide-index="'+e+'"]:not(.'+a.slideDuplicateClass+")").eq(0).index(),E((function(){t.slideTo(r)}))):t.slideTo(r)}else t.slideTo(r)}},loop:{loopCreate:function(){var e=this,t=r(),a=e.params,i=e.$wrapperEl;i.children("."+a.slideClass+"."+a.slideDuplicateClass).remove();var s=i.children("."+a.slideClass);if(a.loopFillGroupWithBlank){var n=a.slidesPerGroup-s.length%a.slidesPerGroup;if(n!==a.slidesPerGroup){for(var l=0;ls.length&&(e.loopedSlides=s.length);var d=[],p=[];s.each((function(t,a){var i=m(t);a=s.length-e.loopedSlides&&d.push(t),i.attr("data-swiper-slide-index",a)}));for(var u=0;u=0;c-=1)i.prepend(m(d[c].cloneNode(!0)).addClass(a.slideDuplicateClass))},loopFix:function(){var e=this;e.emit("beforeLoopFix");var t,a=e.activeIndex,i=e.slides,s=e.loopedSlides,r=e.allowSlidePrev,n=e.allowSlideNext,l=e.snapGrid,o=e.rtlTranslate;e.allowSlidePrev=!0,e.allowSlideNext=!0;var d=-l[a]-e.getTranslate();if(a=i.length-s){t=-i.length+a+s,t+=s,e.slideTo(t,0,!1,!0)&&0!==d&&e.setTranslate((o?-e.translate:e.translate)-d)}e.allowSlidePrev=r,e.allowSlideNext=n,e.emit("loopFix")},loopDestroy:function(){var e=this,t=e.$wrapperEl,a=e.params,i=e.slides;t.children("."+a.slideClass+"."+a.slideDuplicateClass+",."+a.slideClass+"."+a.slideBlankClass).remove(),i.removeAttr("data-swiper-slide-index")}},grabCursor:{setGrabCursor:function(e){var t=this;if(!(t.support.touch||!t.params.simulateTouch||t.params.watchOverflow&&t.isLocked||t.params.cssMode)){var a=t.el;a.style.cursor="move",a.style.cursor=e?"-webkit-grabbing":"-webkit-grab",a.style.cursor=e?"-moz-grabbin":"-moz-grab",a.style.cursor=e?"grabbing":"grab"}},unsetGrabCursor:function(){var e=this;e.support.touch||e.params.watchOverflow&&e.isLocked||e.params.cssMode||(e.el.style.cursor="")}},manipulation:{appendSlide:function(e){var t=this,a=t.$wrapperEl,i=t.params;if(i.loop&&t.loopDestroy(),"object"==typeof e&&"length"in e)for(var s=0;s=n)a.appendSlide(t);else{for(var l=r>e?r+1:r,o=[],d=n-1;d>=e;d-=1){var p=a.slides.eq(d);p.remove(),o.unshift(p)}if("object"==typeof t&&"length"in t){for(var u=0;ue?r+t.length:r}else i.append(t);for(var c=0;c1,c=p.slidesPerColumn>1,h=r.enabled;u&&!c?(n.removeClass(r.containerModifierClass+"multirow "+r.containerModifierClass+"multirow-column"),e.emitContainerClasses()):!u&&c&&(n.addClass(r.containerModifierClass+"multirow"),(p.slidesPerColumnFill&&"column"===p.slidesPerColumnFill||!p.slidesPerColumnFill&&"column"===r.slidesPerColumnFill)&&n.addClass(r.containerModifierClass+"multirow-column"),e.emitContainerClasses());var v=p.direction&&p.direction!==r.direction,f=r.loop&&(p.slidesPerView!==r.slidesPerView||v);v&&a&&e.changeDirection(),M(e.params,p);var m=e.params.enabled;M(e,{allowTouchMove:e.params.allowTouchMove,allowSlideNext:e.params.allowSlideNext,allowSlidePrev:e.params.allowSlidePrev}),h&&!m?e.disable():!h&&m&&e.enable(),e.currentBreakpoint=o,e.emit("_beforeBreakpoint",p),f&&a&&(e.loopDestroy(),e.loopCreate(),e.updateSlides(),e.slideTo(t-s+e.loopedSlides,0,!1)),e.emit("breakpoint",p)}}},getBreakpoint:function(e,t,a){if(void 0===t&&(t="window"),e&&("container"!==t||a)){var i=!1,s=l(),r="window"===t?s.innerHeight:a.clientHeight,n=Object.keys(e).map((function(e){if("string"==typeof e&&0===e.indexOf("@")){var t=parseFloat(e.substr(1));return{value:r*t,point:e}}return{value:e,point:e}}));n.sort((function(e,t){return parseInt(e.value,10)-parseInt(t.value,10)}));for(var o=0;o0&&t.slidesOffsetBefore+t.spaceBetween*(e.slides.length-1)+e.slides[0].offsetWidth*e.slides.length;t.slidesOffsetBefore&&t.slidesOffsetAfter&&i?e.isLocked=i<=e.size:e.isLocked=1===e.snapGrid.length,e.allowSlideNext=!e.isLocked,e.allowSlidePrev=!e.isLocked,a!==e.isLocked&&e.emit(e.isLocked?"lock":"unlock"),a&&a!==e.isLocked&&(e.isEnd=!1,e.navigation&&e.navigation.update())}},classes:{addClasses:function(){var e,t,a,i=this,s=i.classNames,r=i.params,n=i.rtl,l=i.$el,o=i.device,d=i.support,p=(e=["initialized",r.direction,{"pointer-events":d.pointerEvents&&!d.touch},{"free-mode":r.freeMode},{autoheight:r.autoHeight},{rtl:n},{multirow:r.slidesPerColumn>1},{"multirow-column":r.slidesPerColumn>1&&"column"===r.slidesPerColumnFill},{android:o.android},{ios:o.ios},{"css-mode":r.cssMode}],t=r.containerModifierClass,a=[],e.forEach((function(e){"object"==typeof e?Object.keys(e).forEach((function(i){e[i]&&a.push(t+i)})):"string"==typeof e&&a.push(t+e)})),a);s.push.apply(s,p),l.addClass([].concat(s).join(" ")),i.emitContainerClasses()},removeClasses:function(){var e=this,t=e.$el,a=e.classNames;t.removeClass(a.join(" ")),e.emitContainerClasses()}},images:{loadImage:function(e,t,a,i,s,r){var n,o=l();function d(){r&&r()}m(e).parent("picture")[0]||e.complete&&s?d():t?((n=new o.Image).onload=d,n.onerror=d,i&&(n.sizes=i),a&&(n.srcset=a),t&&(n.src=t)):d()},preloadImages:function(){var e=this;function t(){null!=e&&e&&!e.destroyed&&(void 0!==e.imagesLoaded&&(e.imagesLoaded+=1),e.imagesLoaded===e.imagesToLoad.length&&(e.params.updateOnImagesReady&&e.update(),e.emit("imagesReady")))}e.imagesToLoad=e.$el.find("img");for(var a=0;a1){var n=[];return m(a.el).each((function(e){var i=M({},a,{el:e});n.push(new t(i))})),n}var l=this;l.__swiper__=!0,l.support=$(),l.device=L({userAgent:a.userAgent}),l.browser=I(),l.eventsListeners={},l.eventsAnyListeners=[],void 0===l.modules&&(l.modules={}),Object.keys(l.modules).forEach((function(e){var t=l.modules[e];if(t.params){var i=Object.keys(t.params)[0],s=t.params[i];if("object"!=typeof s||null===s)return;if(["navigation","pagination","scrollbar"].indexOf(i)>=0&&!0===a[i]&&(a[i]={auto:!0}),!(i in a)||!("enabled"in s))return;!0===a[i]&&(a[i]={enabled:!0}),"object"!=typeof a[i]||"enabled"in a[i]||(a[i].enabled=!0),a[i]||(a[i]={enabled:!1})}}));var o,d,p=M({},W);return l.useParams(p),l.params=M({},p,_,a),l.originalParams=M({},l.params),l.passedParams=M({},a),l.params&&l.params.on&&Object.keys(l.params.on).forEach((function(e){l.on(e,l.params.on[e])})),l.params&&l.params.onAny&&l.onAny(l.params.onAny),l.$=m,M(l,{enabled:l.params.enabled,el:e,classNames:[],slides:m(),slidesGrid:[],snapGrid:[],slidesSizesGrid:[],isHorizontal:function(){return"horizontal"===l.params.direction},isVertical:function(){return"vertical"===l.params.direction},activeIndex:0,realIndex:0,isBeginning:!0,isEnd:!1,translate:0,previousTranslate:0,progress:0,velocity:0,animating:!1,allowSlideNext:l.params.allowSlideNext,allowSlidePrev:l.params.allowSlidePrev,touchEvents:(o=["touchstart","touchmove","touchend","touchcancel"],d=["mousedown","mousemove","mouseup"],l.support.pointerEvents&&(d=["pointerdown","pointermove","pointerup"]),l.touchEventsTouch={start:o[0],move:o[1],end:o[2],cancel:o[3]},l.touchEventsDesktop={start:d[0],move:d[1],end:d[2]},l.support.touch||!l.params.simulateTouch?l.touchEventsTouch:l.touchEventsDesktop),touchEventsData:{isTouched:void 0,isMoved:void 0,allowTouchCallbacks:void 0,touchStartTime:void 0,isScrolling:void 0,currentTranslate:void 0,startTranslate:void 0,allowThresholdMove:void 0,focusableElements:l.params.focusableElements,lastClickTime:x(),clickTimeout:void 0,velocities:[],allowMomentumBounce:void 0,isTouchEvent:void 0,startMoving:void 0},allowClick:!0,allowTouchMove:l.params.allowTouchMove,touches:{startX:0,startY:0,currentX:0,currentY:0,diff:0},imagesToLoad:[],imagesLoaded:0}),l.useModules(),l.emit("_swiper"),l.params.init&&l.init(),l}var a,i,s,n=t.prototype;return n.enable=function(){var e=this;e.enabled||(e.enabled=!0,e.params.grabCursor&&e.setGrabCursor(),e.emit("enable"))},n.disable=function(){var e=this;e.enabled&&(e.enabled=!1,e.params.grabCursor&&e.unsetGrabCursor(),e.emit("disable"))},n.setProgress=function(e,t){var a=this;e=Math.min(Math.max(e,0),1);var i=a.minTranslate(),s=(a.maxTranslate()-i)*e+i;a.translateTo(s,void 0===t?0:t),a.updateActiveIndex(),a.updateSlidesClasses()},n.emitContainerClasses=function(){var e=this;if(e.params._emitClasses&&e.el){var t=e.el.className.split(" ").filter((function(t){return 0===t.indexOf("swiper-container")||0===t.indexOf(e.params.containerModifierClass)}));e.emit("_containerClasses",t.join(" "))}},n.getSlideClasses=function(e){var t=this;return e.className.split(" ").filter((function(e){return 0===e.indexOf("swiper-slide")||0===e.indexOf(t.params.slideClass)})).join(" ")},n.emitSlidesClasses=function(){var e=this;if(e.params._emitClasses&&e.el){var t=[];e.slides.each((function(a){var i=e.getSlideClasses(a);t.push({slideEl:a,classNames:i}),e.emit("_slideClass",a,i)})),e.emit("_slideClasses",t)}},n.slidesPerViewDynamic=function(){var e=this,t=e.params,a=e.slides,i=e.slidesGrid,s=e.size,r=e.activeIndex,n=1;if(t.centeredSlides){for(var l,o=a[r].swiperSlideSize,d=r+1;ds&&(l=!0));for(var p=r-1;p>=0;p-=1)a[p]&&!l&&(n+=1,(o+=a[p].swiperSlideSize)>s&&(l=!0))}else for(var u=r+1;u1)&&e.isEnd&&!e.params.centeredSlides?e.slideTo(e.slides.length-1,0,!1,!0):e.slideTo(e.activeIndex,0,!1,!0))||i(),a.watchOverflow&&t!==e.snapGrid&&e.checkOverflow(),e.emit("update")}function i(){var t=e.rtlTranslate?-1*e.translate:e.translate,a=Math.min(Math.max(t,e.maxTranslate()),e.minTranslate());e.setTranslate(a),e.updateActiveIndex(),e.updateSlidesClasses()}},n.changeDirection=function(e,t){void 0===t&&(t=!0);var a=this,i=a.params.direction;return e||(e="horizontal"===i?"vertical":"horizontal"),e===i||"horizontal"!==e&&"vertical"!==e||(a.$el.removeClass(""+a.params.containerModifierClass+i).addClass(""+a.params.containerModifierClass+e),a.emitContainerClasses(),a.params.direction=e,a.slides.each((function(t){"vertical"===e?t.style.width="":t.style.height=""})),a.emit("changeDirection"),t&&a.update()),a},n.mount=function(e){var t=this;if(t.mounted)return!0;var a=m(e||t.params.el);if(!(e=a[0]))return!1;e.swiper=t;var i=function(){return"."+(t.params.wrapperClass||"").trim().split(" ").join(".")},s=function(){if(e&&e.shadowRoot&&e.shadowRoot.querySelector){var t=m(e.shadowRoot.querySelector(i()));return t.children=function(e){return a.children(e)},t}return a.children(i())}();if(0===s.length&&t.params.createElements){var n=r().createElement("div");s=m(n),n.className=t.params.wrapperClass,a.append(n),a.children("."+t.params.slideClass).each((function(e){s.append(e)}))}return M(t,{$el:a,el:e,$wrapperEl:s,wrapperEl:s[0],mounted:!0,rtl:"rtl"===e.dir.toLowerCase()||"rtl"===a.css("direction"),rtlTranslate:"horizontal"===t.params.direction&&("rtl"===e.dir.toLowerCase()||"rtl"===a.css("direction")),wrongRTL:"-webkit-box"===s.css("display")}),!0},n.init=function(e){var t=this;return t.initialized||!1===t.mount(e)||(t.emit("beforeInit"),t.params.breakpoints&&t.setBreakpoint(),t.addClasses(),t.params.loop&&t.loopCreate(),t.updateSize(),t.updateSlides(),t.params.watchOverflow&&t.checkOverflow(),t.params.grabCursor&&t.enabled&&t.setGrabCursor(),t.params.preloadImages&&t.preloadImages(),t.params.loop?t.slideTo(t.params.initialSlide+t.loopedSlides,0,t.params.runCallbacksOnInit,!1,!0):t.slideTo(t.params.initialSlide,0,t.params.runCallbacksOnInit,!1,!0),t.attachEvents(),t.initialized=!0,t.emit("init"),t.emit("afterInit")),t},n.destroy=function(e,t){void 0===e&&(e=!0),void 0===t&&(t=!0);var a,i=this,s=i.params,r=i.$el,n=i.$wrapperEl,l=i.slides;return void 0===i.params||i.destroyed||(i.emit("beforeDestroy"),i.initialized=!1,i.detachEvents(),s.loop&&i.loopDestroy(),t&&(i.removeClasses(),r.removeAttr("style"),n.removeAttr("style"),l&&l.length&&l.removeClass([s.slideVisibleClass,s.slideActiveClass,s.slideNextClass,s.slidePrevClass].join(" ")).removeAttr("style").removeAttr("data-swiper-slide-index")),i.emit("destroy"),Object.keys(i.eventsListeners).forEach((function(e){i.off(e)})),!1!==e&&(i.$el[0].swiper=null,a=i,Object.keys(a).forEach((function(e){try{a[e]=null}catch(e){}try{delete a[e]}catch(e){}}))),i.destroyed=!0),null},t.extendDefaults=function(e){M(_,e)},t.installModule=function(e){t.prototype.modules||(t.prototype.modules={});var a=e.name||Object.keys(t.prototype.modules).length+"_"+x();t.prototype.modules[a]=e},t.use=function(e){return Array.isArray(e)?(e.forEach((function(e){return t.installModule(e)})),t):(t.installModule(e),t)},a=t,s=[{key:"extendedDefaults",get:function(){return _}},{key:"defaults",get:function(){return W}}],(i=null)&&e(a.prototype,i),s&&e(a,s),t}();Object.keys(F).forEach((function(e){Object.keys(F[e]).forEach((function(t){q.prototype[t]=F[e][t]}))})),q.use([O,D]);var j={update:function(e){var t=this,a=t.params,i=a.slidesPerView,s=a.slidesPerGroup,r=a.centeredSlides,n=t.params.virtual,l=n.addSlidesBefore,o=n.addSlidesAfter,d=t.virtual,p=d.from,u=d.to,c=d.slides,h=d.slidesGrid,v=d.renderSlide,f=d.offset;t.updateActiveIndex();var m,g,b,w=t.activeIndex||0;m=t.rtlTranslate?"right":t.isHorizontal()?"left":"top",r?(g=Math.floor(i/2)+s+o,b=Math.floor(i/2)+s+l):(g=i+(s-1)+o,b=s+l);var y=Math.max((w||0)-b,0),E=Math.min((w||0)+g,c.length-1),x=(t.slidesGrid[y]||0)-(t.slidesGrid[0]||0);function T(){t.updateSlides(),t.updateProgress(),t.updateSlidesClasses(),t.lazy&&t.params.lazy.enabled&&t.lazy.load()}if(M(t.virtual,{from:y,to:E,offset:x,slidesGrid:t.slidesGrid}),p===y&&u===E&&!e)return t.slidesGrid!==h&&x!==f&&t.slides.css(m,x+"px"),void t.updateProgress();if(t.params.virtual.renderExternal)return t.params.virtual.renderExternal.call(t,{offset:x,from:y,to:E,slides:function(){for(var e=[],t=y;t<=E;t+=1)e.push(c[t]);return e}()}),void(t.params.virtual.renderExternalUpdate&&T());var C=[],S=[];if(e)t.$wrapperEl.find("."+t.params.slideClass).remove();else for(var z=p;z<=u;z+=1)(zE)&&t.$wrapperEl.find("."+t.params.slideClass+'[data-swiper-slide-index="'+z+'"]').remove();for(var P=0;P=y&&P<=E&&(void 0===u||e?S.push(P):(P>u&&S.push(P),P'+e+"
    ");return s.attr("data-swiper-slide-index")||s.attr("data-swiper-slide-index",t),i.cache&&(a.virtual.cache[t]=s),s},appendSlide:function(e){var t=this;if("object"==typeof e&&"length"in e)for(var a=0;a=0;i-=1)t.virtual.slides.splice(e[i],1),t.params.virtual.cache&&delete t.virtual.cache[e[i]],e[i]0&&0===t.$el.parents("."+t.params.slideActiveClass).length)return;var g=t.$el,b=g[0].clientWidth,w=g[0].clientHeight,y=a.innerWidth,E=a.innerHeight,x=t.$el.offset();s&&(x.left-=t.$el[0].scrollLeft);for(var T=[[x.left,x.top],[x.left+b,x.top],[x.left,x.top+w],[x.left+b,x.top+w]],C=0;C=0&&S[0]<=y&&S[1]>=0&&S[1]<=E){if(0===S[0]&&0===S[1])continue;m=!0}}if(!m)return}t.isHorizontal()?((p||u||c||h)&&(n.preventDefault?n.preventDefault():n.returnValue=!1),((u||h)&&!s||(p||c)&&s)&&t.slideNext(),((p||c)&&!s||(u||h)&&s)&&t.slidePrev()):((p||u||v||f)&&(n.preventDefault?n.preventDefault():n.returnValue=!1),(u||f)&&t.slideNext(),(p||v)&&t.slidePrev()),t.emit("keyPress",o)}}},enable:function(){var e=this,t=r();e.keyboard.enabled||(m(t).on("keydown",e.keyboard.handle),e.keyboard.enabled=!0)},disable:function(){var e=this,t=r();e.keyboard.enabled&&(m(t).off("keydown",e.keyboard.handle),e.keyboard.enabled=!1)}},Z={name:"keyboard",params:{keyboard:{enabled:!1,onlyInViewport:!0,pageUpDown:!0}},create:function(){z(this,{keyboard:t({enabled:!1},K)})},on:{init:function(e){e.params.keyboard.enabled&&e.keyboard.enable()},destroy:function(e){e.keyboard.enabled&&e.keyboard.disable()}}};var J={lastScrollTime:x(),lastEventBeforeSnap:void 0,recentWheelEvents:[],event:function(){return l().navigator.userAgent.indexOf("firefox")>-1?"DOMMouseScroll":function(){var e=r(),t="onwheel",a=t in e;if(!a){var i=e.createElement("div");i.setAttribute(t,"return;"),a="function"==typeof i.onwheel}return!a&&e.implementation&&e.implementation.hasFeature&&!0!==e.implementation.hasFeature("","")&&(a=e.implementation.hasFeature("Events.wheel","3.0")),a}()?"wheel":"mousewheel"},normalize:function(e){var t=0,a=0,i=0,s=0;return"detail"in e&&(a=e.detail),"wheelDelta"in e&&(a=-e.wheelDelta/120),"wheelDeltaY"in e&&(a=-e.wheelDeltaY/120),"wheelDeltaX"in e&&(t=-e.wheelDeltaX/120),"axis"in e&&e.axis===e.HORIZONTAL_AXIS&&(t=a,a=0),i=10*t,s=10*a,"deltaY"in e&&(s=e.deltaY),"deltaX"in e&&(i=e.deltaX),e.shiftKey&&!i&&(i=s,s=0),(i||s)&&e.deltaMode&&(1===e.deltaMode?(i*=40,s*=40):(i*=800,s*=800)),i&&!t&&(t=i<1?-1:1),s&&!a&&(a=s<1?-1:1),{spinX:t,spinY:a,pixelX:i,pixelY:s}},handleMouseEnter:function(){this.enabled&&(this.mouseEntered=!0)},handleMouseLeave:function(){this.enabled&&(this.mouseEntered=!1)},handle:function(e){var t=e,a=this;if(a.enabled){var i=a.params.mousewheel;a.params.cssMode&&t.preventDefault();var s=a.$el;if("container"!==a.params.mousewheel.eventsTarget&&(s=m(a.params.mousewheel.eventsTarget)),!a.mouseEntered&&!s[0].contains(t.target)&&!i.releaseOnEdges)return!0;t.originalEvent&&(t=t.originalEvent);var r=0,n=a.rtlTranslate?-1:1,l=J.normalize(t);if(i.forceToAxis)if(a.isHorizontal()){if(!(Math.abs(l.pixelX)>Math.abs(l.pixelY)))return!0;r=-l.pixelX*n}else{if(!(Math.abs(l.pixelY)>Math.abs(l.pixelX)))return!0;r=-l.pixelY}else r=Math.abs(l.pixelX)>Math.abs(l.pixelY)?-l.pixelX*n:-l.pixelY;if(0===r)return!0;i.invert&&(r=-r);var o=a.getTranslate()+r*i.sensitivity;if(o>=a.minTranslate()&&(o=a.minTranslate()),o<=a.maxTranslate()&&(o=a.maxTranslate()),(!!a.params.loop||!(o===a.minTranslate()||o===a.maxTranslate()))&&a.params.nested&&t.stopPropagation(),a.params.freeMode){var d={time:x(),delta:Math.abs(r),direction:Math.sign(r)},p=a.mousewheel.lastEventBeforeSnap,u=p&&d.time=a.minTranslate()&&(c=a.minTranslate()),c<=a.maxTranslate()&&(c=a.maxTranslate()),a.setTransition(0),a.setTranslate(c),a.updateProgress(),a.updateActiveIndex(),a.updateSlidesClasses(),(!h&&a.isBeginning||!v&&a.isEnd)&&a.updateSlidesClasses(),a.params.freeModeSticky){clearTimeout(a.mousewheel.timeout),a.mousewheel.timeout=void 0;var f=a.mousewheel.recentWheelEvents;f.length>=15&&f.shift();var g=f.length?f[f.length-1]:void 0,b=f[0];if(f.push(d),g&&(d.delta>g.delta||d.direction!==g.direction))f.splice(0);else if(f.length>=15&&d.time-b.time<500&&b.delta-d.delta>=1&&d.delta<=6){var w=r>0?.8:.2;a.mousewheel.lastEventBeforeSnap=d,f.splice(0),a.mousewheel.timeout=E((function(){a.slideToClosest(a.params.speed,!0,void 0,w)}),0)}a.mousewheel.timeout||(a.mousewheel.timeout=E((function(){a.mousewheel.lastEventBeforeSnap=d,f.splice(0),a.slideToClosest(a.params.speed,!0,void 0,.5)}),500))}if(u||a.emit("scroll",t),a.params.autoplay&&a.params.autoplayDisableOnInteraction&&a.autoplay.stop(),c===a.minTranslate()||c===a.maxTranslate())return!0}}else{var y={time:x(),delta:Math.abs(r),direction:Math.sign(r),raw:e},T=a.mousewheel.recentWheelEvents;T.length>=2&&T.shift();var C=T.length?T[T.length-1]:void 0;if(T.push(y),C?(y.direction!==C.direction||y.delta>C.delta||y.time>C.time+150)&&a.mousewheel.animateSlider(y):a.mousewheel.animateSlider(y),a.mousewheel.releaseScroll(y))return!0}return t.preventDefault?t.preventDefault():t.returnValue=!1,!1}},animateSlider:function(e){var t=this,a=l();return!(this.params.mousewheel.thresholdDelta&&e.delta=6&&x()-t.mousewheel.lastScrollTime<60||(e.direction<0?t.isEnd&&!t.params.loop||t.animating||(t.slideNext(),t.emit("scroll",e.raw)):t.isBeginning&&!t.params.loop||t.animating||(t.slidePrev(),t.emit("scroll",e.raw)),t.mousewheel.lastScrollTime=(new a.Date).getTime(),!1)))},releaseScroll:function(e){var t=this,a=t.params.mousewheel;if(e.direction<0){if(t.isEnd&&!t.params.loop&&a.releaseOnEdges)return!0}else if(t.isBeginning&&!t.params.loop&&a.releaseOnEdges)return!0;return!1},enable:function(){var e=this,t=J.event();if(e.params.cssMode)return e.wrapperEl.removeEventListener(t,e.mousewheel.handle),!0;if(!t)return!1;if(e.mousewheel.enabled)return!1;var a=e.$el;return"container"!==e.params.mousewheel.eventsTarget&&(a=m(e.params.mousewheel.eventsTarget)),a.on("mouseenter",e.mousewheel.handleMouseEnter),a.on("mouseleave",e.mousewheel.handleMouseLeave),a.on(t,e.mousewheel.handle),e.mousewheel.enabled=!0,!0},disable:function(){var e=this,t=J.event();if(e.params.cssMode)return e.wrapperEl.addEventListener(t,e.mousewheel.handle),!0;if(!t)return!1;if(!e.mousewheel.enabled)return!1;var a=e.$el;return"container"!==e.params.mousewheel.eventsTarget&&(a=m(e.params.mousewheel.eventsTarget)),a.off(t,e.mousewheel.handle),e.mousewheel.enabled=!1,!0}},Q={toggleEl:function(e,t){e[t?"addClass":"removeClass"](this.params.navigation.disabledClass),e[0]&&"BUTTON"===e[0].tagName&&(e[0].disabled=t)},update:function(){var e=this,t=e.params.navigation,a=e.navigation.toggleEl;if(!e.params.loop){var i=e.navigation,s=i.$nextEl,r=i.$prevEl;r&&r.length>0&&(e.isBeginning?a(r,!0):a(r,!1),e.params.watchOverflow&&e.enabled&&r[e.isLocked?"addClass":"removeClass"](t.lockClass)),s&&s.length>0&&(e.isEnd?a(s,!0):a(s,!1),e.params.watchOverflow&&e.enabled&&s[e.isLocked?"addClass":"removeClass"](t.lockClass))}},onPrevClick:function(e){var t=this;e.preventDefault(),t.isBeginning&&!t.params.loop||t.slidePrev()},onNextClick:function(e){var t=this;e.preventDefault(),t.isEnd&&!t.params.loop||t.slideNext()},init:function(){var e,t,a=this,i=a.params.navigation;(a.params.navigation=k(a.$el,a.params.navigation,a.params.createElements,{nextEl:"swiper-button-next",prevEl:"swiper-button-prev"}),i.nextEl||i.prevEl)&&(i.nextEl&&(e=m(i.nextEl),a.params.uniqueNavElements&&"string"==typeof i.nextEl&&e.length>1&&1===a.$el.find(i.nextEl).length&&(e=a.$el.find(i.nextEl))),i.prevEl&&(t=m(i.prevEl),a.params.uniqueNavElements&&"string"==typeof i.prevEl&&t.length>1&&1===a.$el.find(i.prevEl).length&&(t=a.$el.find(i.prevEl))),e&&e.length>0&&e.on("click",a.navigation.onNextClick),t&&t.length>0&&t.on("click",a.navigation.onPrevClick),M(a.navigation,{$nextEl:e,nextEl:e&&e[0],$prevEl:t,prevEl:t&&t[0]}),a.enabled||(e&&e.addClass(i.lockClass),t&&t.addClass(i.lockClass)))},destroy:function(){var e=this,t=e.navigation,a=t.$nextEl,i=t.$prevEl;a&&a.length&&(a.off("click",e.navigation.onNextClick),a.removeClass(e.params.navigation.disabledClass)),i&&i.length&&(i.off("click",e.navigation.onPrevClick),i.removeClass(e.params.navigation.disabledClass))}},ee={update:function(){var e=this,t=e.rtl,a=e.params.pagination;if(a.el&&e.pagination.el&&e.pagination.$el&&0!==e.pagination.$el.length){var i,s=e.virtual&&e.params.virtual.enabled?e.virtual.slides.length:e.slides.length,r=e.pagination.$el,n=e.params.loop?Math.ceil((s-2*e.loopedSlides)/e.params.slidesPerGroup):e.snapGrid.length;if(e.params.loop?((i=Math.ceil((e.activeIndex-e.loopedSlides)/e.params.slidesPerGroup))>s-1-2*e.loopedSlides&&(i-=s-2*e.loopedSlides),i>n-1&&(i-=n),i<0&&"bullets"!==e.params.paginationType&&(i=n+i)):i=void 0!==e.snapIndex?e.snapIndex:e.activeIndex||0,"bullets"===a.type&&e.pagination.bullets&&e.pagination.bullets.length>0){var l,o,d,p=e.pagination.bullets;if(a.dynamicBullets&&(e.pagination.bulletSize=p.eq(0)[e.isHorizontal()?"outerWidth":"outerHeight"](!0),r.css(e.isHorizontal()?"width":"height",e.pagination.bulletSize*(a.dynamicMainBullets+4)+"px"),a.dynamicMainBullets>1&&void 0!==e.previousIndex&&(e.pagination.dynamicBulletIndex+=i-e.previousIndex,e.pagination.dynamicBulletIndex>a.dynamicMainBullets-1?e.pagination.dynamicBulletIndex=a.dynamicMainBullets-1:e.pagination.dynamicBulletIndex<0&&(e.pagination.dynamicBulletIndex=0)),l=i-e.pagination.dynamicBulletIndex,d=((o=l+(Math.min(p.length,a.dynamicMainBullets)-1))+l)/2),p.removeClass(a.bulletActiveClass+" "+a.bulletActiveClass+"-next "+a.bulletActiveClass+"-next-next "+a.bulletActiveClass+"-prev "+a.bulletActiveClass+"-prev-prev "+a.bulletActiveClass+"-main"),r.length>1)p.each((function(e){var t=m(e),s=t.index();s===i&&t.addClass(a.bulletActiveClass),a.dynamicBullets&&(s>=l&&s<=o&&t.addClass(a.bulletActiveClass+"-main"),s===l&&t.prev().addClass(a.bulletActiveClass+"-prev").prev().addClass(a.bulletActiveClass+"-prev-prev"),s===o&&t.next().addClass(a.bulletActiveClass+"-next").next().addClass(a.bulletActiveClass+"-next-next"))}));else{var u=p.eq(i),c=u.index();if(u.addClass(a.bulletActiveClass),a.dynamicBullets){for(var h=p.eq(l),v=p.eq(o),f=l;f<=o;f+=1)p.eq(f).addClass(a.bulletActiveClass+"-main");if(e.params.loop)if(c>=p.length-a.dynamicMainBullets){for(var g=a.dynamicMainBullets;g>=0;g-=1)p.eq(p.length-g).addClass(a.bulletActiveClass+"-main");p.eq(p.length-a.dynamicMainBullets-1).addClass(a.bulletActiveClass+"-prev")}else h.prev().addClass(a.bulletActiveClass+"-prev").prev().addClass(a.bulletActiveClass+"-prev-prev"),v.next().addClass(a.bulletActiveClass+"-next").next().addClass(a.bulletActiveClass+"-next-next");else h.prev().addClass(a.bulletActiveClass+"-prev").prev().addClass(a.bulletActiveClass+"-prev-prev"),v.next().addClass(a.bulletActiveClass+"-next").next().addClass(a.bulletActiveClass+"-next-next")}}if(a.dynamicBullets){var b=Math.min(p.length,a.dynamicMainBullets+4),w=(e.pagination.bulletSize*b-e.pagination.bulletSize)/2-d*e.pagination.bulletSize,y=t?"right":"left";p.css(e.isHorizontal()?y:"top",w+"px")}}if("fraction"===a.type&&(r.find(P(a.currentClass)).text(a.formatFractionCurrent(i+1)),r.find(P(a.totalClass)).text(a.formatFractionTotal(n))),"progressbar"===a.type){var E;E=a.progressbarOpposite?e.isHorizontal()?"vertical":"horizontal":e.isHorizontal()?"horizontal":"vertical";var x=(i+1)/n,T=1,C=1;"horizontal"===E?T=x:C=x,r.find(P(a.progressbarFillClass)).transform("translate3d(0,0,0) scaleX("+T+") scaleY("+C+")").transition(e.params.speed)}"custom"===a.type&&a.renderCustom?(r.html(a.renderCustom(e,i+1,n)),e.emit("paginationRender",r[0])):e.emit("paginationUpdate",r[0]),e.params.watchOverflow&&e.enabled&&r[e.isLocked?"addClass":"removeClass"](a.lockClass)}},render:function(){var e=this,t=e.params.pagination;if(t.el&&e.pagination.el&&e.pagination.$el&&0!==e.pagination.$el.length){var a=e.virtual&&e.params.virtual.enabled?e.virtual.slides.length:e.slides.length,i=e.pagination.$el,s="";if("bullets"===t.type){var r=e.params.loop?Math.ceil((a-2*e.loopedSlides)/e.params.slidesPerGroup):e.snapGrid.length;e.params.freeMode&&!e.params.loop&&r>a&&(r=a);for(var n=0;n";i.html(s),e.pagination.bullets=i.find(P(t.bulletClass))}"fraction"===t.type&&(s=t.renderFraction?t.renderFraction.call(e,t.currentClass,t.totalClass):' / ',i.html(s)),"progressbar"===t.type&&(s=t.renderProgressbar?t.renderProgressbar.call(e,t.progressbarFillClass):'',i.html(s)),"custom"!==t.type&&e.emit("paginationRender",e.pagination.$el[0])}},init:function(){var e=this;e.params.pagination=k(e.$el,e.params.pagination,e.params.createElements,{el:"swiper-pagination"});var t=e.params.pagination;if(t.el){var a=m(t.el);0!==a.length&&(e.params.uniqueNavElements&&"string"==typeof t.el&&a.length>1&&(a=e.$el.find(t.el)),"bullets"===t.type&&t.clickable&&a.addClass(t.clickableClass),a.addClass(t.modifierClass+t.type),"bullets"===t.type&&t.dynamicBullets&&(a.addClass(""+t.modifierClass+t.type+"-dynamic"),e.pagination.dynamicBulletIndex=0,t.dynamicMainBullets<1&&(t.dynamicMainBullets=1)),"progressbar"===t.type&&t.progressbarOpposite&&a.addClass(t.progressbarOppositeClass),t.clickable&&a.on("click",P(t.bulletClass),(function(t){t.preventDefault();var a=m(this).index()*e.params.slidesPerGroup;e.params.loop&&(a+=e.loopedSlides),e.slideTo(a)})),M(e.pagination,{$el:a,el:a[0]}),e.enabled||a.addClass(t.lockClass))}},destroy:function(){var e=this,t=e.params.pagination;if(t.el&&e.pagination.el&&e.pagination.$el&&0!==e.pagination.$el.length){var a=e.pagination.$el;a.removeClass(t.hiddenClass),a.removeClass(t.modifierClass+t.type),e.pagination.bullets&&e.pagination.bullets.removeClass(t.bulletActiveClass),t.clickable&&a.off("click",P(t.bulletClass))}}},te={setTranslate:function(){var e=this;if(e.params.scrollbar.el&&e.scrollbar.el){var t=e.scrollbar,a=e.rtlTranslate,i=e.progress,s=t.dragSize,r=t.trackSize,n=t.$dragEl,l=t.$el,o=e.params.scrollbar,d=s,p=(r-s)*i;a?(p=-p)>0?(d=s-p,p=0):-p+s>r&&(d=r+p):p<0?(d=s+p,p=0):p+s>r&&(d=r-p),e.isHorizontal()?(n.transform("translate3d("+p+"px, 0, 0)"),n[0].style.width=d+"px"):(n.transform("translate3d(0px, "+p+"px, 0)"),n[0].style.height=d+"px"),o.hide&&(clearTimeout(e.scrollbar.timeout),l[0].style.opacity=1,e.scrollbar.timeout=setTimeout((function(){l[0].style.opacity=0,l.transition(400)}),1e3))}},setTransition:function(e){var t=this;t.params.scrollbar.el&&t.scrollbar.el&&t.scrollbar.$dragEl.transition(e)},updateSize:function(){var e=this;if(e.params.scrollbar.el&&e.scrollbar.el){var t=e.scrollbar,a=t.$dragEl,i=t.$el;a[0].style.width="",a[0].style.height="";var s,r=e.isHorizontal()?i[0].offsetWidth:i[0].offsetHeight,n=e.size/e.virtualSize,l=n*(r/e.size);s="auto"===e.params.scrollbar.dragSize?r*n:parseInt(e.params.scrollbar.dragSize,10),e.isHorizontal()?a[0].style.width=s+"px":a[0].style.height=s+"px",i[0].style.display=n>=1?"none":"",e.params.scrollbar.hide&&(i[0].style.opacity=0),M(t,{trackSize:r,divider:n,moveDivider:l,dragSize:s}),e.params.watchOverflow&&e.enabled&&t.$el[e.isLocked?"addClass":"removeClass"](e.params.scrollbar.lockClass)}},getPointerPosition:function(e){return this.isHorizontal()?"touchstart"===e.type||"touchmove"===e.type?e.targetTouches[0].clientX:e.clientX:"touchstart"===e.type||"touchmove"===e.type?e.targetTouches[0].clientY:e.clientY},setDragPosition:function(e){var t,a=this,i=a.scrollbar,s=a.rtlTranslate,r=i.$el,n=i.dragSize,l=i.trackSize,o=i.dragStartPos;t=(i.getPointerPosition(e)-r.offset()[a.isHorizontal()?"left":"top"]-(null!==o?o:n/2))/(l-n),t=Math.max(Math.min(t,1),0),s&&(t=1-t);var d=a.minTranslate()+(a.maxTranslate()-a.minTranslate())*t;a.updateProgress(d),a.setTranslate(d),a.updateActiveIndex(),a.updateSlidesClasses()},onDragStart:function(e){var t=this,a=t.params.scrollbar,i=t.scrollbar,s=t.$wrapperEl,r=i.$el,n=i.$dragEl;t.scrollbar.isTouched=!0,t.scrollbar.dragStartPos=e.target===n[0]||e.target===n?i.getPointerPosition(e)-e.target.getBoundingClientRect()[t.isHorizontal()?"left":"top"]:null,e.preventDefault(),e.stopPropagation(),s.transition(100),n.transition(100),i.setDragPosition(e),clearTimeout(t.scrollbar.dragTimeout),r.transition(0),a.hide&&r.css("opacity",1),t.params.cssMode&&t.$wrapperEl.css("scroll-snap-type","none"),t.emit("scrollbarDragStart",e)},onDragMove:function(e){var t=this,a=t.scrollbar,i=t.$wrapperEl,s=a.$el,r=a.$dragEl;t.scrollbar.isTouched&&(e.preventDefault?e.preventDefault():e.returnValue=!1,a.setDragPosition(e),i.transition(0),s.transition(0),r.transition(0),t.emit("scrollbarDragMove",e))},onDragEnd:function(e){var t=this,a=t.params.scrollbar,i=t.scrollbar,s=t.$wrapperEl,r=i.$el;t.scrollbar.isTouched&&(t.scrollbar.isTouched=!1,t.params.cssMode&&(t.$wrapperEl.css("scroll-snap-type",""),s.transition("")),a.hide&&(clearTimeout(t.scrollbar.dragTimeout),t.scrollbar.dragTimeout=E((function(){r.css("opacity",0),r.transition(400)}),1e3)),t.emit("scrollbarDragEnd",e),a.snapOnRelease&&t.slideToClosest())},enableDraggable:function(){var e=this;if(e.params.scrollbar.el){var t=r(),a=e.scrollbar,i=e.touchEventsTouch,s=e.touchEventsDesktop,n=e.params,l=e.support,o=a.$el[0],d=!(!l.passiveListener||!n.passiveListeners)&&{passive:!1,capture:!1},p=!(!l.passiveListener||!n.passiveListeners)&&{passive:!0,capture:!1};o&&(l.touch?(o.addEventListener(i.start,e.scrollbar.onDragStart,d),o.addEventListener(i.move,e.scrollbar.onDragMove,d),o.addEventListener(i.end,e.scrollbar.onDragEnd,p)):(o.addEventListener(s.start,e.scrollbar.onDragStart,d),t.addEventListener(s.move,e.scrollbar.onDragMove,d),t.addEventListener(s.end,e.scrollbar.onDragEnd,p)))}},disableDraggable:function(){var e=this;if(e.params.scrollbar.el){var t=r(),a=e.scrollbar,i=e.touchEventsTouch,s=e.touchEventsDesktop,n=e.params,l=e.support,o=a.$el[0],d=!(!l.passiveListener||!n.passiveListeners)&&{passive:!1,capture:!1},p=!(!l.passiveListener||!n.passiveListeners)&&{passive:!0,capture:!1};o&&(l.touch?(o.removeEventListener(i.start,e.scrollbar.onDragStart,d),o.removeEventListener(i.move,e.scrollbar.onDragMove,d),o.removeEventListener(i.end,e.scrollbar.onDragEnd,p)):(o.removeEventListener(s.start,e.scrollbar.onDragStart,d),t.removeEventListener(s.move,e.scrollbar.onDragMove,d),t.removeEventListener(s.end,e.scrollbar.onDragEnd,p)))}},init:function(){var e=this,t=e.scrollbar,a=e.$el;e.params.scrollbar=k(a,e.params.scrollbar,e.params.createElements,{el:"swiper-scrollbar"});var i=e.params.scrollbar;if(i.el){var s=m(i.el);e.params.uniqueNavElements&&"string"==typeof i.el&&s.length>1&&1===a.find(i.el).length&&(s=a.find(i.el));var r=s.find("."+e.params.scrollbar.dragClass);0===r.length&&(r=m('
    '),s.append(r)),M(t,{$el:s,el:s[0],$dragEl:r,dragEl:r[0]}),i.draggable&&t.enableDraggable(),s&&s[e.enabled?"removeClass":"addClass"](e.params.scrollbar.lockClass)}},destroy:function(){this.scrollbar.disableDraggable()}},ae={setTransform:function(e,t){var a=this.rtl,i=m(e),s=a?-1:1,r=i.attr("data-swiper-parallax")||"0",n=i.attr("data-swiper-parallax-x"),l=i.attr("data-swiper-parallax-y"),o=i.attr("data-swiper-parallax-scale"),d=i.attr("data-swiper-parallax-opacity");if(n||l?(n=n||"0",l=l||"0"):this.isHorizontal()?(n=r,l="0"):(l=r,n="0"),n=n.indexOf("%")>=0?parseInt(n,10)*t*s+"%":n*t*s+"px",l=l.indexOf("%")>=0?parseInt(l,10)*t+"%":l*t+"px",null!=d){var p=d-(d-1)*(1-Math.abs(t));i[0].style.opacity=p}if(null==o)i.transform("translate3d("+n+", "+l+", 0px)");else{var u=o-(o-1)*(1-Math.abs(t));i.transform("translate3d("+n+", "+l+", 0px) scale("+u+")")}},setTranslate:function(){var e=this,t=e.$el,a=e.slides,i=e.progress,s=e.snapGrid;t.children("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((function(t){e.parallax.setTransform(t,i)})),a.each((function(t,a){var r=t.progress;e.params.slidesPerGroup>1&&"auto"!==e.params.slidesPerView&&(r+=Math.ceil(a/2)-i*(s.length-1)),r=Math.min(Math.max(r,-1),1),m(t).find("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((function(t){e.parallax.setTransform(t,r)}))}))},setTransition:function(e){void 0===e&&(e=this.params.speed);this.$el.find("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((function(t){var a=m(t),i=parseInt(a.attr("data-swiper-parallax-duration"),10)||e;0===e&&(i=0),a.transition(i)}))}},ie={getDistanceBetweenTouches:function(e){if(e.targetTouches.length<2)return 1;var t=e.targetTouches[0].pageX,a=e.targetTouches[0].pageY,i=e.targetTouches[1].pageX,s=e.targetTouches[1].pageY;return Math.sqrt(Math.pow(i-t,2)+Math.pow(s-a,2))},onGestureStart:function(e){var t=this,a=t.support,i=t.params.zoom,s=t.zoom,r=s.gesture;if(s.fakeGestureTouched=!1,s.fakeGestureMoved=!1,!a.gestures){if("touchstart"!==e.type||"touchstart"===e.type&&e.targetTouches.length<2)return;s.fakeGestureTouched=!0,r.scaleStart=ie.getDistanceBetweenTouches(e)}r.$slideEl&&r.$slideEl.length||(r.$slideEl=m(e.target).closest("."+t.params.slideClass),0===r.$slideEl.length&&(r.$slideEl=t.slides.eq(t.activeIndex)),r.$imageEl=r.$slideEl.find("img, svg, canvas, picture, .swiper-zoom-target"),r.$imageWrapEl=r.$imageEl.parent("."+i.containerClass),r.maxRatio=r.$imageWrapEl.attr("data-swiper-zoom")||i.maxRatio,0!==r.$imageWrapEl.length)?(r.$imageEl&&r.$imageEl.transition(0),t.zoom.isScaling=!0):r.$imageEl=void 0},onGestureChange:function(e){var t=this,a=t.support,i=t.params.zoom,s=t.zoom,r=s.gesture;if(!a.gestures){if("touchmove"!==e.type||"touchmove"===e.type&&e.targetTouches.length<2)return;s.fakeGestureMoved=!0,r.scaleMove=ie.getDistanceBetweenTouches(e)}r.$imageEl&&0!==r.$imageEl.length?(a.gestures?s.scale=e.scale*s.currentScale:s.scale=r.scaleMove/r.scaleStart*s.currentScale,s.scale>r.maxRatio&&(s.scale=r.maxRatio-1+Math.pow(s.scale-r.maxRatio+1,.5)),s.scales.touchesStart.x))return void(s.isTouched=!1);if(!t.isHorizontal()&&(Math.floor(s.minY)===Math.floor(s.startY)&&s.touchesCurrent.ys.touchesStart.y))return void(s.isTouched=!1)}e.cancelable&&e.preventDefault(),e.stopPropagation(),s.isMoved=!0,s.currentX=s.touchesCurrent.x-s.touchesStart.x+s.startX,s.currentY=s.touchesCurrent.y-s.touchesStart.y+s.startY,s.currentXs.maxX&&(s.currentX=s.maxX-1+Math.pow(s.currentX-s.maxX+1,.8)),s.currentYs.maxY&&(s.currentY=s.maxY-1+Math.pow(s.currentY-s.maxY+1,.8)),r.prevPositionX||(r.prevPositionX=s.touchesCurrent.x),r.prevPositionY||(r.prevPositionY=s.touchesCurrent.y),r.prevTime||(r.prevTime=Date.now()),r.x=(s.touchesCurrent.x-r.prevPositionX)/(Date.now()-r.prevTime)/2,r.y=(s.touchesCurrent.y-r.prevPositionY)/(Date.now()-r.prevTime)/2,Math.abs(s.touchesCurrent.x-r.prevPositionX)<2&&(r.x=0),Math.abs(s.touchesCurrent.y-r.prevPositionY)<2&&(r.y=0),r.prevPositionX=s.touchesCurrent.x,r.prevPositionY=s.touchesCurrent.y,r.prevTime=Date.now(),i.$imageWrapEl.transform("translate3d("+s.currentX+"px, "+s.currentY+"px,0)")}}},onTouchEnd:function(){var e=this.zoom,t=e.gesture,a=e.image,i=e.velocity;if(t.$imageEl&&0!==t.$imageEl.length){if(!a.isTouched||!a.isMoved)return a.isTouched=!1,void(a.isMoved=!1);a.isTouched=!1,a.isMoved=!1;var s=300,r=300,n=i.x*s,l=a.currentX+n,o=i.y*r,d=a.currentY+o;0!==i.x&&(s=Math.abs((l-a.currentX)/i.x)),0!==i.y&&(r=Math.abs((d-a.currentY)/i.y));var p=Math.max(s,r);a.currentX=l,a.currentY=d;var u=a.width*e.scale,c=a.height*e.scale;a.minX=Math.min(t.slideWidth/2-u/2,0),a.maxX=-a.minX,a.minY=Math.min(t.slideHeight/2-c/2,0),a.maxY=-a.minY,a.currentX=Math.max(Math.min(a.currentX,a.maxX),a.minX),a.currentY=Math.max(Math.min(a.currentY,a.maxY),a.minY),t.$imageWrapEl.transition(p).transform("translate3d("+a.currentX+"px, "+a.currentY+"px,0)")}},onTransitionEnd:function(){var e=this,t=e.zoom,a=t.gesture;a.$slideEl&&e.previousIndex!==e.activeIndex&&(a.$imageEl&&a.$imageEl.transform("translate3d(0,0,0) scale(1)"),a.$imageWrapEl&&a.$imageWrapEl.transform("translate3d(0,0,0)"),t.scale=1,t.currentScale=1,a.$slideEl=void 0,a.$imageEl=void 0,a.$imageWrapEl=void 0)},toggle:function(e){var t=this.zoom;t.scale&&1!==t.scale?t.out():t.in(e)},in:function(e){var t,a,i,s,r,n,o,d,p,u,c,h,v,f,g,b,w=this,y=l(),E=w.zoom,x=w.params.zoom,T=E.gesture,C=E.image;(T.$slideEl||(e&&e.target&&(T.$slideEl=m(e.target).closest("."+w.params.slideClass)),T.$slideEl||(w.params.virtual&&w.params.virtual.enabled&&w.virtual?T.$slideEl=w.$wrapperEl.children("."+w.params.slideActiveClass):T.$slideEl=w.slides.eq(w.activeIndex)),T.$imageEl=T.$slideEl.find("img, svg, canvas, picture, .swiper-zoom-target"),T.$imageWrapEl=T.$imageEl.parent("."+x.containerClass)),T.$imageEl&&0!==T.$imageEl.length&&T.$imageWrapEl&&0!==T.$imageWrapEl.length)&&(T.$slideEl.addClass(""+x.zoomedSlideClass),void 0===C.touchesStart.x&&e?(t="touchend"===e.type?e.changedTouches[0].pageX:e.pageX,a="touchend"===e.type?e.changedTouches[0].pageY:e.pageY):(t=C.touchesStart.x,a=C.touchesStart.y),E.scale=T.$imageWrapEl.attr("data-swiper-zoom")||x.maxRatio,E.currentScale=T.$imageWrapEl.attr("data-swiper-zoom")||x.maxRatio,e?(g=T.$slideEl[0].offsetWidth,b=T.$slideEl[0].offsetHeight,i=T.$slideEl.offset().left+y.scrollX+g/2-t,s=T.$slideEl.offset().top+y.scrollY+b/2-a,o=T.$imageEl[0].offsetWidth,d=T.$imageEl[0].offsetHeight,p=o*E.scale,u=d*E.scale,v=-(c=Math.min(g/2-p/2,0)),f=-(h=Math.min(b/2-u/2,0)),(r=i*E.scale)v&&(r=v),(n=s*E.scale)f&&(n=f)):(r=0,n=0),T.$imageWrapEl.transition(300).transform("translate3d("+r+"px, "+n+"px,0)"),T.$imageEl.transition(300).transform("translate3d(0,0,0) scale("+E.scale+")"))},out:function(){var e=this,t=e.zoom,a=e.params.zoom,i=t.gesture;i.$slideEl||(e.params.virtual&&e.params.virtual.enabled&&e.virtual?i.$slideEl=e.$wrapperEl.children("."+e.params.slideActiveClass):i.$slideEl=e.slides.eq(e.activeIndex),i.$imageEl=i.$slideEl.find("img, svg, canvas, picture, .swiper-zoom-target"),i.$imageWrapEl=i.$imageEl.parent("."+a.containerClass)),i.$imageEl&&0!==i.$imageEl.length&&i.$imageWrapEl&&0!==i.$imageWrapEl.length&&(t.scale=1,t.currentScale=1,i.$imageWrapEl.transition(300).transform("translate3d(0,0,0)"),i.$imageEl.transition(300).transform("translate3d(0,0,0) scale(1)"),i.$slideEl.removeClass(""+a.zoomedSlideClass),i.$slideEl=void 0)},toggleGestures:function(e){var t=this,a=t.zoom,i=a.slideSelector,s=a.passiveListener;t.$wrapperEl[e]("gesturestart",i,a.onGestureStart,s),t.$wrapperEl[e]("gesturechange",i,a.onGestureChange,s),t.$wrapperEl[e]("gestureend",i,a.onGestureEnd,s)},enableGestures:function(){this.zoom.gesturesEnabled||(this.zoom.gesturesEnabled=!0,this.zoom.toggleGestures("on"))},disableGestures:function(){this.zoom.gesturesEnabled&&(this.zoom.gesturesEnabled=!1,this.zoom.toggleGestures("off"))},enable:function(){var e=this,t=e.support,a=e.zoom;if(!a.enabled){a.enabled=!0;var i=!("touchstart"!==e.touchEvents.start||!t.passiveListener||!e.params.passiveListeners)&&{passive:!0,capture:!1},s=!t.passiveListener||{passive:!1,capture:!0},r="."+e.params.slideClass;e.zoom.passiveListener=i,e.zoom.slideSelector=r,t.gestures?(e.$wrapperEl.on(e.touchEvents.start,e.zoom.enableGestures,i),e.$wrapperEl.on(e.touchEvents.end,e.zoom.disableGestures,i)):"touchstart"===e.touchEvents.start&&(e.$wrapperEl.on(e.touchEvents.start,r,a.onGestureStart,i),e.$wrapperEl.on(e.touchEvents.move,r,a.onGestureChange,s),e.$wrapperEl.on(e.touchEvents.end,r,a.onGestureEnd,i),e.touchEvents.cancel&&e.$wrapperEl.on(e.touchEvents.cancel,r,a.onGestureEnd,i)),e.$wrapperEl.on(e.touchEvents.move,"."+e.params.zoom.containerClass,a.onTouchMove,s)}},disable:function(){var e=this,t=e.zoom;if(t.enabled){var a=e.support;e.zoom.enabled=!1;var i=!("touchstart"!==e.touchEvents.start||!a.passiveListener||!e.params.passiveListeners)&&{passive:!0,capture:!1},s=!a.passiveListener||{passive:!1,capture:!0},r="."+e.params.slideClass;a.gestures?(e.$wrapperEl.off(e.touchEvents.start,e.zoom.enableGestures,i),e.$wrapperEl.off(e.touchEvents.end,e.zoom.disableGestures,i)):"touchstart"===e.touchEvents.start&&(e.$wrapperEl.off(e.touchEvents.start,r,t.onGestureStart,i),e.$wrapperEl.off(e.touchEvents.move,r,t.onGestureChange,s),e.$wrapperEl.off(e.touchEvents.end,r,t.onGestureEnd,i),e.touchEvents.cancel&&e.$wrapperEl.off(e.touchEvents.cancel,r,t.onGestureEnd,i)),e.$wrapperEl.off(e.touchEvents.move,"."+e.params.zoom.containerClass,t.onTouchMove,s)}}},se={loadInSlide:function(e,t){void 0===t&&(t=!0);var a=this,i=a.params.lazy;if(void 0!==e&&0!==a.slides.length){var s=a.virtual&&a.params.virtual.enabled?a.$wrapperEl.children("."+a.params.slideClass+'[data-swiper-slide-index="'+e+'"]'):a.slides.eq(e),r=s.find("."+i.elementClass+":not(."+i.loadedClass+"):not(."+i.loadingClass+")");!s.hasClass(i.elementClass)||s.hasClass(i.loadedClass)||s.hasClass(i.loadingClass)||r.push(s[0]),0!==r.length&&r.each((function(e){var r=m(e);r.addClass(i.loadingClass);var n=r.attr("data-background"),l=r.attr("data-src"),o=r.attr("data-srcset"),d=r.attr("data-sizes"),p=r.parent("picture");a.loadImage(r[0],l||n,o,d,!1,(function(){if(null!=a&&a&&(!a||a.params)&&!a.destroyed){if(n?(r.css("background-image",'url("'+n+'")'),r.removeAttr("data-background")):(o&&(r.attr("srcset",o),r.removeAttr("data-srcset")),d&&(r.attr("sizes",d),r.removeAttr("data-sizes")),p.length&&p.children("source").each((function(e){var t=m(e);t.attr("data-srcset")&&(t.attr("srcset",t.attr("data-srcset")),t.removeAttr("data-srcset"))})),l&&(r.attr("src",l),r.removeAttr("data-src"))),r.addClass(i.loadedClass).removeClass(i.loadingClass),s.find("."+i.preloaderClass).remove(),a.params.loop&&t){var e=s.attr("data-swiper-slide-index");if(s.hasClass(a.params.slideDuplicateClass)){var u=a.$wrapperEl.children('[data-swiper-slide-index="'+e+'"]:not(.'+a.params.slideDuplicateClass+")");a.lazy.loadInSlide(u.index(),!1)}else{var c=a.$wrapperEl.children("."+a.params.slideDuplicateClass+'[data-swiper-slide-index="'+e+'"]');a.lazy.loadInSlide(c.index(),!1)}}a.emit("lazyImageReady",s[0],r[0]),a.params.autoHeight&&a.updateAutoHeight()}})),a.emit("lazyImageLoad",s[0],r[0])}))}},load:function(){var e=this,t=e.$wrapperEl,a=e.params,i=e.slides,s=e.activeIndex,r=e.virtual&&a.virtual.enabled,n=a.lazy,l=a.slidesPerView;function o(e){if(r){if(t.children("."+a.slideClass+'[data-swiper-slide-index="'+e+'"]').length)return!0}else if(i[e])return!0;return!1}function d(e){return r?m(e).attr("data-swiper-slide-index"):m(e).index()}if("auto"===l&&(l=0),e.lazy.initialImageLoaded||(e.lazy.initialImageLoaded=!0),e.params.watchSlidesVisibility)t.children("."+a.slideVisibleClass).each((function(t){var a=r?m(t).attr("data-swiper-slide-index"):m(t).index();e.lazy.loadInSlide(a)}));else if(l>1)for(var p=s;p1||n.loadPrevNextAmount&&n.loadPrevNextAmount>1){for(var u=n.loadPrevNextAmount,c=l,h=Math.min(s+c+Math.max(u,c),i.length),v=Math.max(s-Math.max(c,u),0),f=s+l;f0&&e.lazy.loadInSlide(d(b));var w=t.children("."+a.slidePrevClass);w.length>0&&e.lazy.loadInSlide(d(w))}},checkInViewOnLoad:function(){var e=l(),t=this;if(t&&!t.destroyed){var a=t.params.lazy.scrollingElement?m(t.params.lazy.scrollingElement):m(e),i=a[0]===e,s=i?e.innerWidth:a[0].offsetWidth,r=i?e.innerHeight:a[0].offsetHeight,n=t.$el.offset(),o=!1;t.rtlTranslate&&(n.left-=t.$el[0].scrollLeft);for(var d=[[n.left,n.top],[n.left+t.width,n.top],[n.left,n.top+t.height],[n.left+t.width,n.top+t.height]],p=0;p=0&&u[0]<=s&&u[1]>=0&&u[1]<=r){if(0===u[0]&&0===u[1])continue;o=!0}}var c=!("touchstart"!==t.touchEvents.start||!t.support.passiveListener||!t.params.passiveListeners)&&{passive:!0,capture:!1};o?(t.lazy.load(),a.off("scroll",t.lazy.checkInViewOnLoad,c)):t.lazy.scrollHandlerAttached||(t.lazy.scrollHandlerAttached=!0,a.on("scroll",t.lazy.checkInViewOnLoad,c))}}},re={LinearSpline:function(e,t){var a,i,s,r,n,l=function(e,t){for(i=-1,a=e.length;a-i>1;)e[s=a+i>>1]<=t?i=s:a=s;return a};return this.x=e,this.y=t,this.lastIndex=e.length-1,this.interpolate=function(e){return e?(n=l(this.x,e),r=n-1,(e-this.x[r])*(this.y[n]-this.y[r])/(this.x[n]-this.x[r])+this.y[r]):0},this},getInterpolateFunction:function(e){var t=this;t.controller.spline||(t.controller.spline=t.params.loop?new re.LinearSpline(t.slidesGrid,e.slidesGrid):new re.LinearSpline(t.snapGrid,e.snapGrid))},setTranslate:function(e,t){var a,i,s=this,r=s.controller.control,n=s.constructor;function l(e){var t=s.rtlTranslate?-s.translate:s.translate;"slide"===s.params.controller.by&&(s.controller.getInterpolateFunction(e),i=-s.controller.spline.interpolate(-t)),i&&"container"!==s.params.controller.by||(a=(e.maxTranslate()-e.minTranslate())/(s.maxTranslate()-s.minTranslate()),i=(t-s.minTranslate())*a+e.minTranslate()),s.params.controller.inverse&&(i=e.maxTranslate()-i),e.updateProgress(i),e.setTranslate(i,s),e.updateActiveIndex(),e.updateSlidesClasses()}if(Array.isArray(r))for(var o=0;o0&&(e.isBeginning?(e.a11y.disableEl(i),e.a11y.makeElNotFocusable(i)):(e.a11y.enableEl(i),e.a11y.makeElFocusable(i))),a&&a.length>0&&(e.isEnd?(e.a11y.disableEl(a),e.a11y.makeElNotFocusable(a)):(e.a11y.enableEl(a),e.a11y.makeElFocusable(a)))}},updatePagination:function(){var e=this,t=e.params.a11y;e.pagination&&e.params.pagination.clickable&&e.pagination.bullets&&e.pagination.bullets.length&&e.pagination.bullets.each((function(a){var i=m(a);e.a11y.makeElFocusable(i),e.params.pagination.renderBullet||(e.a11y.addElRole(i,"button"),e.a11y.addElLabel(i,t.paginationBulletMessage.replace(/\{\{index\}\}/,i.index()+1)))}))},init:function(){var e=this,t=e.params.a11y;e.$el.append(e.a11y.liveRegion);var a=e.$el;t.containerRoleDescriptionMessage&&e.a11y.addElRoleDescription(a,t.containerRoleDescriptionMessage),t.containerMessage&&e.a11y.addElLabel(a,t.containerMessage);var i=e.$wrapperEl,s=i.attr("id")||"swiper-wrapper-"+e.a11y.getRandomNumber(16),r=e.params.autoplay&&e.params.autoplay.enabled?"off":"polite";e.a11y.addElId(i,s),e.a11y.addElLive(i,r),t.itemRoleDescriptionMessage&&e.a11y.addElRoleDescription(m(e.slides),t.itemRoleDescriptionMessage),e.a11y.addElRole(m(e.slides),t.slideRole);var n,l,o=e.params.loop?e.slides.filter((function(t){return!t.classList.contains(e.params.slideDuplicateClass)})).length:e.slides.length;e.slides.each((function(a,i){var s=m(a),r=e.params.loop?parseInt(s.attr("data-swiper-slide-index"),10):i,n=t.slideLabelMessage.replace(/\{\{index\}\}/,r+1).replace(/\{\{slidesLength\}\}/,o);e.a11y.addElLabel(s,n)})),e.navigation&&e.navigation.$nextEl&&(n=e.navigation.$nextEl),e.navigation&&e.navigation.$prevEl&&(l=e.navigation.$prevEl),n&&n.length&&(e.a11y.makeElFocusable(n),"BUTTON"!==n[0].tagName&&(e.a11y.addElRole(n,"button"),n.on("keydown",e.a11y.onEnterOrSpaceKey)),e.a11y.addElLabel(n,t.nextSlideMessage),e.a11y.addElControls(n,s)),l&&l.length&&(e.a11y.makeElFocusable(l),"BUTTON"!==l[0].tagName&&(e.a11y.addElRole(l,"button"),l.on("keydown",e.a11y.onEnterOrSpaceKey)),e.a11y.addElLabel(l,t.prevSlideMessage),e.a11y.addElControls(l,s)),e.pagination&&e.params.pagination.clickable&&e.pagination.bullets&&e.pagination.bullets.length&&e.pagination.$el.on("keydown",P(e.params.pagination.bulletClass),e.a11y.onEnterOrSpaceKey)},destroy:function(){var e,t,a=this;a.a11y.liveRegion&&a.a11y.liveRegion.length>0&&a.a11y.liveRegion.remove(),a.navigation&&a.navigation.$nextEl&&(e=a.navigation.$nextEl),a.navigation&&a.navigation.$prevEl&&(t=a.navigation.$prevEl),e&&e.off("keydown",a.a11y.onEnterOrSpaceKey),t&&t.off("keydown",a.a11y.onEnterOrSpaceKey),a.pagination&&a.params.pagination.clickable&&a.pagination.bullets&&a.pagination.bullets.length&&a.pagination.$el.off("keydown",P(a.params.pagination.bulletClass),a.a11y.onEnterOrSpaceKey)}},le={init:function(){var e=this,t=l();if(e.params.history){if(!t.history||!t.history.pushState)return e.params.history.enabled=!1,void(e.params.hashNavigation.enabled=!0);var a=e.history;a.initialized=!0,a.paths=le.getPathValues(e.params.url),(a.paths.key||a.paths.value)&&(a.scrollToSlide(0,a.paths.value,e.params.runCallbacksOnInit),e.params.history.replaceState||t.addEventListener("popstate",e.history.setHistoryPopState))}},destroy:function(){var e=l();this.params.history.replaceState||e.removeEventListener("popstate",this.history.setHistoryPopState)},setHistoryPopState:function(){var e=this;e.history.paths=le.getPathValues(e.params.url),e.history.scrollToSlide(e.params.speed,e.history.paths.value,!1)},getPathValues:function(e){var t=l(),a=(e?new URL(e):t.location).pathname.slice(1).split("/").filter((function(e){return""!==e})),i=a.length;return{key:a[i-2],value:a[i-1]}},setHistory:function(e,t){var a=this,i=l();if(a.history.initialized&&a.params.history.enabled){var s;s=a.params.url?new URL(a.params.url):i.location;var r=a.slides.eq(t),n=le.slugify(r.attr("data-history"));if(a.params.history.root.length>0){var o=a.params.history.root;"/"===o[o.length-1]&&(o=o.slice(0,o.length-1)),n=o+"/"+e+"/"+n}else s.pathname.includes(e)||(n=e+"/"+n);var d=i.history.state;d&&d.value===n||(a.params.history.replaceState?i.history.replaceState({value:n},null,n):i.history.pushState({value:n},null,n))}},slugify:function(e){return e.toString().replace(/\s+/g,"-").replace(/[^\w-]+/g,"").replace(/--+/g,"-").replace(/^-+/,"").replace(/-+$/,"")},scrollToSlide:function(e,t,a){var i=this;if(t)for(var s=0,r=i.slides.length;s
    '),i.append(e)),e.css({height:r+"px"})):0===(e=a.find(".swiper-cube-shadow")).length&&(e=m('
    '),a.append(e)));for(var v=0;v-1&&(h=90*g+90*y,l&&(h=90*-g-90*y)),f.transform(C),p.slideShadows){var S=u?f.find(".swiper-slide-shadow-left"):f.find(".swiper-slide-shadow-top"),M=u?f.find(".swiper-slide-shadow-right"):f.find(".swiper-slide-shadow-bottom");0===S.length&&(S=m('
    '),f.append(S)),0===M.length&&(M=m('
    '),f.append(M)),S.length&&(S[0].style.opacity=Math.max(-y,0)),M.length&&(M[0].style.opacity=Math.max(y,0))}}if(i.css({"-webkit-transform-origin":"50% 50% -"+o/2+"px","-moz-transform-origin":"50% 50% -"+o/2+"px","-ms-transform-origin":"50% 50% -"+o/2+"px","transform-origin":"50% 50% -"+o/2+"px"}),p.shadow)if(u)e.transform("translate3d(0px, "+(r/2+p.shadowOffset)+"px, "+-r/2+"px) rotateX(90deg) rotateZ(0deg) scale("+p.shadowScale+")");else{var z=Math.abs(h)-90*Math.floor(Math.abs(h)/90),P=1.5-(Math.sin(2*z*Math.PI/360)/2+Math.cos(2*z*Math.PI/360)/2),k=p.shadowScale,$=p.shadowScale/P,L=p.shadowOffset;e.transform("scale3d("+k+", 1, "+$+") translate3d(0px, "+(n/2+L)+"px, "+-n/2/$+"px) rotateX(-90deg)")}var I=d.isSafari||d.isWebView?-o/2:0;i.transform("translate3d(0px,0,"+I+"px) rotateX("+(t.isHorizontal()?0:h)+"deg) rotateY("+(t.isHorizontal()?-h:0)+"deg)")},setTransition:function(e){var t=this,a=t.$el;t.slides.transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e),t.params.cubeEffect.shadow&&!t.isHorizontal()&&a.find(".swiper-cube-shadow").transition(e)}},ce={setTranslate:function(){for(var e=this,t=e.slides,a=e.rtlTranslate,i=0;i
    '),s.append(p)),0===u.length&&(u=m('
    '),s.append(u)),p.length&&(p[0].style.opacity=Math.max(-r,0)),u.length&&(u[0].style.opacity=Math.max(r,0))}s.transform("translate3d("+o+"px, "+d+"px, 0px) rotateX("+l+"deg) rotateY("+n+"deg)")}},setTransition:function(e){var t=this,a=t.slides,i=t.activeIndex,s=t.$wrapperEl;if(a.transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e),t.params.virtualTranslate&&0!==e){var r=!1;a.eq(i).transitionEnd((function(){if(!r&&t&&!t.destroyed){r=!0,t.animating=!1;for(var e=["webkitTransitionEnd","transitionend"],a=0;a
    '),h.append(S)),0===M.length&&(M=m('
    '),h.append(M)),S.length&&(S[0].style.opacity=f>0?f:0),M.length&&(M[0].style.opacity=-f>0?-f:0)}}},setTransition:function(e){this.slides.transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e)}},ve={init:function(){var e=this,t=e.params.thumbs;if(e.thumbs.initialized)return!1;e.thumbs.initialized=!0;var a=e.constructor;return t.swiper instanceof a?(e.thumbs.swiper=t.swiper,M(e.thumbs.swiper.originalParams,{watchSlidesProgress:!0,slideToClickedSlide:!1}),M(e.thumbs.swiper.params,{watchSlidesProgress:!0,slideToClickedSlide:!1})):C(t.swiper)&&(e.thumbs.swiper=new a(M({},t.swiper,{watchSlidesVisibility:!0,watchSlidesProgress:!0,slideToClickedSlide:!1})),e.thumbs.swiperCreated=!0),e.thumbs.swiper.$el.addClass(e.params.thumbs.thumbsContainerClass),e.thumbs.swiper.on("tap",e.thumbs.onThumbClick),!0},onThumbClick:function(){var e=this,t=e.thumbs.swiper;if(t){var a=t.clickedIndex,i=t.clickedSlide;if(!(i&&m(i).hasClass(e.params.thumbs.slideThumbActiveClass)||null==a)){var s;if(s=t.params.loop?parseInt(m(t.clickedSlide).attr("data-swiper-slide-index"),10):a,e.params.loop){var r=e.activeIndex;e.slides.eq(r).hasClass(e.params.slideDuplicateClass)&&(e.loopFix(),e._clientLeft=e.$wrapperEl[0].clientLeft,r=e.activeIndex);var n=e.slides.eq(r).prevAll('[data-swiper-slide-index="'+s+'"]').eq(0).index(),l=e.slides.eq(r).nextAll('[data-swiper-slide-index="'+s+'"]').eq(0).index();s=void 0===n?l:void 0===l?n:l-r1?p:o:p-ot.previousIndex?"next":"prev"}else l=(n=t.realIndex)>t.previousIndex?"next":"prev";r&&(n+="next"===l?s:-1*s),a.visibleSlidesIndexes&&a.visibleSlidesIndexes.indexOf(n)<0&&(a.params.centeredSlides?n=n>o?n-Math.floor(i/2)+1:n+Math.floor(i/2)-1:n>o&&a.params.slidesPerGroup,a.slideTo(n,e?0:void 0))}var u=1,c=t.params.thumbs.slideThumbActiveClass;if(t.params.slidesPerView>1&&!t.params.centeredSlides&&(u=t.params.slidesPerView),t.params.thumbs.multipleActiveThumbs||(u=1),u=Math.floor(u),a.slides.removeClass(c),a.params.loop||a.params.virtual&&a.params.virtual.enabled)for(var h=0;h0&&!m(a).hasClass(e.params.pagination.bulletClass)){if(e.navigation&&(e.navigation.nextEl&&a===e.navigation.nextEl||e.navigation.prevEl&&a===e.navigation.prevEl))return;!0===e.pagination.$el.hasClass(e.params.pagination.hiddenClass)?e.emit("paginationShow"):e.emit("paginationHide"),e.pagination.$el.toggleClass(e.params.pagination.hiddenClass)}}}},{name:"scrollbar",params:{scrollbar:{el:null,dragSize:"auto",hide:!1,draggable:!1,snapOnRelease:!0,lockClass:"swiper-scrollbar-lock",dragClass:"swiper-scrollbar-drag"}},create:function(){z(this,{scrollbar:t({isTouched:!1,timeout:null,dragTimeout:null},te)})},on:{init:function(e){e.scrollbar.init(),e.scrollbar.updateSize(),e.scrollbar.setTranslate()},update:function(e){e.scrollbar.updateSize()},resize:function(e){e.scrollbar.updateSize()},observerUpdate:function(e){e.scrollbar.updateSize()},setTranslate:function(e){e.scrollbar.setTranslate()},setTransition:function(e,t){e.scrollbar.setTransition(t)},"enable disable":function(e){var t=e.scrollbar.$el;t&&t[e.enabled?"removeClass":"addClass"](e.params.scrollbar.lockClass)},destroy:function(e){e.scrollbar.destroy()}}},{name:"parallax",params:{parallax:{enabled:!1}},create:function(){z(this,{parallax:t({},ae)})},on:{beforeInit:function(e){e.params.parallax.enabled&&(e.params.watchSlidesProgress=!0,e.originalParams.watchSlidesProgress=!0)},init:function(e){e.params.parallax.enabled&&e.parallax.setTranslate()},setTranslate:function(e){e.params.parallax.enabled&&e.parallax.setTranslate()},setTransition:function(e,t){e.params.parallax.enabled&&e.parallax.setTransition(t)}}},{name:"zoom",params:{zoom:{enabled:!1,maxRatio:3,minRatio:1,toggle:!0,containerClass:"swiper-zoom-container",zoomedSlideClass:"swiper-slide-zoomed"}},create:function(){var e=this;z(e,{zoom:t({enabled:!1,scale:1,currentScale:1,isScaling:!1,gesture:{$slideEl:void 0,slideWidth:void 0,slideHeight:void 0,$imageEl:void 0,$imageWrapEl:void 0,maxRatio:3},image:{isTouched:void 0,isMoved:void 0,currentX:void 0,currentY:void 0,minX:void 0,minY:void 0,maxX:void 0,maxY:void 0,width:void 0,height:void 0,startX:void 0,startY:void 0,touchesStart:{},touchesCurrent:{}},velocity:{x:void 0,y:void 0,prevPositionX:void 0,prevPositionY:void 0,prevTime:void 0}},ie)});var a=1;Object.defineProperty(e.zoom,"scale",{get:function(){return a},set:function(t){if(a!==t){var i=e.zoom.gesture.$imageEl?e.zoom.gesture.$imageEl[0]:void 0,s=e.zoom.gesture.$slideEl?e.zoom.gesture.$slideEl[0]:void 0;e.emit("zoomChange",t,i,s)}a=t}})},on:{init:function(e){e.params.zoom.enabled&&e.zoom.enable()},destroy:function(e){e.zoom.disable()},touchStart:function(e,t){e.zoom.enabled&&e.zoom.onTouchStart(t)},touchEnd:function(e,t){e.zoom.enabled&&e.zoom.onTouchEnd(t)},doubleTap:function(e,t){!e.animating&&e.params.zoom.enabled&&e.zoom.enabled&&e.params.zoom.toggle&&e.zoom.toggle(t)},transitionEnd:function(e){e.zoom.enabled&&e.params.zoom.enabled&&e.zoom.onTransitionEnd()},slideChange:function(e){e.zoom.enabled&&e.params.zoom.enabled&&e.params.cssMode&&e.zoom.onTransitionEnd()}}},{name:"lazy",params:{lazy:{checkInView:!1,enabled:!1,loadPrevNext:!1,loadPrevNextAmount:1,loadOnTransitionStart:!1,scrollingElement:"",elementClass:"swiper-lazy",loadingClass:"swiper-lazy-loading",loadedClass:"swiper-lazy-loaded",preloaderClass:"swiper-lazy-preloader"}},create:function(){z(this,{lazy:t({initialImageLoaded:!1},se)})},on:{beforeInit:function(e){e.params.lazy.enabled&&e.params.preloadImages&&(e.params.preloadImages=!1)},init:function(e){e.params.lazy.enabled&&!e.params.loop&&0===e.params.initialSlide&&(e.params.lazy.checkInView?e.lazy.checkInViewOnLoad():e.lazy.load())},scroll:function(e){e.params.freeMode&&!e.params.freeModeSticky&&e.lazy.load()},"scrollbarDragMove resize _freeModeNoMomentumRelease":function(e){e.params.lazy.enabled&&e.lazy.load()},transitionStart:function(e){e.params.lazy.enabled&&(e.params.lazy.loadOnTransitionStart||!e.params.lazy.loadOnTransitionStart&&!e.lazy.initialImageLoaded)&&e.lazy.load()},transitionEnd:function(e){e.params.lazy.enabled&&!e.params.lazy.loadOnTransitionStart&&e.lazy.load()},slideChange:function(e){var t=e.params,a=t.lazy,i=t.cssMode,s=t.watchSlidesVisibility,r=t.watchSlidesProgress,n=t.touchReleaseOnEdges,l=t.resistanceRatio;a.enabled&&(i||(s||r)&&(n||0===l))&&e.lazy.load()}}},{name:"controller",params:{controller:{control:void 0,inverse:!1,by:"slide"}},create:function(){z(this,{controller:t({control:this.params.controller.control},re)})},on:{update:function(e){e.controller.control&&e.controller.spline&&(e.controller.spline=void 0,delete e.controller.spline)},resize:function(e){e.controller.control&&e.controller.spline&&(e.controller.spline=void 0,delete e.controller.spline)},observerUpdate:function(e){e.controller.control&&e.controller.spline&&(e.controller.spline=void 0,delete e.controller.spline)},setTranslate:function(e,t,a){e.controller.control&&e.controller.setTranslate(t,a)},setTransition:function(e,t,a){e.controller.control&&e.controller.setTransition(t,a)}}},{name:"a11y",params:{a11y:{enabled:!0,notificationClass:"swiper-notification",prevSlideMessage:"Previous slide",nextSlideMessage:"Next slide",firstSlideMessage:"This is the first slide",lastSlideMessage:"This is the last slide",paginationBulletMessage:"Go to slide {{index}}",slideLabelMessage:"{{index}} / {{slidesLength}}",containerMessage:null,containerRoleDescriptionMessage:null,itemRoleDescriptionMessage:null,slideRole:"group"}},create:function(){z(this,{a11y:t({},ne,{liveRegion:m('')})})},on:{afterInit:function(e){e.params.a11y.enabled&&(e.a11y.init(),e.a11y.updateNavigation())},toEdge:function(e){e.params.a11y.enabled&&e.a11y.updateNavigation()},fromEdge:function(e){e.params.a11y.enabled&&e.a11y.updateNavigation()},paginationUpdate:function(e){e.params.a11y.enabled&&e.a11y.updatePagination()},destroy:function(e){e.params.a11y.enabled&&e.a11y.destroy()}}},{name:"history",params:{history:{enabled:!1,root:"",replaceState:!1,key:"slides"}},create:function(){z(this,{history:t({},le)})},on:{init:function(e){e.params.history.enabled&&e.history.init()},destroy:function(e){e.params.history.enabled&&e.history.destroy()},"transitionEnd _freeModeNoMomentumRelease":function(e){e.history.initialized&&e.history.setHistory(e.params.history.key,e.activeIndex)},slideChange:function(e){e.history.initialized&&e.params.cssMode&&e.history.setHistory(e.params.history.key,e.activeIndex)}}},{name:"hash-navigation",params:{hashNavigation:{enabled:!1,replaceState:!1,watchState:!1}},create:function(){z(this,{hashNavigation:t({initialized:!1},oe)})},on:{init:function(e){e.params.hashNavigation.enabled&&e.hashNavigation.init()},destroy:function(e){e.params.hashNavigation.enabled&&e.hashNavigation.destroy()},"transitionEnd _freeModeNoMomentumRelease":function(e){e.hashNavigation.initialized&&e.hashNavigation.setHash()},slideChange:function(e){e.hashNavigation.initialized&&e.params.cssMode&&e.hashNavigation.setHash()}}},{name:"autoplay",params:{autoplay:{enabled:!1,delay:3e3,waitForTransition:!0,disableOnInteraction:!0,stopOnLastSlide:!1,reverseDirection:!1,pauseOnMouseEnter:!1}},create:function(){z(this,{autoplay:t({},de,{running:!1,paused:!1})})},on:{init:function(e){e.params.autoplay.enabled&&(e.autoplay.start(),r().addEventListener("visibilitychange",e.autoplay.onVisibilityChange),e.autoplay.attachMouseEvents())},beforeTransitionStart:function(e,t,a){e.autoplay.running&&(a||!e.params.autoplay.disableOnInteraction?e.autoplay.pause(t):e.autoplay.stop())},sliderFirstMove:function(e){e.autoplay.running&&(e.params.autoplay.disableOnInteraction?e.autoplay.stop():e.autoplay.pause())},touchEnd:function(e){e.params.cssMode&&e.autoplay.paused&&!e.params.autoplay.disableOnInteraction&&e.autoplay.run()},destroy:function(e){e.autoplay.detachMouseEvents(),e.autoplay.running&&e.autoplay.stop(),r().removeEventListener("visibilitychange",e.autoplay.onVisibilityChange)}}},{name:"effect-fade",params:{fadeEffect:{crossFade:!1}},create:function(){z(this,{fadeEffect:t({},pe)})},on:{beforeInit:function(e){if("fade"===e.params.effect){e.classNames.push(e.params.containerModifierClass+"fade");var t={slidesPerView:1,slidesPerColumn:1,slidesPerGroup:1,watchSlidesProgress:!0,spaceBetween:0,virtualTranslate:!0};M(e.params,t),M(e.originalParams,t)}},setTranslate:function(e){"fade"===e.params.effect&&e.fadeEffect.setTranslate()},setTransition:function(e,t){"fade"===e.params.effect&&e.fadeEffect.setTransition(t)}}},{name:"effect-cube",params:{cubeEffect:{slideShadows:!0,shadow:!0,shadowOffset:20,shadowScale:.94}},create:function(){z(this,{cubeEffect:t({},ue)})},on:{beforeInit:function(e){if("cube"===e.params.effect){e.classNames.push(e.params.containerModifierClass+"cube"),e.classNames.push(e.params.containerModifierClass+"3d");var t={slidesPerView:1,slidesPerColumn:1,slidesPerGroup:1,watchSlidesProgress:!0,resistanceRatio:0,spaceBetween:0,centeredSlides:!1,virtualTranslate:!0};M(e.params,t),M(e.originalParams,t)}},setTranslate:function(e){"cube"===e.params.effect&&e.cubeEffect.setTranslate()},setTransition:function(e,t){"cube"===e.params.effect&&e.cubeEffect.setTransition(t)}}},{name:"effect-flip",params:{flipEffect:{slideShadows:!0,limitRotation:!0}},create:function(){z(this,{flipEffect:t({},ce)})},on:{beforeInit:function(e){if("flip"===e.params.effect){e.classNames.push(e.params.containerModifierClass+"flip"),e.classNames.push(e.params.containerModifierClass+"3d");var t={slidesPerView:1,slidesPerColumn:1,slidesPerGroup:1,watchSlidesProgress:!0,spaceBetween:0,virtualTranslate:!0};M(e.params,t),M(e.originalParams,t)}},setTranslate:function(e){"flip"===e.params.effect&&e.flipEffect.setTranslate()},setTransition:function(e,t){"flip"===e.params.effect&&e.flipEffect.setTransition(t)}}},{name:"effect-coverflow",params:{coverflowEffect:{rotate:50,stretch:0,depth:100,scale:1,modifier:1,slideShadows:!0}},create:function(){z(this,{coverflowEffect:t({},he)})},on:{beforeInit:function(e){"coverflow"===e.params.effect&&(e.classNames.push(e.params.containerModifierClass+"coverflow"),e.classNames.push(e.params.containerModifierClass+"3d"),e.params.watchSlidesProgress=!0,e.originalParams.watchSlidesProgress=!0)},setTranslate:function(e){"coverflow"===e.params.effect&&e.coverflowEffect.setTranslate()},setTransition:function(e,t){"coverflow"===e.params.effect&&e.coverflowEffect.setTransition(t)}}},{name:"thumbs",params:{thumbs:{swiper:null,multipleActiveThumbs:!0,autoScrollOffset:0,slideThumbActiveClass:"swiper-slide-thumb-active",thumbsContainerClass:"swiper-container-thumbs"}},create:function(){z(this,{thumbs:t({swiper:null,initialized:!1},ve)})},on:{beforeInit:function(e){var t=e.params.thumbs;t&&t.swiper&&(e.thumbs.init(),e.thumbs.update(!0))},slideChange:function(e){e.thumbs.swiper&&e.thumbs.update()},update:function(e){e.thumbs.swiper&&e.thumbs.update()},resize:function(e){e.thumbs.swiper&&e.thumbs.update()},observerUpdate:function(e){e.thumbs.swiper&&e.thumbs.update()},setTransition:function(e,t){var a=e.thumbs.swiper;a&&a.setTransition(t)},beforeDestroy:function(e){var t=e.thumbs.swiper;t&&e.thumbs.swiperCreated&&t&&t.destroy()}}}];return q.use(fe),q})); +//# sourceMappingURL=swiper-bundle.min.js.map \ No newline at end of file diff --git a/sa-token-doc/static/vue.css b/sa-token-doc/static/vue.css new file mode 100644 index 000000000..86258a8b5 --- /dev/null +++ b/sa-token-doc/static/vue.css @@ -0,0 +1 @@ +@import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative;cursor:pointer}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30;cursor:pointer}.sidebar-toggle:hover .sidebar-toggle-button{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:80%;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{-webkit-animation:none;animation:none}.github-corner .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}}@-webkit-keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{flex:1;margin:-20px 16px 0;text-align:center;z-index:1}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid var(--theme-color,#42b983);box-sizing:border-box;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code{border-radius:2px;color:#e96900;font-size:.8rem;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section code,.markdown-section pre{background-color:#f8f8f8;font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section pre{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;line-height:1.5rem;margin:1.2em 0;overflow:auto;padding:0 1.4rem;position:relative;word-wrap:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:var(--theme-color,#42b983)}.token.function,.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;background-color:#f8f8f8;border-radius:2px;color:#525252;display:block;font-family:Roboto Mono,Monaco,courier,monospace;font-size:.8rem;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;padding:2.2em 5px;white-space:inherit}.markdown-section code:after,.markdown-section code:before{letter-spacing:.05rem}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto}pre:after{color:#ccc;content:attr(data-lang);font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0} \ No newline at end of file diff --git a/sa-token-doc/static/water-change-theme/gsap-3.12.2.min.js b/sa-token-doc/static/water-change-theme/gsap-3.12.2.min.js new file mode 100644 index 000000000..63cb55cb1 --- /dev/null +++ b/sa-token-doc/static/water-change-theme/gsap-3.12.2.min.js @@ -0,0 +1,10 @@ +/*! + * GSAP 3.12.2 + * https://greensock.com + * + * @license Copyright 2023, GreenSock. All rights reserved. + * Subject to the terms at https://greensock.com/standard-license or for Club GreenSock members, the agreement issued with that membership. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function P(t){return(i=yt(t,ot))&&Ee}function Q(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function R(t,e){return!e&&console.warn(t)}function S(t,e){return t&&(ot[t]=e)&&i&&(i[t]=e)||ot}function T(){return 0}function ea(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=gt.length;r--&&!gt[r].targetTest(i););e=gt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Vt(t[r],e)))||t.splice(r,1);return t}function fa(t){return t._gsap||ea(Ot(t))[0]._gsap}function ga(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ha(t,e){return(t=t.split(",")).forEach(e)||t}function ia(t){return Math.round(1e5*t)/1e5||0}function ja(t){return Math.round(1e7*t)/1e7||0}function ka(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function la(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++ia;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function ya(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function za(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Aa(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Ca(t,e,r,i){return t._startAt&&(L?t._startAt.revert(ht):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ea(t){return t._repeat?Tt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ga(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ha(t){return t._end=ja(t._start+(t._tDur/Math.abs(t._ts||t._rts||X)||0))}function Ia(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ja(r._time-(0X)&&e.render(r,!0)),Aa(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur(n=Math.abs(n))&&(a=i,o=n);return a}function tb(t){return za(t),t.scrollTrigger&&t.scrollTrigger.kill(!!L),t.progress()<1&&At(t,"onInterrupt"),t}function wb(t){if(x()&&t){var e=(t=!t.name&&t.default||t).name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:T,render:he,add:Qt,kill:ce,modifier:fe,rawVars:0},a={targetTest:0,get:0,getSetter:ne,aliases:{},register:0};if(Ft(),t!==i){if(pt[e])return;qa(i,qa(ua(t,n),a)),yt(i.prototype,yt(n,ua(t,a))),pt[i.prop=e]=i,t.targetTest&&(gt.push(i),ft[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}S(e,i),t.register&&t.register(Ee,i,_e)}else t&&Ct.push(t)}function zb(t,e,r){return(6*(t+=t<0?1:1>16,e>>8&St,e&St]:0:Et.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),Et[e])p=Et[e];else if("#"===e.charAt(0)){if(e.length<6&&(e="#"+(n=e.charAt(1))+n+(a=e.charAt(2))+a+(s=e.charAt(3))+s+(5===e.length?e.charAt(4)+e.charAt(4):"")),9===e.length)return[(p=parseInt(e.substr(1,6),16))>>16,p>>8&St,p&St,parseInt(e.substr(7),16)/255];p=[(e=parseInt(e.substr(1),16))>>16,e>>8&St,e&St]}else if("hsl"===e.substr(0,3))if(p=d=e.match(tt),r){if(~e.indexOf("="))return p=e.match(et),i&&p.length<4&&(p[3]=1),p}else o=+p[0]%360/360,u=p[1]/100,n=2*(h=p[2]/100)-(a=h<=.5?h*(u+1):h+u-h*u),3=U?u.endTime(!1):t._dur;return r(e)&&(isNaN(e)||e in o)?(a=e.charAt(0),s="%"===e.substr(-1),n=e.indexOf("="),"<"===a||">"===a?(0<=n&&(e=e.replace(/=/,"")),("<"===a?u._start:u.endTime(0<=u._repeat))+(parseFloat(e.substr(1))||0)*(s?(n<0?u:i).totalDuration()/100:1)):n<0?(e in o||(o[e]=h),o[e]):(a=parseFloat(e.charAt(n-1)+e.substr(n+1)),s&&i&&(a=a/100*($(i)?i[0]:i).totalDuration()),1=r&&te)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if("isPause"===i.data&&i._start=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0=i&&(a instanceof Zt?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return r(t)?this.removeLabel(t):s(t)?this.killTweensOf(t):(ya(this,t),t===this._recent&&(this._recent=this._last),Aa(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=ja(Rt.time-(0r:!r||s.isActive())&&n.push(s):(i=s.getTweensOf(a,r)).length&&n.push.apply(n,i),s=s._next;return n},e.tweenTo=function tweenTo(t,e){e=e||{};var r,i=this,n=xt(i,t),a=e.startAt,s=e.onStart,o=e.onStartParams,u=e.immediateRender,h=Zt.to(i,qa({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:n,overwrite:"auto",duration:e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale())||X,onStart:function onStart(){if(i.pause(),!r){var t=e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale());h._dur!==t&&Ra(h,t,0,1).render(h._time,!0,!0),r=1}s&&s.apply(h,o||[])}},e));return u?h.render(0):h},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,qa({startAt:{time:xt(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),rb(this,xt(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),rb(this,xt(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+X)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);for(var i,n=this._first,a=this.labels;n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return Aa(this)},e.invalidate=function invalidate(t){var e=this._first;for(this._lock=0;e;)e.invalidate(t),e=e._next;return i.prototype.invalidate.call(this,t)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Aa(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=U;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,Ka(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=r/a._ts,a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Ra(a,a===I&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(I._ts&&(na(I,Ga(t,I)),f=Rt.frame),Rt.frame>=mt){mt+=q.autoSleep||120;var e=I._first;if((!e||!e._ts)&&q.autoSleep&&Rt._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Rt.sleep()}}},Timeline}(Ut);qa(Xt.prototype,{_lock:0,_hasPause:0,_forcing:0});function ac(t,e,i,n,a,o){var u,h,l,f;if(pt[t]&&!1!==(u=new pt[t]).init(a,u.rawVars?e[t]:function _processVars(t,e,i,n,a){if(s(t)&&(t=Kt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||$(t)||Z(t))return r(t)?Kt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Kt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new _e(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==c))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function gc(t,r,e,i){var n,a,s=r.ease||i||"power1.inOut";if($(r))a=e[t]||(e[t]=[]),r.forEach(function(t,e){return a.push({t:e/(r.length-1)*100,v:t,e:s})});else for(n in r)a=e[n]||(e[n]=[]),"ease"===n||a.push({t:parseFloat(t),v:r[n],e:s})}var Nt,Wt,Qt=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var c,d=t[e],p="get"!==i?i:s(d)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():d,_=s(d)?l?re:te:$t;if(r(n)&&(~n.indexOf("random(")&&(n=ob(n)),"="===n.charAt(1)&&(!(c=ka(p,n)+(Ya(p)||0))&&0!==c||(n=c))),!f||p!==n||Wt)return isNaN(p*n)||""===n?(d||e in t||Q(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,c,d,p,_=new _e(this._pt,t,e,0,1,ue,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(d=~(i+="").indexOf("random("))&&(i=ob(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(it)||[];o=it.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(c=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:c,c:"="===l.charAt(1)?ka(c,l)-c:parseFloat(l)-c,m:h&&h<4?Math.round:0},m=it.lastIndex);return _.c=m")}),s.duration();else{for(l in u={},x)"ease"===l||"easeEach"===l||gc(l,x[l],u,x.easeEach);for(l in u)for(C=u[l].sort(function(t,e){return t.t-e.t}),o=D=0;o=t._tDur||e<0)&&t.ratio===u&&(u&&za(t,1),r||L||(At(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(t){return t&&this.vars.runBackwards||(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(t),z.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i){d||Rt.wake(),this._ts||this.play();var n,a=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||Gt(this,a),n=this._ease(a/this._dur),function _updatePropTweens(t,e,r,i,n,a,s){var o,u,h,l,f=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!f)for(f=t._ptCache[e]=[],h=t._ptLookup,l=t._targets.length;l--;){if((o=h[l][e])&&o.d&&o.d._pt)for(o=o.d._pt;o&&o.p!==e&&o.fp!==e;)o=o._next;if(!o)return Wt=1,t.vars[e]="+=0",Gt(t,s),Wt=0,1;f.push(o)}for(l=f.length;l--;)(o=(u=f[l])._pt||u).s=!i&&0!==i||n?o.s+(i||0)+a*o.c:i,o.c=r-o.s,u.e&&(u.e=ia(r)+Ya(u.e)),u.b&&(u.b=o.s+Ya(u.b))}(this,t,e,r,i,n,a)?this.resetTo(t,e,r,i):(Ia(this,0),this.parent||xa(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?tb(this):this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Nt&&!0!==Nt.vars.overwrite)._first||tb(this),this.parent&&i!==this.timeline.totalDuration()&&Ra(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,c=t?Ot(t):f,d=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,c))return"all"===e&&(this._pt=0),tb(this);for(n=this._op=this._op||[],"all"!==e&&(r(e)&&(u={},ha(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?fa(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=yt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~c.indexOf(f[l]))for(u in a=d[l],"all"===e?(n[l]=e,o=a,s={}):(s=n[l]=n[l]||{},o=e),o)(h=a&&a[u])&&("kill"in h.d&&!0!==h.d.kill(u)||ya(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&tb(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Va(1,arguments)},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return Va(2,arguments)},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return I.killTweensOf(t,e,r)},Tween}(Ut);qa(Zt.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ha("staggerTo,staggerFrom,staggerFromTo",function(r){Zt[r]=function(){var t=new Xt,e=Mt.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function oc(t,e,r){return t.setAttribute(e,r)}function wc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var $t=function _setterPlain(t,e,r){return t[e]=r},te=function _setterFunc(t,e,r){return t[e](r)},re=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},ne=function _getSetter(t,e){return s(t[e])?te:u(t[e])&&t.setAttribute?oc:$t},ae=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},se=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},ue=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},he=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},fe=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},ce=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?ya(this,i,"_pt"):i.dep||(e=1),i=r;return!e},pe=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},_e=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=wc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||ae,this.d=s||this,this.set=o||$t,this.pr=u||0,(this._next=t)&&(t._prev=this)}ha(vt+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",function(t){return ft[t]=1}),ot.TweenMax=ot.TweenLite=Zt,ot.TimelineLite=ot.TimelineMax=Xt,I=new Xt({sortChildren:!1,defaults:V,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),q.stringFilter=Fb;function Ec(t){return(ye[t]||Te).map(function(t){return t()})}function Fc(){var t=Date.now(),o=[];2 div { + background-color: var(--st-bg-elevated); + border-color: var(--st-border); + color: var(--st-fg); +} + +body[data-st-theme] .zk-box .zk-context a { + color: var(--st-fg) !important; +} + +body[data-st-theme] .zk-box .zk-context .zk-fengexian { + border-bottom-color: var(--st-border); +} + +body[data-st-theme] .theme-box span { + border-color: var(--st-border); +} + +body[data-st-theme] .nav-left .select-version, +body[data-st-theme] #translateSelectLanguage { + border-color: var(--st-border) !important; + color: var(--st-fg) !important; + background-color: var(--st-bg-elevated) !important; +} + +/* —— 搜索 —— */ +body[data-st-theme] .sear-box .search input { + border-color: var(--st-border); + color: var(--st-fg) !important; + background-color: var(--st-bg-elevated); + background-image: url(../search-icon.svg); + background-repeat: no-repeat; + background-position: 10px 8px; + background-size: 14px; +} + +body[data-st-theme] .results-panel { + background-color: var(--st-bg-elevated); + border-color: var(--st-border); + color: var(--st-fg) !important; +} + +body[data-st-theme] .search a, +body[data-st-theme] .results-panel .matching-post { + color: var(--st-fg) !important; +} + +body[data-st-theme] .search .search-keyword { + color: var(--st-accent) !important; +} + +/* —— 左侧目录 —— */ +body[data-st-theme] .sidebar .sidebar-nav > ul > li > strong, +body[data-st-theme] .sidebar .sidebar-nav > ul > li > p, +body[data-st-theme] .sidebar .sidebar-nav > ul > li > p strong, +body[data-st-theme] .sidebar .sidebar-nav strong { + color: var(--st-heading) !important; +} + +body[data-st-theme] .sidebar .sidebar-nav, +body[data-st-theme] .sidebar .sidebar-nav p { + color: var(--st-sidebar) !important; +} + +body[data-st-theme] .main-box .sidebar ul li a { + color: var(--st-sidebar) !important; +} + +body[data-st-theme] .main-box .sidebar li a:hover, +body[data-st-theme] .sidebar .sidebar-nav > ul > li > ul > li.active-rep > a { + color: var(--st-accent) !important; +} + +body[data-st-theme] .main-box .sidebar-nav > ul > li > ul > li > .app-sub-sidebar li a { + color: var(--st-fg-muted) !important; +} + +body[data-st-theme] .main-box .sidebar-nav > ul > li > ul > li > .app-sub-sidebar li.active a { + color: var(--st-accent) !important; +} + +body[data-st-theme] .sidebar-toggle span { + background-color: var(--st-fg-muted); +} + +/* —— 正文区 —— */ +body[data-st-theme] #main, +body[data-st-theme] #main .markdown-section { + color: var(--st-fg) !important; +} + +/* 覆盖 vue.css:.markdown-section h1~h4, strong { color: #2c3e50 } */ +body[data-st-theme] .main-box .markdown-section h1, +body[data-st-theme] .main-box .markdown-section h2, +body[data-st-theme] .main-box .markdown-section h3, +body[data-st-theme] .main-box .markdown-section h4, +body[data-st-theme] .main-box .markdown-section h5, +body[data-st-theme] .main-box .markdown-section h6, +body[data-st-theme] .main-box .markdown-section strong, +body[data-st-theme] #main h1, +body[data-st-theme] #main h2, +body[data-st-theme] #main h3, +body[data-st-theme] #main h4, +body[data-st-theme] #main h5, +body[data-st-theme] #main h6, +body[data-st-theme] #main strong { + color: var(--st-heading) !important; +} + +/* 覆盖 vue.css:.anchor span { color: #34495e } */ +body[data-st-theme] .main-box .markdown-section .anchor, +body[data-st-theme] .main-box .markdown-section .anchor span, +body[data-st-theme] .main-box .markdown-section h1 a, +body[data-st-theme] .main-box .markdown-section h2 a, +body[data-st-theme] .main-box .markdown-section h3 a, +body[data-st-theme] .main-box .markdown-section h4 a, +body[data-st-theme] .main-box .markdown-section h5 a, +body[data-st-theme] .main-box .markdown-section h6 a { + color: var(--st-heading) !important; + border-bottom-color: transparent; +} + +body[data-st-theme] #main p, +body[data-st-theme] #main li, +body[data-st-theme] #main td, +body[data-st-theme] #main th, +body[data-st-theme] #main em, +body[data-st-theme] #main label, +body[data-st-theme] #main .markdown-section blockquote, +body[data-st-theme] #main .markdown-section blockquote p { + color: var(--st-fg) !important; +} + +body[data-st-theme] .main-box .markdown-section :not(pre) > code { + color: var(--st-accent) !important; + background-color: var(--st-code-inline-bg) !important; +} + +body[data-st-theme] .main-box .markdown-section pre { + background-color: var(--st-code-inline-bg); +} + +body[data-st-theme] .main-box .markdown-section blockquote { + background-color: var(--st-blockquote-bg); + color: var(--st-fg-muted) !important; +} + +body[data-st-theme] .main-box .markdown-section hr { + border-color: var(--st-border); +} + +/* 正文图片:透明底 PNG 在暗色背景下加白底(徽章区除外) */ +body[data-st-theme] .main-box .markdown-section img { + background-color: #fff; +} + +body[data-st-theme] .main-box .markdown-section .badge-box img { + background-color: transparent !important; +} + +/* —— 右侧栏 / TOC —— */ +body[data-st-theme] .doc-right-bj-box-title { + color: var(--st-fg-muted) !important; + border-bottom-color: var(--st-border); +} + +body[data-st-theme] .doc-right-more-item { + color: var(--st-fg) !important; +} + +body[data-st-theme] #main .toc-box .toc-h2 a span, +body[data-st-theme] #main .toc-box a span { + color: var(--st-fg) !important; +} + +body[data-st-theme] .ad-title { + color: var(--st-fg-muted) !important; + border-bottom-color: var(--st-border); +} + +body[data-st-theme] .ew-wa { + color: var(--st-fg-muted) !important; +} + +body[data-st-theme] .ew-wa a { + color: var(--st-link) !important; +} + +body[data-st-theme] .help-btn { + background-color: var(--st-bg-soft); + color: var(--st-accent) !important; + border-color: var(--st-accent); +} + +body[data-st-theme] .help-btn:hover { + box-shadow: 0 0 20px rgba(0, 0, 0, 0.4) !important; +} + +body[data-st-theme] .mad-bg-box { + background-color: var(--st-ad-bg); +} + +body[data-st-theme] .main-box .top-ad-box .mad-text { + color: var(--st-fg) !important; +} + +/* —— 表格 / 广告 / 按钮 —— */ +body[data-st-theme] #main .gzh-table tr { + background-color: var(--st-table-bg); +} + +body[data-st-theme] #main table tr { + background-color: transparent; +} + +body[data-st-theme] #main table th, +body[data-st-theme] #main table td { + border-color: var(--st-border); + color: var(--st-fg) !important; +} + +body[data-st-theme] .main-box { + --docsifytabs-border-color: var(--st-border); + --docsifytabs-tab-color: var(--st-fg-muted); +} + +body[data-st-theme] .docsify-tabs__tab { + color: var(--st-fg-muted) !important; +} + +body[data-st-theme] .docsify-tabs__tab--active { + color: var(--st-fg) !important; +} + +body[data-st-theme] #main .doc-inline-ad__card { + background-color: var(--st-ad-bg); +} + +body[data-st-theme] #main .doc-inline-ad__text p, +body[data-st-theme] #main .doc-inline-ad__close { + color: var(--st-fg-muted) !important; +} + +body[data-st-theme] #main .doc-inline-ad__close:hover { + color: var(--st-fg) !important; +} + +body[data-st-theme] .show-img { + background-color: var(--st-bg-elevated); + border-color: var(--st-accent); + color: var(--st-accent) !important; +} + +body[data-st-theme] .show-img:hover { + background-color: var(--st-bg-soft); +} + +body[data-st-theme] #main .dt-btn, +body[data-st-theme] #main .case-btn { + background-color: var(--st-bg-soft); + color: var(--st-fg) !important; + border-color: var(--st-border) !important; +} + +body[data-st-theme] #main .case-btn.case-btn-video { + background-color: var(--st-bg-soft); + color: var(--st-link) !important; + border-color: var(--st-border) !important; +} + +/* —— details / alert —— */ +body[data-st-theme] .main-box details { + background-color: var(--st-bg-soft); + border-color: var(--st-accent); + color: var(--st-fg); +} + +body[data-st-theme] .main-box details summary { + background-color: var(--st-bg-soft); + color: var(--st-accent) !important; +} + +/* —— flexible-alerts 提示框(flat / callout) —— */ +body[data-st-theme] .main-box .alert.flat, +body[data-st-theme] .main-box .alert.callout { + color: var(--st-fg) !important; +} + +body[data-st-theme] .main-box .alert.flat p, +body[data-st-theme] .main-box .alert.callout p { + color: var(--st-fg) !important; +} + +body[data-st-theme] .main-box .alert.flat .icon, +body[data-st-theme] .main-box .alert.callout .icon { + filter: brightness(0) invert(1); + opacity: 0.9; +} + +body[data-st-theme] .main-box .alert.flat.note { + background-color: var(--st-alert-note-bg) !important; + border-color: var(--st-alert-note-border) !important; +} + +body[data-st-theme] .main-box .alert.flat.note .title { + color: var(--st-alert-note-title) !important; +} + +body[data-st-theme] .main-box .alert.flat.tip { + background-color: var(--st-alert-tip-bg) !important; + border-color: var(--st-alert-tip-border) !important; +} + +body[data-st-theme] .main-box .alert.flat.tip .title { + color: var(--st-alert-tip-title) !important; +} + +body[data-st-theme] .main-box .alert.flat.warning { + background-color: var(--st-alert-warn-bg) !important; + border-color: var(--st-alert-warn-border) !important; +} + +body[data-st-theme] .main-box .alert.flat.warning .title { + color: var(--st-alert-warn-title) !important; +} + +body[data-st-theme] .main-box .alert.flat.attention { + background-color: var(--st-alert-attention-bg) !important; + border-color: var(--st-alert-attention-border) !important; +} + +body[data-st-theme] .main-box .alert.flat.attention .title { + color: var(--st-alert-attention-title) !important; +} + +body[data-st-theme] .main-box .alert.callout { + background-color: var(--st-bg-soft) !important; + border-color: var(--st-border) !important; +} + +body[data-st-theme] .main-box .alert.callout.note { + border-left-color: var(--st-alert-note-border) !important; +} + +body[data-st-theme] .main-box .alert.callout.note .title { + color: var(--st-alert-note-title) !important; +} + +body[data-st-theme] .main-box .alert.callout.tip { + border-left-color: var(--st-alert-tip-border) !important; +} + +body[data-st-theme] .main-box .alert.callout.tip .title { + color: var(--st-alert-tip-title) !important; +} + +body[data-st-theme] .main-box .alert.callout.warning { + border-left-color: var(--st-alert-warn-border) !important; +} + +body[data-st-theme] .main-box .alert.callout.warning .title { + color: var(--st-alert-warn-title) !important; +} + +body[data-st-theme] .main-box .alert.callout.attention { + border-left-color: var(--st-alert-attention-border) !important; +} + +body[data-st-theme] .main-box .alert.callout.attention .title { + color: var(--st-alert-attention-title) !important; +} + +/* —— 赞助 / 团队 —— */ +body[data-st-theme] .zanzhu-sort-box .zanzhu-sort-btn { + color: var(--st-fg-muted) !important; +} + +body[data-st-theme] .zanzhu-sort-box .zanzhu-sort-btn:hover, +body[data-st-theme] .zanzhu-sort-box .zanzhu-sort-btn.zz-sort-native { + color: var(--st-link) !important; +} + +body[data-st-theme] .zz-btn-box { + color: var(--st-fg-muted) !important; +} + +body[data-st-theme] .zz-btn-box button, +body[data-st-theme] .syzz-show-btn { + background-color: var(--st-bg-elevated); + color: var(--st-fg) !important; + border-color: var(--st-border); +} + +body[data-st-theme] .zanzhu-box table tr td:nth-child(2) { + color: #f48771 !important; +} + +/* —— 页脚:首行保持正常可读,仅压暗下方说明与备案 —— */ +body[data-st-theme] #main footer > span:first-of-type, +body[data-st-theme] #main footer > a:not(.footer-beian) { + color: var(--st-fg) !important; +} + +body[data-st-theme] #main footer > span:first-of-type a, +body[data-st-theme] #main footer > a:not(.footer-beian) { + color: var(--st-link) !important; + border-bottom-color: var(--st-link) !important; +} + +body[data-st-theme] #main footer > span:first-of-type a:hover, +body[data-st-theme] #main footer > a:not(.footer-beian):hover { + color: var(--st-link-hover) !important; + border-bottom-color: var(--st-link-hover) !important; +} + +body[data-st-theme] #main footer p, +body[data-st-theme] #main footer p[style], +body[data-st-theme] #main footer p a { + color: var(--st-footer-fg) !important; + border-bottom-color: transparent !important; +} + +body[data-st-theme] #main footer p a:hover { + color: var(--st-fg-muted) !important; +} + +body[data-st-theme] #main footer > span[style], +body[data-st-theme] #main footer a.footer-beian { + color: var(--st-footer-link) !important; + border-bottom-color: transparent !important; +} + +body[data-st-theme] #main footer a.footer-beian:hover { + color: var(--st-fg-muted) !important; + border-bottom-color: transparent !important; +} + +body[data-st-theme] #main footer hr { + border-color: var(--st-border) !important; + opacity: 0.6; +} + +body[data-st-theme] .hover-change-img { + border-color: var(--st-border); +} diff --git a/sa-token-doc/static/water-change-theme/water-change-theme.js b/sa-token-doc/static/water-change-theme/water-change-theme.js new file mode 100644 index 000000000..0fb24d6c9 --- /dev/null +++ b/sa-token-doc/static/water-change-theme/water-change-theme.js @@ -0,0 +1,194 @@ +// 暗色主题(参考常见 IDE) +const ST_DARK_THEMES = [ + { id: 'vscode-dark', name: 'VS Code Dark+', color: '#1e1e1e' }, + { id: 'darcula', name: 'JetBrains Darcula', color: '#2b2b2b' }, + { id: 'one-dark', name: 'Atom One Dark', color: '#282c34' }, + { id: 'dracula', name: 'Dracula', color: '#282a36' }, + { id: 'tokyo-night', name: 'Tokyo Night', color: '#1a1b26' } +]; + +const ST_THEME_MODE_KEY = 'st-theme-mode'; +const ST_DARK_ID_KEY = 'st-dark-theme-id'; +const ST_BG_COLOR_KEY = 'bg-color-value'; + +function getDarkTheme(id) { + for (let i = 0; i < ST_DARK_THEMES.length; i++) { + if (ST_DARK_THEMES[i].id === id) { + return ST_DARK_THEMES[i]; + } + } + return null; +} + +function clearLightInlineStyles() { + document.body.style.backgroundColor = ''; + const header = document.querySelector('.doc-header'); + if (header) { + header.style.backgroundColor = ''; + } +} + +// 应用暗色主题 +function setDarkTheme(themeId) { + const theme = getDarkTheme(themeId); + if (!theme) { + return; + } + clearLightInlineStyles(); + document.body.setAttribute('data-st-theme', theme.id); + console.log('---- 暗色主题设定为:', theme.name); +} + +// 绑定修改背景色 / 暗色主题 +$('.theme-box').on('click', 'span', function() { + const darkId = this.getAttribute('data-st-dark'); + const rect = this.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + + if (darkId) { + const theme = getDarkTheme(darkId); + if (!theme) { + return; + } + createWaterDrop(x - 7, y + 5, theme.color, function() { + setDarkTheme(theme.id); + }); + localStorage.setItem(ST_THEME_MODE_KEY, 'dark'); + localStorage.setItem(ST_DARK_ID_KEY, theme.id); + return; + } + + // 浅色:只改背景 + const bgColor = this.style.backgroundColor; + createWaterDrop(x - 7, y + 5, bgColor, function() { + document.body.removeAttribute('data-st-theme'); + setBg(bgColor); + }); + localStorage.setItem(ST_THEME_MODE_KEY, 'light'); + localStorage.removeItem(ST_DARK_ID_KEY); + localStorage.setItem(ST_BG_COLOR_KEY, bgColor); +}); + +// 创建水滴动画(onDone:扩散结束后的回调) +function createWaterDrop(x, y, color, onDone) { + const waterDrop = document.createElement('div'); + waterDrop.className = 'water-drop'; + waterDrop.style.backgroundColor = color; + waterDrop.style.left = `${x}px`; + waterDrop.style.top = `${y}px`; + + document.body.appendChild(waterDrop); + + const viewportHeight = window.innerHeight; + + gsap.to(waterDrop, { + top: viewportHeight - 30, + duration: 1.5, + ease: "power2.in", + onComplete: function() { + document.body.removeChild(waterDrop); + createColorWave(x, viewportHeight, color, onDone); + } + }); +} + + +// 创建颜色扩散效果 +function createColorWave(x, y, color, onDone) { + const colorWave = document.createElement('div'); + colorWave.className = 'color-wave'; + colorWave.style.backgroundColor = color; + + const maxDistance = Math.sqrt( + Math.pow(Math.max(x, window.innerWidth - x), 2) + + Math.pow(Math.max(y, window.innerHeight - y), 2) + ); + + colorWave.style.width = `${maxDistance * 2}px`; + colorWave.style.height = `${maxDistance * 2}px`; + colorWave.style.left = `${x - maxDistance}px`; + colorWave.style.top = `${y - maxDistance}px`; + + document.body.appendChild(colorWave); + + gsap.to(colorWave, { + scale: 1, + duration: 1.2, + ease: "power2.out", + onComplete: function() { + if (typeof onDone === 'function') { + onDone(); + } else { + setBg(color); + } + + setTimeout(() => { + document.body.removeChild(colorWave); + }, 500); + } + }); +} + + +// 读取上次记录 +(function restoreTheme() { + const mode = localStorage.getItem(ST_THEME_MODE_KEY); + if (mode === 'dark') { + const darkId = localStorage.getItem(ST_DARK_ID_KEY); + if (getDarkTheme(darkId)) { + setDarkTheme(darkId); + return; + } + } + + const bgColor = localStorage.getItem(ST_BG_COLOR_KEY); + if (bgColor) { + document.body.removeAttribute('data-st-theme'); + setBg(bgColor); + } +})(); + +// 设置背景颜色(浅色模式) +function setBg(bgColor) { + console.log('---- 背景颜色设定为:', bgColor); + + document.body.style.backgroundColor = bgColor; + + let headerBg = bgColor; + // 如果是 16 进制,转 rgba + if (headerBg.indexOf('#') == 0) { + headerBg = hexToRgba(headerBg, 0.97); + } + // 如果是 rgb,转 rgba + else if (headerBg.match(/\,/g) && headerBg.match(/\,/g).length == 2) { + headerBg = headerBg.replace(')', ' ,0.97)'); + } + + document.querySelector('.doc-header').style.backgroundColor = headerBg; +} + +// 16进制 转 rgba +function hexToRgba(str, a) { + a = a || 1; + + var reg = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/ + if (!reg.test(str)) { + return; + } + let newStr = (str.toLowerCase()).replace(/\#/g, '') + let len = newStr.length; + if (len == 3) { + let t = '' + for (var i = 0; i < len; i++) { + t += newStr.slice(i, i + 1).concat(newStr.slice(i, i + 1)) + } + newStr = t + } + let arr = []; + for (var i = 0; i < 6; i = i + 2) { + let s = newStr.slice(i, i + 2) + arr.push(parseInt("0x" + s)) + } + return 'rgb(' + arr.join(",") + ', ' + a + ')'; +} diff --git a/sa-token-doc/static/zoom-image.min.js b/sa-token-doc/static/zoom-image.min.js new file mode 100644 index 000000000..8912c492c --- /dev/null +++ b/sa-token-doc/static/zoom-image.min.js @@ -0,0 +1 @@ +!function(){function o(e){return"IMG"===e.tagName}function E(e){return e&&1===e.nodeType}function c(e){return".svg"===(e.currentSrc||e.src).substr(-4).toLowerCase()}function m(e){try{return Array.isArray(e)?e.filter(o):(t=e,NodeList.prototype.isPrototypeOf(t)?[].slice.call(e).filter(o):E(e)?[e].filter(o):"string"==typeof e?[].slice.call(document.querySelectorAll(e)).filter(o):[])}catch(e){throw new TypeError("The provided selector is invalid.\nExpects a CSS selector, a Node element, a NodeList or an array.\nSee: https://github.com/francoischalifour/medium-zoom")}var t}function w(e,t){var o=l({bubbles:!1,cancelable:!1,detail:void 0},t);return"function"==typeof window.CustomEvent?new CustomEvent(e,o):((t=document.createEvent("CustomEvent")).initCustomEvent(e,o.bubbles,o.cancelable,o.detail),t)}function a(e,t){function o(e){function u(){var e={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},t=void 0,o=void 0;v.container&&(v.container instanceof Object?(t=(e=l({},e,v.container)).width-e.left-e.right-2*v.margin,o=e.height-e.top-e.bottom-2*v.margin):(d=(i=(E(v.container)?v.container:document.querySelector(v.container)).getBoundingClientRect()).width,a=i.height,r=i.left,m=i.top,e=l({},e,{width:d,height:a,left:r,top:m}))),t=t||e.width-2*v.margin,o=o||e.height-2*v.margin;var n=z.zoomedHd||z.original,i=!c(n)&&n.naturalWidth||t,d=!c(n)&&n.naturalHeight||o,r=(a=n.getBoundingClientRect()).top,m=a.left,n=a.width,a=a.height,i=Math.min(i,t)/n,d=Math.min(d,o)/a,d="scale("+(d=Math.min(i,d))+") translate3d("+((t-n)/2-m+v.margin+e.left)/d+"px, "+((o-a)/2-r+v.margin+e.top)/d+"px, 0)";z.zoomed.style.transform=d,z.zoomedHd&&(z.zoomedHd.style.transform=d)}var s=(0v.scrollOffset&&setTimeout(p,150))}),window.addEventListener("resize",p);var b={open:o,close:p,toggle:d,update:function(e){var t=0 + + +当我们输入账号密码后 `(sa / 123456)`,才可以继续访问数据: + +sa-basic-ok.png + + +### 2、其它启用方式 +``` java +// 对当前会话进行 Http Basic 校验,账号密码为 yml 配置的值(例如:sa-token.http-basic=sa:123456) +SaHttpBasicUtil.check(); + +// 对当前会话进行 Http Basic 校验,账号密码为:`sa / 123456` +SaHttpBasicUtil.check("sa:123456"); + +// 以注解方式启用 Http Basic 校验 +@SaCheckHttpBasic(account = "sa:123456") +@RequestMapping("test3") +public SaResult test3() { + return SaResult.ok(); +} + +// 在全局拦截器 或 过滤器中启用 Basic 认证 +@Bean +public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + .addInclude("/**").addExclude("/favicon.ico") + .setAuth(obj -> { + SaRouter.match("/test/**", () -> SaHttpBasicUtil.check("sa:123456")); + }); +} +``` + +### 3、URL 认证 +除了访问后再输入账号密码外,我们还可以在 URL 中直接拼接账号密码通过 Basic 认证,例如: +``` url +http://sa:123456@127.0.0.1:8081/test/test3 +``` + + +### 4、Http Digest 认证 + +Http Digest 认证是 Http Basic 认证的升级版,Http Digest 在提交请求时不会使用明文方式传输认证信息,而是使用一定的规则加密后提交。 +不过对于开发者来讲,开启 Http Digest 认证校验的流程与 Http Basic 认证基本是一致的。 + +``` java +// 测试 Http Digest 认证 浏览器访问: http://localhost:8081/test/testDigest +@RequestMapping("testDigest") +public SaResult testDigest() { + SaHttpDigestUtil.check("sa", "123456"); + return SaResult.ok(); +} + +// 使用注解方式开启 Http Digest 认证 +@SaCheckHttpDigest("sa:123456") +@RequestMapping("testDigest2") +public SaResult testDigest() { + return SaResult.ok(); +} + + +// 对当前会话进行 Http Digest 校验,账号密码为 yml 配置的值(例如:sa-token.http-digest=sa:123456) +SaHttpDigestUtil.check(); +``` + +与上面的 Http Basic 认证一致,在访问这个路由时,浏览器会强制弹出一个表单,客户端输入正确的账号密码后即可通过校验。 + +同样的,Http Digest 也支持在浏览器访问接口时直接使用 @ 符号拼接账号密码信息,使客户端直接通过校验。 + +``` url +http://sa:123456@127.0.0.1:8081/test/testDigest +``` + + + +--- + + + 本章代码示例:Sa-Token Http Basic 认证 —— [ HttpBasicController.java ] + + + + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-doc/up/disable.md b/sa-token-doc/up/disable.md new file mode 100644 index 000000000..f9f892a53 --- /dev/null +++ b/sa-token-doc/up/disable.md @@ -0,0 +1,304 @@ +# 账号封禁 + +之前的章节中,我们学习了 踢人下线 和 强制注销 功能,用于清退违规账号。 + +在部分场景下,我们还需要将其 **账号封禁**,以防止其再次登录。 + +--- + +### 1、账号封禁 + +对指定账号进行封禁: + +``` java +// 封禁指定账号 +StpUtil.disable(10001, 86400); +``` + +参数含义: +- 参数1:要封禁的账号id。 +- 参数2:封禁时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。 + + +注意点:对于正在登录的账号,将其封禁并不会使它立即掉线,如果我们需要它即刻下线,可采用先踢再封禁的策略,例如:
    +``` java +// 先踢下线 +StpUtil.kickout(10001); +// 再封禁账号 +StpUtil.disable(10001, 86400); +``` + +待到下次登录时,我们先校验一下这个账号是否已被封禁: +``` java +// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException` +StpUtil.checkDisable(10001); + +// 通过校验后,再进行登录: +StpUtil.login(10001); +``` + +> [!ATTENTION| label:升级注意:] +> 旧版本在 `StpUtil.login()` 时会自动校验账号是否被封禁,v1.31.0 之后将 校验封禁 和 登录 两个动作分离成两个方法,不再自动校验,请注意其中的逻辑更改。 + +此模块所有方法: +``` java +// 封禁指定账号 +StpUtil.disable(10001, 86400); + +// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁) +StpUtil.isDisable(10001); + +// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException` +StpUtil.checkDisable(10001); + +// 获取指定账号剩余封禁时间,单位:秒,如果该账号未被封禁,则返回-2 +StpUtil.getDisableTime(10001); + +// 解除封禁 +StpUtil.untieDisable(10001); +``` + + +### 2、分类封禁 + +有的时候,我们并不需要将整个账号禁掉,而是只禁止其访问部分服务。 + +假设我们在开发一个电商系统,对于违规账号的处罚,我们设定三种分类封禁: + +- 1、封禁评价能力:账号A 因为多次虚假好评,被限制订单评价功能。 +- 2、封禁下单能力:账号B 因为多次薅羊毛,被限制下单功能。 +- 3、封禁开店能力:账号C 因为店铺销售假货,被限制开店功能。 + +相比于封禁账号的一刀切处罚,这里的关键点在于:每一项能力封禁的同时,都不会对其它能力造成影响。 + +也就是说我们需要一种只对部分服务进行限制的能力,对应到代码层面,就是只禁止部分接口的调用。 + +``` java +// 封禁指定用户评论能力,期限为 1天 +StpUtil.disable(10001, "comment", 86400); +``` +参数释义: +- 参数1:要封禁的账号id。 +- 参数2:针对这个账号,要封禁的服务标识(可以是任意的自定义字符串)。 +- 参数3:要封禁的时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。 + +分类封禁模块所有可用API: + +``` java +/* + * 以下示例中:"comment"=评论服务标识、"place-order"=下单服务标识、"open-shop"=开店服务标识 + */ + +// 封禁指定用户评论能力,期限为 1天 +StpUtil.disable(10001, "comment", 86400); + +// 在评论接口,校验一下,会抛出异常:`DisableServiceException`,使用 e.getService() 可获取业务标识 `comment` +StpUtil.checkDisable(10001, "comment"); + +// 在下单时,我们校验一下 下单能力,并不会抛出异常,因为我们没有限制其下单功能 +StpUtil.checkDisable(10001, "place-order"); + +// 现在我们再将其下单能力封禁一下,期限为 7天 +StpUtil.disable(10001, "place-order", 86400 * 7); + +// 然后在下单接口,我们添加上校验代码,此时用户便会因为下单能力被封禁而无法下单(代码抛出异常) +StpUtil.checkDisable(10001, "place-order"); + +// 但是此时,用户如果调用开店功能的话,还是可以通过,因为我们没有限制其开店能力 (除非我们再调用了封禁开店的代码) +StpUtil.checkDisable(10001, "open-shop"); +``` + +通过以上示例,你应该大致可以理解 `业务封禁 -> 业务校验` 的处理步骤。 + +有关分类封禁的所有方法: +``` java +// 封禁:指定账号的指定服务 +StpUtil.disable(10001, "<业务标识>", 86400); + +// 判断:指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁) +StpUtil.isDisable(10001, "<业务标识>"); + +// 校验:指定账号的指定服务 是否已被封禁,如果被封禁则抛出异常 `DisableServiceException` +StpUtil.checkDisable(10001, "<业务标识>"); + +// 获取:指定账号的指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) +StpUtil.getDisableTime(10001, "<业务标识>"); + +// 解封:指定账号的指定服务 +StpUtil.untieDisable(10001, "<业务标识>"); +``` + + +### 3、阶梯封禁 + +对于多次违规的用户,我们常常采取阶梯处罚的策略,这种 “阶梯” 一般有两种形式: + +- 处罚时间阶梯:首次违规封禁 1 天,第二次封禁 7 天,第三次封禁 30 天,依次顺延…… +- 处罚力度阶梯:首次违规消息提醒、第二次禁言禁评论、第三次禁止账号登录,等等…… + +基于处罚时间的阶梯,我们只需在封禁时 `StpUtil.disable(10001, 86400)` 传入不同的封禁时间即可,下面我们着重探讨一下基于处罚力度的阶梯形式。 + +假设我们在开发一个论坛系统,对于违规账号的处罚,我们设定三种力度: + +- 1、轻度违规:封禁其发帖、评论能力,但允许其点赞、关注等操作。 +- 2、中度违规:封禁其发帖、评论、点赞、关注等一切与别人互动的能力,但允许其浏览帖子、浏览评论。 +- 3、重度违规:封禁其登录功能,限制一切能力。 + +解决这种需求的关键在于,我们需要把不同处罚力度,量化成不同的处罚等级,比如上述的 `轻度`、`中度`、`重度` 3 个力度, +我们将其量化为`一级封禁`、`二级封禁`、`三级封禁` 3个等级,数字越大代表封禁力度越高。 + +然后我们就可以使用阶梯封禁的API,进行鉴权了: + +``` java +// 阶梯封禁,参数:封禁账号、封禁级别、封禁时间 +StpUtil.disableLevel(10001, 3, 10000); + +// 获取:指定账号封禁的级别 (如果此账号未被封禁则返回 -2) +StpUtil.getDisableLevel(10001); + +// 判断:指定账号是否已被封禁到指定级别,返回 true 或 false +StpUtil.isDisableLevel(10001, 3); + +// 校验:指定账号是否已被封禁到指定级别,如果已达到此级别(例如已被3级封禁,这里校验是否达到2级),则抛出异常 `DisableServiceException` +StpUtil.checkDisableLevel(10001, 2); +``` + +注意点:`DisableServiceException` 异常代表当前账号未通过封禁校验,可以: +- 通过 `e.getLevel()` 获取这个账号实际被封禁的等级。 +- 通过 `e.getLimitLevel()` 获取这个账号在校验时要求低于的等级。当 `Level >= LimitLevel` 时,框架就会抛出异常。 + +如果业务足够复杂,我们还可能将 分类封禁 和 阶梯封禁 组合使用: + +``` java +// 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间 +StpUtil.disableLevel(10001, "comment", 3, 10000); + +// 获取:指定账号的指定服务 封禁的级别 (如果此账号未被封禁则返回 -2) +StpUtil.getDisableLevel(10001, "comment"); + +// 判断:指定账号的指定服务 是否已被封禁到指定级别,返回 true 或 false +StpUtil.isDisableLevel(10001, "comment", 3); + +// 校验:指定账号的指定服务 是否已被封禁到指定级别(例如 comment服务 已被3级封禁,这里校验是否达到2级),如果已达到此级别,则抛出异常 +StpUtil.checkDisableLevel(10001, "comment", 2); +``` + + + +### 4、使用注解完成封禁校验 +首先我们需要注册 Sa-Token 全局拦截器(可参考 [注解鉴权](/use/at-check) 章节),然后我们就可以使用以下注解校验账号是否封禁 + +``` java +// 校验当前账号是否被封禁,如果已被封禁会抛出异常,无法进入方法 +@SaCheckDisable +@PostMapping("send") +public SaResult send() { + // ... + return SaResult.ok(); +} + +// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 +@SaCheckDisable("comment") +@PostMapping("send") +public SaResult send() { + // ... + return SaResult.ok(); +} + +// 校验当前账号是否被封禁 comment、place-order、open-shop 等服务,指定多个值,只要有一个已被封禁,就无法进入方法 +@SaCheckDisable({"comment", "place-order", "open-shop"}) +@PostMapping("send") +public SaResult send() { + // ... + return SaResult.ok(); +} + +// 阶梯封禁,校验当前账号封禁等级是否达到5级,如果达到则抛出异常 +@SaCheckDisable(level = 5) +@PostMapping("send") +public SaResult send() { + // ... + return SaResult.ok(); +} + +// 分类封禁 + 阶梯封禁 校验:校验当前账号的 comment 服务,封禁等级是否达到5级,如果达到则抛出异常 +@SaCheckDisable(value = "comment", level = 5) +@PostMapping("send") +public SaResult send() { + // ... + return SaResult.ok(); +} +``` + + +### 5、封禁信息持久化 + +Sa-Token 默认将封禁信息储存在缓存中,缓存中的数据是“临时性的”、“易丢失的”,而在大多数系统的设计中,需要将封禁数据持久化到数据库中。 + +要使封禁信息持久化,你只需要在调用 Sa-Token 的封禁 API 后,再继续调用插入数据库的代码即可,形如: + +``` java +// 在 Sa-Token 框架中封禁指定账号 +StpUtil.disable(10001, 86400); + +// 更改数据库中此人信息 (举例代码) +userMapper.disableUser(10001); +``` + +这样即可保证封禁数据同步插入到缓存和数据库中,但是还有一个问题,如果我们的程序或缓存中间件重启了,导致缓存数据丢失, +那再调用 `StpUtil.checkDisable(10001)` 代码将没有效果,无法约束到此用户。 + +比较次的解决方案是在程序启动时,读取数据库中所有封禁信息同步到缓存中去,但是如果封禁记录较多这样将会严重拖慢程序启动时间。 + +Sa-Token 提供一种方案,可以在你调用 `StpUtil.checkDisable(10001)` 校验封禁时才会触发查询数据库 10001 账号到底有没有被封禁。 +你只需要实现 `StpInterface` 的 `isDisabled` 方法即可,例: + +``` java +@Component +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回指定账号 id 是否被封禁 + * + * @param loginId 账号id + * @param service 业务标识符 + * @return 描述该账号是否封禁的包装信息对象 + */ + public SaDisableWrapperInfo isDisabled(Object loginId, String service) { + // 查库操作 ... (此处仅做示例代码) + return SaDisableWrapperInfo.createDisabled(86400, 1); + } + +} +``` + +该方法返回一个 `SaDisableWrapperInfo` 实例对象,用来描述指定账号是否已被封禁,一般有以下几种写法: +``` java +// 标准写法:new 对象返回,参数为:是否被封禁、封禁时间(秒)、封禁等级 +public SaDisableWrapperInfo isDisabled(Object loginId, String service) { + return new SaDisableWrapperInfo(true, 86400, 1); +} + +// 快捷写法:被封禁,解封倒计时86400秒,封禁等级1 +public SaDisableWrapperInfo isDisabled(Object loginId, String service) { + return SaDisableWrapperInfo.createDisabled(86400, 1); +} + +// 快捷写法:未被封禁 +public SaDisableWrapperInfo isDisabled(Object loginId, String service) { + return SaDisableWrapperInfo.createNotDisabled(); +} + +// 快捷写法:未被封禁,且将查询结果保存到缓存中,ttl为86400,改时间内不再重复进入 isDisabled 方法 +public SaDisableWrapperInfo isDisabled(Object loginId, String service) { + return SaDisableWrapperInfo.createNotDisabled(86400); +} +``` + + +--- + + + 本章代码示例:Sa-Token 账号禁用 —— [ DisableController.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/global-filter.md b/sa-token-doc/up/global-filter.md new file mode 100644 index 000000000..3ee24375a --- /dev/null +++ b/sa-token-doc/up/global-filter.md @@ -0,0 +1,148 @@ +# 全局过滤器 +--- + +### 组件简述 + +之前的章节中,我们学习了“根据拦截器实现路由拦截鉴权”,其实在大多数web框架中,使用过滤器可以实现同样的功能,本章我们就利用Sa-Token全局过滤器来实现路由拦截器鉴权。 + +首先我们先梳理清楚一个问题,既然拦截器已经可以实现路由鉴权,为什么还要用过滤器再实现一遍呢?简而言之: +1. 相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描。 +2. 过滤器可以拦截静态资源,方便我们做一些权限控制。 +3. 部分Web框架根本就没有提供拦截器功能,但几乎所有的Web框架都会提供过滤器机制。 + +但是过滤器也有一些缺点,比如: +1. 由于太过底层,导致无法率先拿到`HandlerMethod`对象,无法据此添加一些额外功能。 +2. 由于拦截的太全面了,导致我们需要对很多特殊路由(如`/favicon.ico`)做一些额外处理。 +3. 在Spring中,过滤器中抛出的异常无法进入全局`@ExceptionHandler`,我们必须额外编写代码进行异常处理。 + +Sa-Token同时提供过滤器和拦截器机制,不是为了让谁替代谁,而是为了让大家根据自己的实际业务合理选择,拥有更多的发挥空间。 + + +### 在 SpringBoot 中注册过滤器 +同拦截器一样,为了避免不必要的性能浪费,Sa-Token全局过滤器默认处于关闭状态,若要使用过滤器组件,首先你需要注册它到项目中: +``` java +/** + * [Sa-Token 权限认证] 配置类 + */ +@Configuration +public class SaTokenConfigure { + + /** + * 注册 [Sa-Token全局过滤器] + */ + @Bean + public SaServletFilter getSaServletFilter() { + return new SaServletFilter() + + // 指定 拦截路由 与 放行路由 + .addInclude("/**").addExclude("/favicon.ico") /* 排除掉 /favicon.ico */ + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + System.out.println("---------- 进入Sa-Token全局认证 -----------"); + + // 登录认证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 + SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin()); + + // 更多拦截处理方式,请参考“路由拦截式鉴权”章节 */ + }) + + // 异常处理函数:每次认证函数发生异常时执行此函数 + .setError(e -> { + System.out.println("---------- 进入Sa-Token异常处理 -----------"); + return SaResult.error(e.getMessage()); + }) + + // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) + .setBeforeAuth(r -> { + // ---------- 设置一些安全响应头 ---------- + SaHolder.getResponse() + // 服务器名称 + .setServer("sa-server") + // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 + .setHeader("X-Frame-Options", "SAMEORIGIN") + // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 + .setHeader("X-XSS-Protection", "1; mode=block") + // 禁用浏览器内容嗅探 + .setHeader("X-Content-Type-Options", "nosniff") + ; + }) + ; + } + +} +``` + +> [!WARNING| label:注意事项:] +> - 在`[认证函数]`里,你可以写和拦截器里一致的代码,进行路由匹配鉴权,参考:[路由拦截鉴权](/use/route-check)。 +> - 由于过滤器中抛出的异常不进入全局异常处理,所以你必须提供`[异常处理函数]`来处理`[认证函数]`里抛出的异常。 +> - 在`[异常处理函数]`里的返回值,将作为字符串输出到前端,如果需要定制化返回数据,请注意其中的格式转换。 + +改写 `setError` 函数的响应格式示例: +``` java +.setError(e -> { + // 设置响应头 + SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); + // 使用封装的 JSON 工具类转换数据格式 + return JSONUtil.toJsonStr( SaResult.error(e.getMessage()) ); +}) +``` +JSON 工具类可参考:[Hutool-Json](https://hutool.cn/docs/#/json/JSONUtil) + + +### 自定义过滤器执行顺序 + +SaServletFilter 默认执行顺序为 `-100`,如果你要自定义过滤器的执行顺序,可以使用 `FilterRegistrationBean` 注册,参考: + +``` java +/** + * 注册 [Sa-Token 全局过滤器] + */ +@Bean +public FilterRegistrationBean getSaServletFilter() { + FilterRegistrationBean frBean = new FilterRegistrationBean<>(); + frBean.setFilter( + new SaServletFilter() + .addInclude("/**") + .setAuth(obj -> { + // .... + }) + // 等等,其它代码 ... + ); + frBean.setOrder(-101); // 更改顺序为 -101 + return frBean; +} +``` + +在 SpringBoot 中, Order 值越小,执行时机越靠前。 + + +### 在 WebFlux 中注册过滤器 +`Spring WebFlux`中不提供拦截器机制,因此若你的项目需要路由鉴权功能,过滤器是你唯一的选择,在`Spring WebFlux`注册过滤器的流程与上述流程几乎完全一致, +除了您需要将过滤器名称由`SaServletFilter`更换为`SaReactorFilter`以外,其它所有步骤均可参考以上示例。 +``` java +/** + * [Sa-Token 权限认证] 配置类 + */ +@Configuration +public class SaTokenConfigure { + + /** + * 注册 [Sa-Token全局过滤器] + */ + @Bean + public SaReactorFilter getSaReactorFilter() { + return new SaReactorFilter() + // 其它代码... + ; + } + +} +``` + +--- + + + 本章代码示例:Sa-Token 全局过滤器 —— [ SaTokenConfigure.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/global-listener.md b/sa-token-doc/up/global-listener.md new file mode 100644 index 000000000..33c6418c9 --- /dev/null +++ b/sa-token-doc/up/global-listener.md @@ -0,0 +1,226 @@ +# 全局侦听器 + +--- + +### 1、工作原理 + +Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。 + +事件触发流程大致如下: + +sa-token-listener + +框架默认内置了侦听器 `SaTokenListenerForLog` 实现:[代码参考](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java) +,功能是控制台 log 打印输出,你可以通过配置`sa-token.is-log=true`开启。 + +要注册自定义的侦听器也非常简单: +1. 新建类实现 `SaTokenListener` 接口。 +2. 将实现类注册到 `SaTokenEventCenter` 事件发布中心。 + + +### 2、自定义侦听器实现 + +##### 2.1、新建实现类: + +新建`MySaTokenListener.java`,实现`SaTokenListener`接口,并添加上注解`@Component`,保证此类被`SpringBoot`扫描到: + +``` java +/** + * 自定义侦听器的实现 + */ +@Component +public class MySaTokenListener implements SaTokenListener { + + /** 每次登录时触发 */ + @Override + public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { + System.out.println("---------- 自定义侦听器实现 doLogin"); + } + + /** 每次注销时触发 */ + @Override + public void doLogout(String loginType, Object loginId, String tokenValue) { + System.out.println("---------- 自定义侦听器实现 doLogout"); + } + + /** 每次被踢下线时触发 */ + @Override + public void doKickout(String loginType, Object loginId, String tokenValue) { + System.out.println("---------- 自定义侦听器实现 doKickout"); + } + + /** 每次被顶下线时触发 */ + @Override + public void doReplaced(String loginType, Object loginId, String tokenValue) { + System.out.println("---------- 自定义侦听器实现 doReplaced"); + } + + /** 每次被封禁时触发 */ + @Override + public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { + System.out.println("---------- 自定义侦听器实现 doDisable"); + } + + /** 每次被解封时触发 */ + @Override + public void doUntieDisable(String loginType, Object loginId, String service) { + System.out.println("---------- 自定义侦听器实现 doUntieDisable"); + } + + /** 每次二级认证时触发 */ + @Override + public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { + System.out.println("---------- 自定义侦听器实现 doOpenSafe"); + } + + /** 每次退出二级认证时触发 */ + @Override + public void doCloseSafe(String loginType, String tokenValue, String service) { + System.out.println("---------- 自定义侦听器实现 doCloseSafe"); + } + + /** 每次创建Session时触发 */ + @Override + public void doCreateSession(String id) { + System.out.println("---------- 自定义侦听器实现 doCreateSession"); + } + + /** 每次注销Session时触发 */ + @Override + public void doLogoutSession(String id) { + System.out.println("---------- 自定义侦听器实现 doLogoutSession"); + } + + /** 每次Token续期时触发 */ + @Override + public void doRenewTimeout(String tokenValue, Object loginId, long timeout) { + System.out.println("---------- 自定义侦听器实现 doRenewTimeout"); + } + + /** 每次Token续期时触发 */ + @Override + public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { + System.out.println("---------- 自定义侦听器实现 doRenewTimeout"); + } + +} +``` + +##### 2.2、将侦听器注册到事件中心: + +以上代码由于添加了 `@Component` 注解,会被 SpringBoot 扫描并自动注册到事件中心,此时我们无需手动注册。 + +如果我们没有添加 `@Component` 注解或者项目属于非 IOC 自动注入环境,则需要我们手动将这个侦听器注册到事件中心: + +``` java +// 将侦听器注册到事件发布中心 +SaTokenEventCenter.registerListener(new MySaTokenListener()); +``` + +事件中心的其它一些常用方法: + +``` java +// 获取已注册的所有侦听器 +SaTokenEventCenter.getListenerList(); + +// 重置侦听器集合 +SaTokenEventCenter.setListenerList(listenerList); + +// 注册一个侦听器 +SaTokenEventCenter.registerListener(listener); + +// 注册一组侦听器 +SaTokenEventCenter.registerListenerList(listenerList); + +// 移除一个侦听器 +SaTokenEventCenter.removeListener(listener); + +// 移除指定类型的所有侦听器 +SaTokenEventCenter.removeListener(cls); + +// 清空所有已注册的侦听器 +SaTokenEventCenter.clearListener(); + +// 判断是否已经注册了指定侦听器 +SaTokenEventCenter.hasListener(listener); + +// 判断是否已经注册了指定类型的侦听器 +SaTokenEventCenter.hasListener(cls); +``` + +##### 2.3、启动测试: +在 `TestController` 中添加登录测试代码: +``` java +// 测试登录接口 +@RequestMapping("login") +public SaResult login() { + System.out.println("登录前"); + StpUtil.login(10001); + System.out.println("登录后"); + return SaResult.ok(); +} +``` + +启动项目,访问登录接口,观察控制台输出: + +sa-token-listener-println + + +### 3、其它注意点 + +##### 3.1、你可以通过继承 `SaTokenListenerForSimple` 快速实现一个侦听器: + +``` java +@Component +public class MySaTokenListener extends SaTokenListenerForSimple { + /* + * SaTokenListenerForSimple 对所有事件提供了空实现,通过继承此类,你只需重写一部分方法即可实现一个可用的侦听器。 + */ + /** 每次登录时触发 */ + @Override + public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { + System.out.println("---------- 自定义侦听器实现 doLogin"); + } +} +``` + +##### 3.2、使用匿名内部类的方式注册: +``` java +// 登录时触发 +SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { + @Override + public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { + System.out.println("---------------- doLogin"); + } +}); +``` + +##### 3.3、使用 try-catch 包裹不安全的代码: +如果你认为你的事件处理代码是不安全的(代码可能在运行时抛出异常),则需要使用 `try-catch` 包裹代码,以防因为抛出异常导致 Sa-Token 的整个登录流程被强制中断。 + +``` java +// 登录时触发 +SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { + @Override + public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { + try { + // 不安全代码需要写在 try-catch 里 + // ...... + } catch (Exception e) { + e.printStackTrace(); + } + } +}); +``` + +##### 3.4、疑问:一个项目可以注册多个侦听器吗? +可以,多个侦听器间彼此独立,互不影响,按照注册顺序依次接受到事件通知。 + + +--- + + + 本章代码示例:Sa-Token 自定义侦听器 —— [ MySaTokenListener.java ] + + diff --git a/sa-token-doc/up/integ-redis.md b/sa-token-doc/up/integ-redis.md new file mode 100644 index 000000000..ba50aef5f --- /dev/null +++ b/sa-token-doc/up/integ-redis.md @@ -0,0 +1,366 @@ +# Sa-Token 集成 Redis +--- + +Sa-Token 默认将数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如: + +1. 重启后数据会丢失。 +2. 无法在分布式环境中共享数据。 + +为此,Sa-Token 提供了扩展接口,你可以轻松将会话数据存储在一些专业的缓存中间件上(比如 Redis), +做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。 + +--- + +### 1、Sa-Token 整合 RedisTemplate + +RedisTemplate 是 SpringBoot 官方推荐的 Redis 客户端,Sa-Token 提供基于 RedisTemplate 的 Redis 整合方案: + + + +``` xml + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + + + + + org.apache.commons + commons-pool2 + +``` + +``` gradle +// Sa-Token 整合 RedisTemplate +implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' + +// 提供 Redis 连接池 +implementation 'org.apache.commons:commons-pool2' +``` + + + +Redis 的集成有多种方式,缓存的方案也不止 Redis 一种,Sa-Token 为缓存方案提供多种扩展实现。 + +如果你对 Sa-Token 还不太熟悉,或者只想“省心省事”,我们推荐你直接使用上述的 RedisTemplate 集成方案,而不必进行过多研究。到此为止,你可以跳转到下一章节了。 + +如果你想对缓存方案再进行一下深入探究,那么你可以参考:[缓存层扩展](/plugin/dao-extend) + + +### 2、自定义序列化方案 + +如果你按照上述 RedisTemplate 方案进行集成测试,会发现框架在 Redis 中是以 json 格式存储数据的。可以自定义数据序列化格式吗?当然是可以的。 + +框架的默认序列化层调用为 `String 序列化` -> `JSON 序列化`。要自定义数据序列化方式你可以从这两方面入手: + + +#### 2.1、自定义 JSON 序列化方案: + +先说较为底层的 `JSON 序列化`,如果你引入的是 sa-token-spring-boot-starter 集成包 (含SpringBoot3) ,那么框架将会自动引入 Jackson 框架作为 JSON 序列化方案。 + +如果你想更换为其它 JSON 解析框架,可以引入相关依赖: + + + + + +``` xml + + + cn.dev33 + sa-token-fastjson + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-fastjson:${sa.top.version}'` + + +``` xml + + + cn.dev33 + sa-token-fastjson2 + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-fastjson2:${sa.top.version}'` + + +``` xml + + + cn.dev33 + sa-token-snack3 + ${sa.top.version} + +``` +Gradle 参考:`implementation 'cn.dev33:sa-token-snack3:${sa.top.version}'` + + + +完整插件列表请参考:[JSON 序列化扩展](/plugin/json-extend) + +#### 2.2、自定义 String 序列化方案: + +或者你想更直接点,不使用 json 序列化方案,也是可以的。你可以直接自定义数据的 String 序列化方案: + + + + +``` java +// 设置序列化方案: jdk序列化 (base64编码) +@PostConstruct +public void rewriteComponent() { + SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseBase64()); +} +``` + + +``` java +// 设置序列化方案: jdk序列化 (16进制编码) +@PostConstruct +public void rewriteComponent() { + SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseHex()); +} +``` + + +``` java +// 设置序列化方案: jdk序列化 (ISO-8859-1编码) +@PostConstruct +public void rewriteComponent() { + SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseISO_8859_1()); +} +``` + + +除了以上的几种序列化方案,我们还提供了序列化扩展包,详细可参考:[序列化插件扩展包](/plugin/custom-serializer) + + +### 3、集成 Redis 请注意: + +**1. 引入了依赖,我还需要为 Redis 配置连接信息吗?**
    +需要!只有项目初始化了正确的 Redis 实例,`Sa-Token`才可以使用 Redis 进行数据持久化,参考以下`yml配置`: + + + +``` yaml +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + # password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 +``` + +``` properties +# Redis数据库索引(默认为0) +spring.redis.database=1 +# Redis服务器地址 +spring.redis.host=127.0.0.1 +# Redis服务器连接端口 +spring.redis.port=6379 +# Redis服务器连接密码(默认为空) +# spring.redis.password= +# 连接超时时间 +spring.redis.timeout=10s +# 连接池最大连接数 +spring.redis.lettuce.pool.max-active=200 +# 连接池最大阻塞等待时间(使用负值表示没有限制) +spring.redis.lettuce.pool.max-wait=-1ms +# 连接池中的最大空闲连接 +spring.redis.lettuce.pool.max-idle=10 +# 连接池中的最小空闲连接 +spring.redis.lettuce.pool.min-idle=0 +``` + + +> [!WARNING| label:小提示 ] +> 如果你使用的是 SpringBoot3.x 版本,则需要将前缀 `spring.redis` 改为 `spring.data.redis`。 + + +**2. 集成 Redis 后,是我额外手动保存数据,还是框架自动保存?**
    +框架自动保存。集成 `Redis` 只需要引入对应的 `pom依赖` 即可,框架所有上层 API 保持不变。 + +**3. 集成包版本问题**
    +Sa-Token-Redis 集成包的版本尽量与 Sa-Token-Starter 集成包的版本一致,否则可能出现兼容性问题。 + + +### 4、多个项目共用同一个 Redis,怎么防止冲突? + +如无特殊需求,建议多个项目不要共用同一个 Redis。如果非要共用,可用以下方式隔离数据: + +**方式 1:使用不同的 db 索引** + +Redis 默认提供 16 个 database,每个项目配置不同的 `spring.redis.database`(SpringBoot3 为 `spring.data.redis.database`)即可。 + +**方式 2:配置不同的 `sa-token.token-name`** + +此配置项默认为 `satoken`,会作为框架在 Redis 中存储数据时的统一前缀,例如: + +``` yaml +sa-token: + token-name: my-app-a +``` + +> [!NOTE| label:注意 ] +> `token-name` 同时也会作为前端提交 Token 时的参数名 / Header 名。若只想隔离 Redis 键、又不想改前端传参方式,请看方式 4。 + +**方式 3:使用 Alone 独立 Redis / Redisson 插件** + +让权限缓存与业务缓存分离,或让不同项目连接不同的 Redis 实例: +- RedisTemplate:[Alone 独立 Redis 插件](/plugin/alone-redis) +- Redisson:[Alone 独立 Redisson 插件](/plugin/alone-redisson) + +**方式 4:重写 `wrapKey` 自定义键前缀(保底方案)** + +`sa-token-redis-template`(及 `sa-token-redis-template-jdk-serializer`)提供了 `wrapKey` 钩子,默认原样返回 key。需要给所有 Redis 键加项目前缀时,可注册自定义 Dao 并重写该方法: + +``` java +@Configuration +public class SaTokenDaoConfig { + @Bean + @Primary + public SaTokenDao saTokenDao() { + return new SaTokenDaoForRedisTemplate() { + @Override + public String wrapKey(String key) { + return "my-app:" + key; + } + }; + } +} +``` + +重写后,框架读写的键会变成类似:`my-app:satoken:login:token:xxxx`。 + + +### 5、集成 Redisson + +如果你的项目用的是 Redisson,而不是上文的 RedisTemplate,可以改用本节方案。这是与第 1 节 **并列的可选项**,不是必须步骤。已经按 RedisTemplate 集成的,不必再引入 Redisson;也不要两套 Dao 同时引入。 + +集成 Redisson 有两种方式,**二选一**即可。 + +#### 5.1、方式一:引入 `sa-token-redisson` + +通用插件,Spring Boot、Solon、JFinal 等都可用。需要项目里已有 `RedissonClient`,并自己注册 `SaTokenDao`。 + + + +``` xml + + + cn.dev33 + sa-token-redisson + ${sa.top.version} + +``` + +``` gradle +implementation 'cn.dev33:sa-token-redisson:${sa.top.version}' +``` + + +然后注册 Dao(`RedissonClient` 由你现有的 Redisson 配置提供): + +``` java +@Configuration +public class SaTokenDaoConfig { + @Bean + public SaTokenDao saTokenDao(RedissonClient redissonClient) { + return new SaTokenDaoForRedisson(redissonClient); + } +} +``` + +#### 5.2、方式二:引入 `sa-token-redisson-spring-boot-starter` + +Spring Boot 专用自动配置包,内部已包含 `sa-token-redisson` 和官方 `redisson-spring-boot-starter`。引入后会自动注册 `SaTokenDao`,不用手写 Java。 + + + +``` xml + + + cn.dev33 + sa-token-redisson-spring-boot-starter + ${sa.top.version} + +``` + +``` gradle +implementation 'cn.dev33:sa-token-redisson-spring-boot-starter:${sa.top.version}' +``` + + +配置 Redis 连接即可(与官方 Redisson starter 相同)。Spring Boot 3.x 请将前缀 `spring.redis` 改为 `spring.data.redis`: + + + +``` yaml +spring: + redis: + host: 127.0.0.1 + port: 6379 + database: 1 + # password: +``` + +``` properties +spring.redis.host=127.0.0.1 +spring.redis.port=6379 +spring.redis.database=1 +# spring.redis.password= +``` + + +示例:[sa-token-demo-springboot-redisson](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-springboot-redisson) + +若需要权限缓存与业务缓存分离,请改用 [Alone 独立 Redisson 插件](/plugin/alone-redisson)。 + +#### 5.3、升级注意 + +`SaTokenDaoForRedisson` 按 String 读写,默认使用 `StringCodec`,与业务 `RedissonClient` 的全局 codec(未配置时为 `Kryo5Codec`)隔离。 + +Sa-Token 旧版本下(version<=v1.45.0) `getBucket(key)` 跟随 `RedissonClient` 全局 `codec` (一般默认是 `Kryo5Codec`)。升级 `sa-token-redisson` 或 `sa-token-redisson-spring-boot-starter` 后改为默认为 `StringCodec`,旧缓存将无法反序列化,登录态会失效。处理方式二选一: + +1. **清空旧缓存**(推荐):删除 Redis 中 Sa-Token 相关 key(默认前缀为配置项 `sa-token.token-name`,一般为 `satoken:`),让用户重新登录。 +2. **保持旧 codec**:重写 `SaTokenDao` 的注册方式,在构造时显式传入升级前的 `codec`。未自定义过 Redisson codec 时,传入 `new Kryo5Codec()`。例如: + +``` java +@Configuration +public class SaTokenDaoConfig { + @Bean + @Primary + public SaTokenDao saTokenDao(RedissonClient redissonClient) { + return new SaTokenDaoForRedisson(redissonClient, new Kryo5Codec()); + } +} +``` + +若你曾在 Redisson 配置里指定过其它 codec(如 `JsonJacksonCodec`),请传入当时使用的那个,而不是 `Kryo5Codec`。 + + +### 6、扩展:集成 MongoDB + +- [集成 MongoDB 参考一](/up/integ-spring-mongod-1) +- [集成 MongoDB 参考二](/up/integ-spring-mongod-2) diff --git a/sa-token-doc/up/integ-spring-mongod-1.md b/sa-token-doc/up/integ-spring-mongod-1.md new file mode 100644 index 000000000..6ec89cbcd --- /dev/null +++ b/sa-token-doc/up/integ-spring-mongod-1.md @@ -0,0 +1,337 @@ +# Sa-Token 集成 MongoDB +--- + +此章介绍如何通过扩展 `SaTokenDao` 接口来实现 MongodDB 的集成。 + +[示例代码:sa-token-mongodb-demo](https://gitee.com/lilihao/sa-token-mongodb-demo) + +先决条件: +1. Spring Boot 3 +2. Spring Data Mongodb + +以下是依赖的引入: + +--- + + + + +``` xml + + + org.springframework.boot + spring-boot-starter-data-mongodb + +``` + +``` gradle +// 引入 spring data mongodb +implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' +``` + + +优点:少量改造即可完成集成 MongodDB + + + + +### 集成代码: + + +**1. 创建一个类来包装`Sa—Token`的数据** +```java +@Document("saTokenMongo") // 你也可以自定义集合名称 +public class SaTokenMongoData { + + @Id + private String id; + + // token + @Indexed(unique = true) + private String key; + + // sa-token 的 session + private SaSession session; + + // sa-token 的 token string + private String string; + + //使用 @SuppressWarnings("removal") 的目的是,防止IDEA报错,因为 expireAfterSeconds是不在支持的属性。 + @SuppressWarnings("removal") + // 给 expireAt 添加 `@Indexed(expireAfterSeconds = 0)` 注解,当过期时MongoDB会自动帮我删除过期的数据 + @Indexed(expireAfterSeconds = 0) + private LocalDateTime expireAt; // 你也可以使用 Date 类型,对应的在`SaTokenMongoDao`中,需要将LocalDateTime替换成Date + + // 忽略 getter setter +} +``` + +**2.实现 SaTokenDao** + +这个 SaTokenMongoDao 是仿照官方的 redis 集成实现的 +```java +package com.xx.xx.security; + +import cn.dev33.satoken.dao.SaTokenDao; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +@Component +public class SaTokenMongoDao implements SaTokenDao { + + private final MongoTemplate mongoTemplate; + + public SaTokenMongoDao(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + + private Query keyQuery(String key) { + return Query.query(Criteria.where("key").is(key)); + } + + /** + * 获取 value,如无返空 + * + * @param key 键名称 + * @return value + */ + @Override + public String get(String key) { + + return Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getString).orElse(null); + } + + + LocalDateTime getExpireAtFromTimeout(long timeout) { + // 当接受到的值是`SaTokenDao.NEVER_EXPIRE`时,说明永不过期,对应的我们需要把 expireAt 设置为null mongodb就不会删除这个记录 + return timeout == SaTokenDao.NEVER_EXPIRE ? null : LocalDateTime.now().plusSeconds(timeout); + } + + /** + * 写入 value,并设定存活时间(单位: 秒) + * + * @param key 键名称 + * @param value 值 + * @param timeout 数据有效期(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) + */ + @Override + public void set(String key, String value, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + + // 判断是否为永不过期 + mongoTemplate.upsert( + keyQuery(key), + Update.update("string", value).set("expireAt", getExpireAtFromTimeout(timeout)), + SaTokenMongoData.class + ); + } + + /** + * 更新 value (过期时间不变) + * + * @param key 键名称 + * @param value 值 + */ + @Override + public void update(String key, String value) { + long expire = getTimeout(key); + // -2 = 无此键 + if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.set(key, value, expire); + } + + /** + * 删除 value + * + * @param key 键名称 + */ + @Override + public void delete(String key) { + mongoTemplate.remove(keyQuery(key), SaTokenMongoData.class); + } + + /** + * 获取 value 的剩余存活时间(单位: 秒) + * + * @param key 指定 key + * @return 这个 key 的剩余存活时间 + */ + @Override + public long getTimeout(String key) { + + LocalDateTime localDateTime = Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getExpireAt).orElse(LocalDateTime.MIN); + + long seconds = Duration.between(LocalDateTime.now(), localDateTime).getSeconds(); + if (seconds < 0) { + return 0; + } + return seconds; + } + + /** + * 修改 value 的剩余存活时间(单位: 秒) + * + * @param key 指定 key + * @param timeout 过期时间(单位: 秒) + */ + @Override + public void updateTimeout(String key, long timeout) { + // 判断是否想要设置为永久 + if (timeout == SaTokenDao.NEVER_EXPIRE) { + long expire = getTimeout(key); + //noinspection StatementWithEmptyBody + if (expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.set(key, this.get(key), timeout); + } + return; + } + + mongoTemplate.upsert( + keyQuery(key), + Update.update("expireAt", getExpireAtFromTimeout(timeout)), + SaTokenMongoData.class + ); + } + + /** + * 获取 Object,如无返空 + * + * @param key 键名称 + * @return object + */ + @Override + public Object getObject(String key) { + return Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getSession).orElse(null); + } + + /** + * 写入 Object,并设定存活时间 (单位: 秒) + * + * @param key 键名称 + * @param object 值 + * @param timeout 存活时间(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) + */ + @Override + public void setObject(String key, Object object, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // 判断是否为永不过期 + mongoTemplate.upsert( + keyQuery(key), + Update.update("session", object).set("expireAt", getExpireAtFromTimeout(timeout)), + SaTokenMongoData.class + ); + } + + /** + * 更新 Object (过期时间不变) + * + * @param key 键名称 + * @param object 值 + */ + @Override + public void updateObject(String key, Object object) { + long expire = getObjectTimeout(key); + // -2 = 无此键 + if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.setObject(key, object, expire); + } + + /** + * 删除 Object + * + * @param key 键名称 + */ + @Override + public void deleteObject(String key) { + delete(key); + } + + /** + * 获取 Object 的剩余存活时间 (单位: 秒) + * + * @param key 指定 key + * @return 这个 key 的剩余存活时间 + */ + @Override + public long getObjectTimeout(String key) { + return getTimeout(key); + } + + /** + * 修改 Object 的剩余存活时间(单位: 秒) + * + * @param key 指定 key + * @param timeout 剩余存活时间 + */ + @Override + public void updateObjectTimeout(String key, long timeout) { + // 判断是否想要设置为永久 + updateTimeout(key, timeout); + } + + /** + * 搜索数据 + * + * @param prefix 前缀 + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表从 start 处一直取到末尾) + * @param sortType 排序类型(true=正序,false=反序) + * @return 查询到的数据集合 + */ + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + + List criteriaList = new ArrayList<>(); + + if (StringUtils.hasText(prefix)) { + criteriaList.add(Criteria.where("key").regex(Pattern.compile("^" + Pattern.quote(prefix)))); + } + if (StringUtils.hasText(keyword)) { + Pattern keywordPattern = Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE); + criteriaList.add(Criteria.where("key").regex(keywordPattern)); + } + + + Criteria criteria = new Criteria(); + + if (!criteriaList.isEmpty()) { + criteria.andOperator(criteriaList); + } + + long skip = (long) Math.max(start, 0) * Math.max(size, 1); + + Query query = Query.query(criteria).skip(skip).limit(size); + + query.fields().include("key"); + + return mongoTemplate.find(query, SaTokenMongoData.class).stream().map(SaTokenMongoData::getKey).toList(); + } +} +``` + + + diff --git a/sa-token-doc/up/integ-spring-mongod-2.md b/sa-token-doc/up/integ-spring-mongod-2.md new file mode 100644 index 000000000..321af320b --- /dev/null +++ b/sa-token-doc/up/integ-spring-mongod-2.md @@ -0,0 +1,243 @@ +# Sa-Token 集成 MongoDB +--- + +在 Spring Boot 下集成 MongoDB: + + + +``` xml + + + org.springframework.boot + spring-boot-starter-data-mongodb + +``` + +``` gradle +// 提供MongoDB依赖 +implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' +``` + + +1. 创建一个 `MySaSession` 并继承 `SaSession` +```java +public class MySaSession extends SaSession { + public MySaSession(String id) { + super(id); + } + + public void setDataMap(Map dataMap) { + refreshDataMap(dataMap); + } +} +``` +原因:由于 `SaSession` 中的 `dataMap` 字段没有 `setter` 方法,当 `spring-data-mongodb` 反序列化 `SaSession` 时会报 `Cannot set property dataMap because no setter, no wither and it's not part of the persistence constructor public cn.dev33.satoken.session.SaSession()` 错误 + +2. 在 `SpringBoot` 启动方法中重写 `SaStrategy.instance.createSession` 方法,使我们自定义的 `MySaSession` 生效 +```java +@SpringBootApplication +public class SpringApplication { + + public static void main(String[] args) { + + // 重写 SaStrategy.instance.createSession 方法 + SaStrategy.instance.createSession = (sessionId) -> { + return new MySaSession(sessionId); + }; + + SpringApplication.run(SpringApplication.class, args); + } +} +``` + +3. 实现 SaTokenDao 接口 +```java +//定义一个类用于保存SaSession +@Document +public class SaTokenWrap { + private String id; + private String value; + private Object object; + // 这里利用MongoDB的TTL索引,当过期时MongoDB会自动删除过期的数据,同时如果timeout如果为null那么视为永不删除 + @Indexed(expireAfterSeconds = 1, background = true) + private Date timeout; + + + public boolean live() { + return getTimeout() == null || getTimeout().after(new Date()); + } +} +``` +```java +// SaTokenDao 实现 +@Component +public class SaTokenDaoMongo implements SaTokenDao { + + + private final MongoTemplate mongoTemplate; + + public SaTokenDaoMongo(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + + Optional getByKey(String key) { + SaTokenWrap tokenWrap = mongoTemplate.findById(key, SaTokenWrap.class); + + return Optional.ofNullable(tokenWrap).filter(SaTokenWrap::live); + } + + Date timeoutToDate(long timeout) { + + return new Date(timeout * 1000 + System.currentTimeMillis()); + } + + void upsertByPath(String key, String path, Object value, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + Update update = Update.update(path, value); + + if (timeout != SaTokenDao.NEVER_EXPIRE) { + update.set("timeout", timeoutToDate(timeout)); + } else { + update.unset("timeout"); + } + + mongoTemplate.upsert( + Query.query(Criteria.where("id").is(key)), + update, + SaTokenWrap.class + ); + } + + void updateByPath(String key, String path, Object value) { + mongoTemplate.updateFirst( + Query.query(Criteria.where("id").is(key).and("timeout").gte(new Date())), + Update.update(path, value), + SaTokenWrap.class + ); + } + + // ------------------------ String 读写操作 + + @Override + public String get(String key) { + + return getByKey(key).map(SaTokenWrap::getValue).orElse(null); + } + + @Override + public void set(String key, String value, long timeout) { + upsertByPath(key, "value", value, timeout); + } + + @Override + public void update(String key, String value) { + updateByPath(key, "value", value); + } + + @Override + public void delete(String key) { + mongoTemplate.remove(Query.query(Criteria.where("id").is(key))); + } + + @Override + public long getTimeout(String key) { + + SaTokenWrap tokenWrap = mongoTemplate.findById(key, SaTokenWrap.class); + + if (tokenWrap == null) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + + if (tokenWrap.getTimeout() == null) { + return SaTokenDao.NEVER_EXPIRE; + } + + long expire = tokenWrap.getTimeout().getTime(); + long timeout = (expire - System.currentTimeMillis()) / 1000; + + // 小于零时,视为不存在 + if (timeout < 0) { + mongoTemplate.remove(Query.query(Criteria.where("id").is(key))); + return SaTokenDao.NOT_VALUE_EXPIRE; + } + return timeout; + } + + @Override + public void updateTimeout(String key, long timeout) { + + Update update = new Update(); + + if (timeout == SaTokenDao.NEVER_EXPIRE) { + update.unset("timeout"); + } else { + update.set("timeout", timeoutToDate(timeout)); + } + + mongoTemplate.upsert( + Query.query(Criteria.where("id").is(key)), + update, + SaTokenWrap.class + ); + } + + + // ------------------------ Object 读写操作 + + @Override + public Object getObject(String key) { + return getByKey(key).map(SaTokenWrap::getObject).orElse(null); + } + + @Override + public void setObject(String key, Object object, long timeout) { + upsertByPath(key, "object", object, timeout); + } + + @Override + public void updateObject(String key, Object object) { + updateByPath(key, "object", object); + } + + @Override + public void deleteObject(String key) { + delete(key); + } + + @Override + public long getObjectTimeout(String key) { + return getTimeout(key); + } + + @Override + public void updateObjectTimeout(String key, long timeout) { + updateTimeout(key, timeout); + } + + + // ------------------------ Session 读写操作 + // 使用接口默认实现 + + + // --------- 会话管理 + + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + + List wrapList = mongoTemplate.find( + Query.query(Criteria.where("id").regex(prefix + "*" + keyword + "*").and("timeout").gte(new Date())), + SaTokenWrap.class + ); + + List list = wrapList.stream().map(SaTokenWrap::getValue).filter(StringUtils::hasText).collect(Collectors.toList()); + + return SaFoxUtil.searchList(list, start, size, sortType); + } + + +} +``` + diff --git a/sa-token-doc/up/login-parameter.md b/sa-token-doc/up/login-parameter.md new file mode 100644 index 000000000..bf6784cb5 --- /dev/null +++ b/sa-token-doc/up/login-parameter.md @@ -0,0 +1,108 @@ +# 登录参数 + +### 1、登录参数 + +在之前的章节我们提到,通过 `StpUtil.login(xxx)` 可以完成指定账号登录,同时你可以指定第二个参数来扩展登录信息,比如: + +``` java +// 指定`账号id`和`设备类型`进行登录 +StpUtil.login(10001, "PC"); + +// 设置登录账号 id 为 10001,并指定是否为 “记住我” 模式 +StpUtil.login(10001, false); +``` + +除了以上内容,第二个参数你还可以指定一个 `SaLoginParameter` 对象,来详细控制登录的多个细节,例如: + +``` java +StpUtil.login(10001, new SaLoginParameter() + .setDeviceType("PC") // 此次登录的客户端设备类型, 一般用于完成 [同端互斥登录] 功能 + .setDeviceId("xxxxxxxxx") // 此次登录的客户端设备ID, 登录成功后该设备将标记为可信任设备 + .setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + .setTimeout(60 * 60 * 24 * 7) // 指定此次登录 token 的有效期, 单位:秒,-1=永久有效 + .setActiveTimeout(60 * 60 * 24 * 7) // 指定此次登录 token 的最低活跃频率, 单位:秒,-1=不进行活跃检查 + .setIsConcurrent(true) // 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + .setIsShare(false) // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) + .setMaxLoginCount(12) // 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + .setMaxTryTimes(12) // 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) + .setExtra("key", "value") // 记录在 Token 上的扩展参数(只在 jwt 模式下生效) + .setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token + .setIsWriteHeader(false) // 是否在登录后将 Token 写入到响应头 + .setTerminalExtra("key", "value")// 本次登录挂载到 SaTerminalInfo 的自定义扩展数据 + .setReplacedRange(SaReplacedRange.CURR_DEVICE_TYPE) // 顶人下线的范围: CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端 + .setOverflowLogoutMode(SaLogoutMode.LOGOUT) // 溢出 maxLoginCount 的客户端,将以何种方式注销下线: LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线 + .setRightNowCreateTokenSession(true) // 是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) + .setupCookieConfig(cookie->{ // 设置 Cookie 配置项 + cookie.setDomain("sa-token.com"); // 设置:作用域 + cookie.setPath("/shop"); // 设置:路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。) + cookie.setSecure(true); // 设置:是否只在 https 协议下有效 + cookie.setHttpOnly(true); // 设置:是否禁止 js 操作 Cookie + cookie.setSameSite("Lax"); // 设置:第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) + cookie.addExtraAttr("aa", "bb"); // 设置:额外扩展属性 + } +); +``` + +以上大部分参数在未指定时将使用全局配置作为默认值。 + + + +### 2、注销参数 + +同样的,在调用注销时,也可以指定一些参数决定注销的细节行为: + +``` java +// 当前客户端注销 +StpUtil.logout(new SaLogoutParameter() + // 注销范围: TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话 + // 此参数只在调用 StpUtil.logout() 时有效 + .setRange(SaLogoutRange.TOKEN) +); + +// 指定 token 注销 +StpUtil.logoutByTokenValue("xxxxxxxxxxxxxxxxxxxxxxx", new SaLogoutParameter() + // 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API)(默认 false) + // 此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token") 时有效 + .setIsKeepFreezeOps(false) + // 是否保留此 token 的 Token-Session 对象(默认 false) + .setIsKeepTokenSession(true) +); + +// 指定 loginId 注销 +StpUtil.logout(10001, new SaLogoutParameter() + .setDeviceType("PC") // 设置注销的设备类型 (如果不指定,则默认注销所有客户端) + .setIsKeepTokenSession(true) // 是否保留对应 token 的 Token-Session 对象(默认 false) + .setMode(SaLogoutMode.REPLACED) // 设置注销模式:LOGOUT=注销登录、KICKOUT=踢人下线,REPLACED=顶人下线(默认LOGOUT) +); +``` + +以上大部分参数在未指定时将使用全局配置作为默认值。 + + +### 3、遍历登录终端详细操作 + +如果你的 登录策略 或 注销策略 非常复杂,凭借上述参数无法组合出你的业务场景,你可以手动遍历一个账号的已登录终端信息列表,手动决定某个设备是否下线,例如: + +``` java +// 测试 +@RequestMapping("logout") +public SaResult logout() { + + // 遍历账号 10001 已登录终端列表,进行详细操作 + StpUtil.forEachTerminalList(10001, (session, ter) -> { + // 根据登录顺序,奇数的保留,偶数的下线 + if(ter.getIndex() % 2 == 0) { + StpUtil.removeTerminalByLogout(session, ter); // 注销下线方式 移除这个登录客户端 + // StpUtil.removeTerminalByKickout(session, ter); // 踢人下线方式 移除这个登录客户端 + // StpUtil.removeTerminalByReplaced(session, ter); // 顶人下线方式 移除这个登录客户端 + } + }); + + return SaResult.ok(); +} +``` + + + + + diff --git a/sa-token-doc/up/many-account.md b/sa-token-doc/up/many-account.md new file mode 100644 index 000000000..95e782a5f --- /dev/null +++ b/sa-token-doc/up/many-account.md @@ -0,0 +1,341 @@ +# 多账号认证 +--- + +### 1、需求场景 +有的时候,我们会在一个项目中设计两套账号体系,比如一个电商系统的 `user表` 和 `admin表`, +在这种场景下,如果两套账号我们都使用 `StpUtil` 类的API进行登录鉴权,那么势必会发生逻辑冲突。 + +在Sa-Token中,这个问题的模型叫做:多账号体系认证。 + +要解决这个问题,我们必须有一个合理的机制将这两套账号的授权给区分开,让它们互不干扰才行。 + + +### 2、演进思路 +假如说我们的 user表 和 admin表 都有一个 id=10001 的账号,它们对应的登录代码:`StpUtil.login(10001)` 是一样的, +那么问题来了:在`StpUtil.getLoginId()`获取到的账号id如何区分它是User用户,还是Admin用户? + +你可能会想到为他们加一个固定前缀,比如`StpUtil.login("User_" + 10001)`、`StpUtil.login("Admin_" + 10001)`,这样确实是可以解决问题的, +但是同样的:你需要在`StpUtil.getLoginId()`时再裁剪掉相应的前缀才能获取真正的账号id,这样一增一减就让我们的代码变得无比啰嗦。 + +那么,有没有从框架层面支持的,更优雅的解决方案呢? + + +### 3、解决方案 + +前面几篇介绍的api调用,都是经过 StpUtil 类的各种静态方法进行授权认证, +而如果我们深入它的源码,[点此阅览](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java)
    +就会发现,此类并没有任何代码逻辑,唯一做的事就是对成员变量`stpLogic`的各个API包装一下进行转发。 + +这样做有两个好处: +- StpLogic 类的所有函数都可以被重写,按需扩展。 +- 在构造方法时随意传入一个不同的 `loginType`,就可以再造一套账号登录体系。 + + +### 4、操作示例 + +比如说,对于原生`StpUtil`类,我们只做`admin账号`权限认证,而对于`user账号`,我们则: +1. 新建一个新的权限认证类,比如: `StpUserUtil.java`。 +2. 将`StpUtil.java`类的全部代码复制粘贴到 `StpUserUtil.java`里。 +3. 更改一下其 `LoginType`, 比如: + +``` java +public class StpUserUtil { + + /** + * 账号体系标识 + */ + public static final String TYPE = "user"; // 将 LoginType 从`login`改为`user` + + // 其它代码 ... + +} +``` + +成品样例参考:[码云 StpUserUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java) + +4、接下来就可以像调用`StpUtil.java`一样调用 `StpUserUtil.java`了,这两套账号认证的逻辑是完全隔离的。例如: + +``` java +// 凡是在 StpUtil 上有的方法,都可以在 StpUserUtil 上调用 +StpUserUtil.login(10001); // 在当前会话以10001账号进行登录 +StpUserUtil.checkLogin(); // 校验当前账号是否以 User 身份进行登录 +StpUserUtil.getSession(); // 获取当前 User 账号的 Access-Session 对象 +StpUserUtil.checkPermission('xx'); // 校验当前登录的 user 账号是否具有 xx 权限 +// ... +``` + + +### 5、Kit模式 +如果你觉得 “复制代码” 的方式繁琐不够优雅,这里还有另一种方案:建立一个 `StpKit.java` 门面类,声明所有的 `StpLogic` 引用: +``` java +/** + * StpLogic 门面类,管理项目中所有的 StpLogic 账号体系 + */ +public class StpKit { + + /** + * 默认原生会话对象 + */ + public static final StpLogic DEFAULT = StpUtil.stpLogic; + + /** + * Admin 会话对象,管理 Admin 表所有账号的登录、权限认证 + */ + public static final StpLogic ADMIN = new StpLogic("admin"); + + /** + * User 会话对象,管理 User 表所有账号的登录、权限认证 + */ + public static final StpLogic USER = new StpLogic("user"); + + /** + * XX 会话对象,(项目中有多少套账号表,就声明几个 StpLogic 会话对象) + */ + public static final StpLogic XXX = new StpLogic("xx"); + +} +``` + +在需要登录、权限认证的地方: +``` java +// 在当前会话进行 Admin 账号登录 +StpKit.ADMIN.login(10001); + +// 在当前会话进行 User 账号登录 +StpKit.USER.login(10001); + +// 检测当前会话是否以 Admin 账号登录,并具有 article:add 权限 +StpKit.ADMIN.checkPermission("article:add"); + +// 检测当前会话是否以 User 账号登录,并通过了二级认证 +StpKit.USER.checkSafe(); + +// 获取当前 User 会话的 Session 对象,并进行写值操作 +StpKit.USER.getSession().set("name", "zhang"); +``` + + +### 6、在多账户模式下使用注解鉴权 +框架默认的注解鉴权 如`@SaCheckLogin` 只针对原生`StpUtil`进行鉴权。 + +例如,我们在一个方法上加上`@SaCheckLogin`注解,这个注解只会放行通过`StpUtil.login(id)`进行登录的会话, +而对于通过`StpUserUtil.login(id)`进行登录的会话,则始终不会通过校验。 + +那么如何告诉`@SaCheckLogin`要鉴别的是哪套账号的登录会话呢?很简单,你只需要指定一下注解的type属性即可: + +``` java +// 通过type属性指定此注解校验的是我们自定义的`StpUserUtil`,而不是原生`StpUtil` +@SaCheckLogin(type = StpUserUtil.TYPE) +@RequestMapping("info") +public String info() { + return "查询用户信息"; +} +``` + +注:`@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")`同理,亦可根据type属性指定其校验的账号体系,此属性默认为`""`,代表使用原生`StpUtil`账号体系。 + + +### 7、使用注解合并简化代码 +交流群里有同学反应,虽然可以根据 `@SaCheckLogin(type = "user")` 指定账号类型,但几十上百个注解都加上这个的话,还是有些繁琐,代码也不够优雅,有么有更简单的解决方案? + +我们期待一种`[注解继承/合并]`的能力,即:自定义一个注解,标注上`@SaCheckLogin(type = "user")`, +然后在方法上标注这个自定义注解,效果等同于标注`@SaCheckLogin(type = "user")`。 + +很遗憾,JDK默认的注解处理器并没有提供这种`[注解继承/合并]`的能力,不过好在我们可以利用 Spring 的注解处理器,达到同样的目的。 + +1. 重写Sa-Token默认的注解处理器: + +``` java +@Configuration +public class SaTokenConfigure { + @PostConstruct + public void rewriteSaStrategy() { + // 重写Sa-Token的注解处理器,增加注解合并功能 + SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> { + return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); + }; + } +} +``` + +2. 自定义一个注解: + +``` java +/** + * 登录认证(User版):只有登录之后才能进入该方法 + *

    可标注在函数、类上(效果等同于标注在此类的所有方法上) + */ +@SaCheckLogin(type = "user") +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE}) +public @interface SaUserCheckLogin { + +} +``` + +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/merge_annotation)。 + +> [!TIP| label:自定义注解方案] +> 除了注解合并方案,这里还有一份自定义注解方案,参考:[自定义注解](/fun/custom-annotations) + + + +### 8、同端多登陆 +假设我们不仅需要在后台同时集成两套账号,我们还需要在一个客户端同时登陆两套账号(业务场景举例:一个APP中可以同时登陆商家账号和用户账号)。 + +如果我们不做任何特殊处理的话,在客户端会发生`token覆盖`,新登录的 token 会覆盖掉旧登录的 token 从而导致旧登录失效。 + +具体表现大致为:在一个浏览器登录商家账号后,再登录用户账号,然后商家账号的登录态就会自动失效。 + +那么如何解决这个问题?很简单,我们只要更改一下 `StpUserUtil` 的 `TokenName` 即可,参考示例如下: + +``` java +public class StpUserUtil { + + // 使用匿名子类 重写`stpLogic对象`的一些方法 + public static StpLogic stpLogic = new StpLogic("user") { + // 重写 StpLogic 类下的 `splicingKeyTokenName` 函数,返回一个与 `StpUtil` 不同的token名称, 防止冲突 + @Override + public String splicingKeyTokenName() { + return super.splicingKeyTokenName() + "-user"; + } + // 同理你可以按需重写一些其它方法 ... + }; + + // ... + +} +``` + +再次调用 `StpUserUtil.login(10001)` 进行登录授权时,token的名称将不再是 `satoken`,而是我们重写后的 `satoken-user`,这样就不会再客户端发生 token 的相互覆盖了。 + + +### 9、不同体系不同 SaTokenConfig 配置 +如果自定义的 StpUserUtil 需要使用不同 SaTokenConfig 对象, 也很简单,参考示例如下: + +``` java +@Configuration +public class SaTokenConfigure { + + @PostConstruct + public void setSaTokenConfig() { + // 设定 StpUtil 使用的 SaTokenConfig 配置参数对象 + SaTokenConfig config1 = new SaTokenConfig(); + config1.setTokenName("satoken1"); + config1.setTimeout(1000); + config1.setTokenStyle("random-64"); + // 更多设置 ... + StpUtil.stpLogic.setConfig(config1); + + // 设定 StpUserUtil 使用的 SaTokenConfig 配置参数对象 + SaTokenConfig config2 = new SaTokenConfig(); + config2.setTokenName("satoken2"); + config2.setTimeout(2000); + config2.setTokenStyle("tik"); + // 更多设置 ... + StpUserUtil.stpLogic.setConfig(config2); + } + +} + +``` + + +### 10、多账号体系混合鉴权 +QQ群中经常有小伙伴提问:在多账号体系下,怎么在 SaInterceptor 拦截器中给一个接口登录鉴权? + +其实这个问题,主要是靠你的业务需求来决定,以后台 Admin 账号和前台 User 账号为例: + +``` java +// 注册 Sa-Token 拦截器 +@Override +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + + // 如果这个接口,要求客户端登录了后台 Admin 账号才能访问: + SaRouter.match("/art/getInfo").check(r -> StpUtil.checkLogin()); + + // 如果这个接口,要求客户端登录了前台 User 账号才能访问: + SaRouter.match("/art/getInfo").check(r -> StpUserUtil.checkLogin()); + + // 如果这个接口,要求客户端同时登录 Admin 和 User 账号,才能访问: + SaRouter.match("/art/getInfo").check(r -> { + StpUtil.checkLogin(); + StpUserUtil.checkLogin(); + }); + + // 如果这个接口,要求客户端登录 Admin 和 User 账号任意一个,就能访问: + SaRouter.match("/art/getInfo").check(r -> { + if(StpUtil.isLogin() == false && StpUserUtil.isLogin() == false) { + throw new SaTokenException("请登录后再访问接口"); + } + }); + + })).addPathPatterns("/**"); +} +``` + + +### 11、在一个接口里获取是哪个体系的账号正在登录 + +可以分别用两个体系的 isLogin() 方法去判断,哪个返回 true 就代表正在登录哪个体系 + +``` java +@RequestMapping("test") +public SaResult test2() { + + String loginType = ""; + + if(StpUtil.isLogin()) { + loginType = StpUtil.getLoginType(); + } + if(StpUserUtil.isLogin()) { + loginType = StpUserUtil.getLoginType(); + } + + System.out.println("当前登录的 loginType:" + loginType); + + return SaResult.ok(); +} +``` + +请注意此处可能出现的两种边际情况: +- 两个 if 均返回 false:代表客户端在两个账号体系都没有登录。 +- 两个 if 均返回 true:代表客户端在两个账号体系都登录了。 + + +### 12、注意点:运行时不可更改 LoginType +在 Q群 解决问题时,发现有些同学会写出类似下列形式的代码: + +``` java +StpUtil.login(10001); +StpUtil.getStpLogic().setLoginType("user"); +StpUtil.getSession().set("name", "zhangsan"); +``` + +这是一种错误写法:LoginType 不可在运行时更改,只能在项目启动时指定。一旦项目启动成功后再修改 LoginType ,就会造成线程安全问题和严重的逻辑问题。 + + + + + + + +--- + + + 本章代码示例:Sa-Token 多账号体系认证 —— [ StpUserUtil.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/mock-person.md b/sa-token-doc/up/mock-person.md new file mode 100644 index 000000000..b7660cf16 --- /dev/null +++ b/sa-token-doc/up/mock-person.md @@ -0,0 +1,66 @@ +# 模拟他人 +--- + + +以上介绍的 API 都是操作当前账号,对当前账号进行各种鉴权操作,你可能会问,我能不能对别的账号进行一些操作?
    +比如:查看账号 10001 有无某个权限码、获取 账号 id=10002 的 `Account-Session`,等等... + +Sa-Token 在 API 设计时充分考虑了这一点,暴露出多个api进行此类操作: + + +## 有关操作其它账号的api + +``` java +// 获取指定账号10001的`tokenValue`值 +StpUtil.getTokenValueByLoginId(10001); + +// 将账号10001的会话注销登录 +StpUtil.logout(10001); + +// 获取账号10001的Session对象, 如果session尚未创建, 则新建并返回 +StpUtil.getSessionByLoginId(10001); + +// 获取账号10001的Session对象, 如果session尚未创建, 则返回null +StpUtil.getSessionByLoginId(10001, false); + +// 获取账号10001是否含有指定角色标识 +StpUtil.hasRole(10001, "super-admin"); + +// 获取账号10001是否含有指定权限码 +StpUtil.hasPermission(10001, "user:add"); +``` + + + +## 临时身份切换 + +有时候,我们需要直接将当前会话的身份切换为其它账号,比如: +``` java +// 将当前会话[身份临时切换]为其它账号(本次请求内有效) +StpUtil.switchTo(10044); + +// 此时再调用此方法会返回 10044 (我们临时切换到的账号id) +StpUtil.getLoginId(); + +// 结束 [身份临时切换] +StpUtil.endSwitch(); +``` + +你还可以:直接在一个代码段里方法内,临时切换身份为指定loginId(此方式无需手动调用`StpUtil.endSwitch()`关闭身份切换) +``` java +System.out.println("------- [身份临时切换]调用开始..."); +StpUtil.switchTo(10044, () -> { + System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); // 输出 true + System.out.println("获取当前登录账号id: " + StpUtil.getLoginId()); // 输出 10044 +}); +System.out.println("------- [身份临时切换]调用结束..."); +``` + + + +--- + + + 本章代码示例:Sa-Token 身份切换 —— [ SwitchToController.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/mutex-login.md b/sa-token-doc/up/mutex-login.md new file mode 100644 index 000000000..a7d55e554 --- /dev/null +++ b/sa-token-doc/up/mutex-login.md @@ -0,0 +1,52 @@ +# 同端互斥登录 + +如果你经常使用腾讯QQ,就会发现它的登录有如下特点:它可以手机电脑同时在线,但是不能在两个手机上同时登录一个账号。
    +同端互斥登录,指的就是:像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线。 + + + + +--- + +## 具体API + +在 Sa-Token 中如何做到同端互斥登录?
    +首先在配置文件中,将 `isConcurrent` 配置为false,然后调用登录等相关接口时声明设备类型即可: + + +#### 指定设备类型登录 +``` java +// 指定`账号id`和`设备类型`进行登录 +StpUtil.login(10001, "PC"); +``` +调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-4` + + +#### 指定设备类型强制注销 +``` java +// 指定`账号id`和`设备类型`进行强制注销 +StpUtil.logout(10001, "PC"); +``` +如果第二个参数填写null或不填,代表将这个账号id所有在线端强制注销,被踢出者再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-2` + + +#### 查询当前登录的设备类型 +``` java +// 返回当前token的登录设备类型 +StpUtil.getLoginDevice(); +``` + + +#### Id 反查 Token +``` java +// 获取指定loginId指定设备类型端的tokenValue +StpUtil.getTokenValueByLoginId(10001, "APP"); +``` + + +--- + + + 本章代码示例:Sa-Token 同端互斥登录 —— [ MutexLoginController.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/not-cookie.md b/sa-token-doc/up/not-cookie.md new file mode 100644 index 000000000..861d836b5 --- /dev/null +++ b/sa-token-doc/up/not-cookie.md @@ -0,0 +1,110 @@ +# 前后端分离(无Cookie模式) +--- + +### 何为无 Cookie 模式? + +无 Cookie 模式:特指不支持 Cookie 功能的终端,通俗来讲就是我们常说的 —— **前后端分离模式**。 + +常规 Web 端鉴权方法,一般由 `Cookie模式` 完成,而 Cookie 有两个特性: +1. 可由后端控制写入。 +2. 每次请求自动提交。 + +这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)
    +而在app、小程序等前后端分离场景中,一般是没有 Cookie 这一功能的,此时大多数人都会一脸懵逼,咋进行鉴权啊? + +见招拆招,其实答案很简单: +- 不能后端控制写入了,就前端自己写入。(难点在**后端如何将 Token 传递到前端**) +- 每次请求不能自动提交了,那就手动提交。(难点在**前端如何将 Token 传递到后端**,同时**后端将其读取出来**) + + + +### 1、后端将 token 返回到前端 + +1. 首先调用 `StpUtil.login(id)` 进行登录。 +2. 调用 `StpUtil.getTokenInfo()` 返回当前会话的 token 详细参数。 + - 此方法返回一个对象,其有两个关键属性:`tokenName`和`tokenValue`(token 的名称和 token 的值)。 + - 将此对象传递到前台,让前端人员将这两个值保存到本地。 + +代码示例: +``` java +// 登录接口 +@RequestMapping("doLogin") +public SaResult doLogin() { + // 第1步,先登录上 + StpUtil.login(10001); + // 第2步,获取 Token 相关参数 + SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); + // 第3步,返回给前端 + return SaResult.data(tokenInfo); +} +``` + + +### 2、前端将 token 提交到后端 +1. 无论是app还是小程序,其传递方式都大同小异。 +2. 那就是,将 token 塞到请求`header`里 ,格式为:`{tokenName: tokenValue}`。 +3. 以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例: + +**方式1,简单粗暴** + +``` js +// 1、首先在登录时,将 tokenValue 存储在本地,例如: +uni.setStorageSync('tokenValue', tokenValue); + +// 2、在发起ajax请求的地方,获取这个值,并塞到header里 +uni.request({ + url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。 + header: { + "content-type": "application/x-www-form-urlencoded", + "satoken": uni.getStorageSync('tokenValue') // ⚠️ 关键代码, 注意参数名字是 satoken + }, + success: (res) => { + console.log(res.data); + } +}); +``` + +**方式2,更加灵活** + +``` js +// 1、首先在登录时,将tokenName和tokenValue一起存储在本地,例如: +uni.setStorageSync('tokenName', tokenName); +uni.setStorageSync('tokenValue', tokenValue); + +// 2、在发起ajax的地方,获取这两个值, 并组织到head里 +var tokenName = uni.getStorageSync('tokenName'); // 从本地缓存读取tokenName值 +var tokenValue = uni.getStorageSync('tokenValue'); // 从本地缓存读取tokenValue值 +var header = { + "content-type": "application/x-www-form-urlencoded" +}; +if (tokenName != undefined && tokenName != '') { + header[tokenName] = tokenValue; +} + +// 3、后续在发起请求时将 header 对象塞到请求头部 +uni.request({ + url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。 + header: header, + success: (res) => { + console.log(res.data); + } +}); +``` + +4. 只要按照如此方法将`token`值传递到后端,Sa-Token 就能像传统PC端一样自动读取到 token 值,进行鉴权。 +5. 你可能会有疑问,难道我每个`ajax`都要写这么一坨?岂不是麻烦死了? + - 你当然不能每个 ajax 都写这么一坨,因为这种重复性代码都是要封装在一个函数里统一调用的。 + + +### 其它解决方案? +如果你对 Cookie 非常了解,那你就会明白,所谓 Cookie ,本质上就是一个特殊的`header`参数而已, +而既然它只是一个 header 参数,我们就能手动模拟实现它,从而完成鉴权操作。 + +这其实是对`无Cookie模式`的另一种解决方案,有兴趣的同学可以百度了解一下,在此暂不赘述。 + +--- + + + 本章代码示例:Sa-Token 前后端分离样例 —— [ NotCookieController.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/password-secure.md b/sa-token-doc/up/password-secure.md new file mode 100644 index 000000000..8de2e6e13 --- /dev/null +++ b/sa-token-doc/up/password-secure.md @@ -0,0 +1,162 @@ +# 密码加密 + +严格来讲,密码加密不属于 [权限认证] 的范畴,但是对于大多数系统来讲,密码加密又是安全认证不可或缺的部分, +所以,应大家要求,`Sa-Token`在 v1.14 版本添加密码加密模块,该模块非常简单,仅仅封装了一些常见的加密算法。 + + + +### 摘要加密 +md5、sha1、sha256 +``` java +// md5加密 +SaSecureUtil.md5("123456"); + +// sha1加密 +SaSecureUtil.sha1("123456"); + +// sha256加密 +SaSecureUtil.sha256("123456"); +``` + + +### 对称加密 +AES加密 +``` java +// 定义秘钥和明文 +String key = "123456"; +String text = "Sa-Token 一个轻量级java权限认证框架"; + +// 加密 +String ciphertext = SaSecureUtil.aesEncrypt(key, text); +System.out.println("AES加密后:" + ciphertext); + +// 解密 +String text2 = SaSecureUtil.aesDecrypt(key, ciphertext); +System.out.println("AES解密后:" + text2); +``` + +附:内部密钥生成策略,方便其他开发语言对接 +```java + private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException { + KeyGenerator kg = KeyGenerator.getInstance("AES"); + //获取SHA1PRNG伪随机数生成器 + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + //将实际密码作为伪随机数生成器的种子 + random.setSeed(password.getBytes()); + //利用伪随机数生成器生成128位的密钥,能确保解密时生成的密钥的一致性 + kg.init(128, random); + SecretKey secretKey = kg.generateKey(); + return new SecretKeySpec(secretKey.getEncoded(), "AES"); + } +``` + + +### 非对称加密 +~~RSA加密(已过时)~~ +``` java +// 定义私钥和公钥 +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); +``` + +你可能会有疑问,私钥和公钥这么长的一大串,我怎么弄出来,手写吗?当然不是,调用以下方法生成即可 +``` java +// 生成一对公钥和私钥,其中Map对象 (private=私钥, public=公钥) +System.out.println(SaSecureUtil.rsaGenerateKeyPair()); +``` + + +### Base64编码与解码 +``` java +// 文本 +String text = "Sa-Token 一个轻量级java权限认证框架"; + +// 使用Base64编码 +String base64Text = SaBase64Util.encode(text); +System.out.println("Base64编码后:" + base64Text); + +// 使用Base64解码 +String text2 = SaBase64Util.decode(base64Text); +System.out.println("Base64解码后:" + text2); +``` + + +### Base32编码与解码 +``` java +// 文本 +String text = "Sa-Token 一个轻量级java权限认证框架"; + +// 使用Base32编码 +String base32Text = SaBase32Util.encode(text); +System.out.println("Base32编码后:" + base32Text); + +// 使用Base32解码 +String text2 = SaBase32Util.decode(base32Text); +System.out.println("Base32解码后:" + text2); +``` + + +### TOTP 验证器 + +``` java +// 1、生成密钥 +String secretKey = SaTotpUtil.generateSecretKey(); +System.out.println("TOTP 秘钥: " + secretKey); + +// 2、生成扫码字符串 +String qeString = SaTotpUtil.generateGoogleSecretKey("zhangsan", secretKey); +System.out.println("扫码字符串: " + qeString); + +// 3、计算当前 TOTP 码 +String code = SaTotpUtil.generateTOTP(secretKey); +System.out.println("当前时间戳对应的 TOTP 码: " + code); + +// 4、验证用户输入 +boolean isValid = SaTotpUtil.validateTOTP(secretKey, code, 1); +System.out.println("验证结果: " + isValid); +``` + +在线 TOTP 管理器推荐: [TOTP 密码生成管理 - 工具哇](https://toolwa.com/totp/) + + +### BCrypt加密 +由它加密的文件可在所有支持的操作系统和处理器上进行转移 + +它的口令必须是8至56个字符,并将在内部被转化为448位的密钥 + +> 此类来自于https://github.com/jeremyh/jBCrypt/ +``` java +// 使用方法 +String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); + +// 使用checkpw方法检查被加密的字符串是否与原始字符串匹配: +BCrypt.checkpw(candidate_password, stored_hash); + +// gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少,也决定了加密的复杂度: +String strong_salt = BCrypt.gensalt(10); +String stronger_salt = BCrypt.gensalt(12); +``` + + +
    + +如需更多加密算法,可参考 [Hutool-crypto: 加密](https://hutool.cn/docs/#/crypto/%E6%A6%82%E8%BF%B0) + + +--- + + + 本章代码示例:Sa-Token 密码加密 —— [ SecureController.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/remember-me.md b/sa-token-doc/up/remember-me.md new file mode 100644 index 000000000..9b14283d1 --- /dev/null +++ b/sa-token-doc/up/remember-me.md @@ -0,0 +1,91 @@ +# [记住我] 模式 +--- + +如图所示,一般网站的登录界面都会有一个 **`[记住我]`** 按钮,当你勾选它登录后,即使你关闭浏览器再次打开网站,也依然会处于登录状态,无须重复验证密码: + +../static/login-view.png + +那么在Sa-Token中,如何做到 [ 记住我 ] 功能呢? + + +### 在 Sa-Token 中实现记住我功能 + +Sa-Token的登录授权,**默认就是`[记住我]`模式**,为了实现`[非记住我]`模式,你需要在登录时如下设置: + +``` java +// 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录 +StpUtil.login(10001, false); +``` + +那么,Sa-Token实现`[记住我]`的具体原理是? + + +### 实现原理 +Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是: +- 临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失。 +- 持久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失。 + +利用Cookie的此特性,我们便可以轻松实现 [记住我] 模式: +- 勾选 [记住我] 按钮时:调用`StpUtil.login(10001, true)`,在浏览器写入一个`持久Cookie`储存 Token,此时用户即使重启浏览器 Token 依然有效。 +- 不勾选 [记住我] 按钮时:调用`StpUtil.login(10001, false)`,在浏览器写入一个`临时Cookie`储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。 + + + + + +### 前后端分离模式下如何实现[记住我]? + +此时机智的你😏很快发现一个问题,Cookie虽好,却无法在前后端分离环境下使用,那是不是代表上述方案在APP、小程序等环境中无效? + +准确的讲,答案是肯定的,任何基于Cookie的认证方案在前后端分离环境下都会失效(原因在于这些客户端默认没有实现Cookie功能),不过好在,这些客户端一般都提供了替代方案, +唯一遗憾的是,此场景中token的生命周期需要我们在前端手动控制: + +以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例,我们可以使用如下方式达到同样的效果: +``` js +// 使用本地存储保存token,达到 [持久Cookie] 的效果 +uni.setStorageSync("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); + +// 使用globalData保存token,达到 [临时Cookie] 的效果 +getApp().globalData.satoken = "xxxx-xxxx-xxxx-xxxx-xxx"; +``` + +如果你决定在PC浏览器环境下进行前后端分离模式开发,那么更加简单: +``` js +// 使用 localStorage 保存token,达到 [持久Cookie] 的效果 +localStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); + +// 使用 sessionStorage 保存token,达到 [临时Cookie] 的效果 +sessionStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); +``` + +Remember me, it's too easy! + + + +### 登录时指定 Token 有效期 +登录时不仅可以指定是否为`[记住我]`模式,还可以指定一个特定的时间作为 Token 有效时长,如下示例: +``` java +// 示例1: +// 指定token有效期(单位: 秒),如下所示token七天有效 +StpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7)); + +// ----------------------- 示例2:所有参数 +// `SaLoginParameter`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如: +StpUtil.login(10001, new SaLoginParameter() + .setDevice("PC") // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型 + .setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) + .setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值) + .setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token + .setIsWriteHeader(false) // 是否在登录后将 Token 写入到响应头 + ); +``` + + + + +--- + + + 本章代码示例:Sa-Token 记住我登录 —— [ RememberMeController.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/safe-auth.md b/sa-token-doc/up/safe-auth.md new file mode 100644 index 000000000..0f678f9e7 --- /dev/null +++ b/sa-token-doc/up/safe-auth.md @@ -0,0 +1,144 @@ +# 二级认证 + +在某些敏感操作下,我们需要对已登录的会话进行二次验证。 + +比如代码托管平台的仓库删除操作,尽管我们已经登录了账号,当我们点击 **[删除]** 按钮时,还是需要再次输入一遍密码,这么做主要为了两点: + +1. 保证操作者是当前账号本人。 +2. 增加操作步骤,防止误删除重要数据。 + +这就是我们本篇要讲的 —— 二级认证,即:在已登录会话的基础上,进行再次验证,提高会话的安全性。 + + +--- + +### 具体API + +在`Sa-Token`中进行二级认证非常简单,只需要使用以下API: + +``` java +// 在当前会话 开启二级认证,时间为120秒 +StpUtil.openSafe(120); + +// 获取:当前会话是否处于二级认证时间内 +StpUtil.isSafe(); + +// 检查当前会话是否已通过二级认证,如未通过则抛出异常 +StpUtil.checkSafe(); + +// 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) +StpUtil.getSafeTime(); + +// 在当前会话 结束二级认证 +StpUtil.closeSafe(); +``` + + +### 一个小示例 + +一个完整的二级认证业务流程,应该大致如下: +``` java +// 删除仓库 +@RequestMapping("deleteProject") +public SaResult deleteProject(String projectId) { + // 第1步,先检查当前会话是否已完成二级认证 + if(!StpUtil.isSafe()) { + return SaResult.error("仓库删除失败,请完成二级认证后再次访问接口"); + } + + // 第2步,如果已完成二级认证,则开始执行业务逻辑 + // ... + + // 第3步,返回结果 + return SaResult.ok("仓库删除成功"); +} + +// 提供密码进行二级认证 +@RequestMapping("openSafe") +public SaResult openSafe(String password) { + // 比对密码(此处只是举例,真实项目时可拿其它参数进行校验) + if("123456".equals(password)) { + + // 比对成功,为当前会话打开二级认证,有效期为120秒 + StpUtil.openSafe(120); + return SaResult.ok("二级认证成功"); + } + + // 如果密码校验失败,则二级认证也会失败 + return SaResult.error("二级认证失败"); +} +``` + +> [!NOTE| label:调用步骤:] +> 1. 前端调用 `deleteProject` 接口,尝试删除仓库。 +> 2. 后端校验会话尚未完成二级认证,返回: `仓库删除失败,请完成二级认证后再次访问接口`。 +> 3. 前端将信息提示给用户,用户输入密码,调用 `openSafe` 接口。 +> 4. 后端比对用户输入的密码,完成二级认证,有效期为:120秒。 +> 5. 前端在 120 秒内再次调用 `deleteProject` 接口,尝试删除仓库。 +> 6. 后端校验会话已完成二级认证,返回:`仓库删除成功`。 + + +### 指定业务标识进行二级认证 + +如果项目有多条业务线都需要敏感操作验证,则 `StpUtil.openSafe()` 无法提供细粒度的认证操作, +此时我们可以指定一个业务标识来分辨不同的业务线: + +``` java +// 在当前会话 开启二级认证,业务标识为client,时间为600秒 +StpUtil.openSafe("client", 600); + +// 获取:当前会话是否已完成指定业务的二级认证 +StpUtil.isSafe("client"); + +// 校验:当前会话是否已完成指定业务的二级认证 ,如未认证则抛出异常 +StpUtil.checkSafe("client"); + +// 获取当前会话指定业务二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) +StpUtil.getSafeTime("client"); + +// 在当前会话 结束指定业务标识的二级认证 +StpUtil.closeSafe("client"); +``` + +业务标识可以填写任意字符串,不同业务标识之间的认证互不影响,比如: +``` java +// 打开了业务标识为 client 的二级认证 +StpUtil.openSafe("client"); + +// 判断是否处于 shop 的二级认证,会返回 false +StpUtil.isSafe("shop"); // 返回 false + +// 也不会通过校验,会抛出异常 +StpUtil.checkSafe("shop"); +``` + + + +### 使用注解进行二级认证 +在一个方法上使用 `@SaCheckSafe` 注解,可以在代码进入此方法之前进行一次二级认证校验 +``` java +// 二级认证:必须二级认证之后才能进入该方法 +@SaCheckSafe +@RequestMapping("add") +public String add() { + return "用户增加"; +} + +// 指定业务类型,进行二级认证校验 +@SaCheckSafe("art") +@RequestMapping("add2") +public String add2() { + return "文章增加"; +} +``` + +详细使用方法可参考:[注解鉴权](/use/at-check),此处不再赘述 + + + +--- + + + 本章代码示例:Sa-Token 二级认证 —— [ SafeAuthController.java ] + \ No newline at end of file diff --git a/sa-token-doc/up/search-session.md b/sa-token-doc/up/search-session.md new file mode 100644 index 000000000..853d87658 --- /dev/null +++ b/sa-token-doc/up/search-session.md @@ -0,0 +1,133 @@ +# 会话查询 + +--- + +### 1、单账号会话查询 + +使用 `StpUtil.getTerminalListByLoginId( loginId )` 可获取指定账号已登录终端列表信息,例如: + +``` java +public static void main(String[] args) { + System.out.println("账号 10001 登录设备信息:"); + List terminalList = StpUtil.getTerminalListByLoginId(10001); + for (SaTerminalInfo ter : terminalList) { + System.out.println("登录index=" + ter.getIndex() + ", 设备type=" + ter.getDeviceType() + ", token=" + ter.getTokenValue() + ", 登录time=" + ter.getCreateTime()); + } +} +``` + +控制台打印结果: + +``` txt +账号 10001 登录设备信息: +登录index=1, 设备type=PC, token=a8fbb46f-e043-459a-a875-0a2874911be8, 登录time=1742354951192 +登录index=2, 设备type=APP, token=882b6c9c-bdf9-4e8f-a42b-6e17d2fe0e34, 登录time=1742354960950 +登录index=3, 设备type=WEB, token=dacac78c-0983-4819-ab8b-07e7603597fc, 登录time=1742354962848 +``` + +一个 `SaTerminalInfo` 对象代表一个终端信息,其有如下字段: + +``` java +terminal.getIndex(); // 登录会话索引值 (该账号第几个登录的设备) +terminal.getDeviceType(); // 所属设备类型,例如:PC、WEB、HD、MOBILE、APP +terminal.getTokenValue(); // 此次登录的token值 +terminal.getCreateTime(); // 登录时间, 13位时间戳 +terminal.getDeviceId(); // 设备id, 设备唯一标识 +terminal.getExtra("key"); // 此次登录的额外自定义参数 +``` + +`Extra` 自定义参数可以在登录时通过如下方式指定: +``` java +StpUtil.login(10001, new SaLoginParameter().setTerminalExtra("key", "value")); +``` + + + +### 2、全部会话检索 + +``` java +// 查询所有已登录的 Token +StpUtil.searchTokenValue(String keyword, int start, int size, boolean sortType); + +// 查询所有 Account-Session 会话 +StpUtil.searchSessionId(String keyword, int start, int size, boolean sortType); + +// 查询所有 Token-Session 会话 +StpUtil.searchTokenSessionId(String keyword, int start, int size, boolean sortType); +``` + + +#### 参数详解: +- `keyword`: 查询关键字,只有包括这个字符串的 token 值才会被查询出来。 +- `start`: 数据开始处索引。 +- `size`: 要获取的数据条数 (值为-1代表一直获取到末尾)。 +- `sortType`: 排序方式(true=正序:先登录的在前,false=反序:后登录的在前)。 + +简单样例: +``` java +// 查询 value 包括 1000 的所有 token,结果集从第 0 条开始,返回 10 条 +List tokenList = StpUtil.searchTokenValue("1000", 0, 10, true); +for (String token : tokenList) { + System.out.println(token); +} +``` + +#### 深入:`StpUtil.searchTokenValue` 和 `StpUtil.searchSessionId` 的区别? + +- StpUtil.searchTokenValue 查询的是登录产生的所有 Token。 +- StpUtil.searchSessionId 查询的是所有已登录账号会话id。 + +举个例子,项目配置如下: +``` yml +sa-token: + # 允许同一账号在多个设备一起登录 + is-concurrent: true + # 同一账号每次登录产生不同的token + is-share: false +``` + +假设此时账号A在 电脑、手机、平板 依次登录(共3次登录),账号B在 电脑、手机 依次登录(共2次登录),那么: + +- `StpUtil.searchTokenValue` 将返回一共 5 个Token。 +- `StpUtil.searchSessionId` 将返回一共 2 个 SessionId。 + +综上,若要遍历系统所有已登录的会话,代码将大致如下: +``` java +// 获取所有已登录的会话id +List sessionIdList = StpUtil.searchSessionId("", 0, -1, false); + +for (String sessionId : sessionIdList) { + + // 根据会话id,查询对应的 SaSession 对象,此处一个 SaSession 对象即代表一个登录的账号 + SaSession session = StpUtil.getSessionBySessionId(sessionId); + + // 查询这个账号都在哪些设备登录了,依据上面的示例,账号A 的 SaTerminalInfo 数量是 3,账号B 的 SaTerminalInfo 数量是 2 + List terminalList = session.terminalListCopy(); + System.out.println("会话id:" + sessionId + ",共在 " + terminalList.size() + " 设备登录"); +} +``` + + + +
    + +#### 注意事项: +由于会话查询底层采用了遍历方式获取数据,当数据量过大时此操作将会比较耗时,有多耗时呢?这里提供一份参考数据: +- 单机模式下:百万会话取出10条 Token 平均耗时 `0.255s`。 +- Redis模式下:百万会话取出10条 Token 平均耗时 `3.322s`。 + +请根据业务实际水平合理调用API。 + + +> [!WARNING| label:注意] +> 基于活跃 Token 的统计方式会比实际情况略有延迟,如果需要精确统计实时在线用户信息需要采用 WebSocket。 + + +--- + + + 本章代码示例:Sa-Token 会话查询 —— [ SearchSessionController.java ] + + + diff --git a/sa-token-doc/up/token-prefix.md b/sa-token-doc/up/token-prefix.md new file mode 100644 index 000000000..0d824f8d9 --- /dev/null +++ b/sa-token-doc/up/token-prefix.md @@ -0,0 +1,57 @@ +# Token 提交前缀 + +### 需求场景 + +在某些系统中,前端提交token时会在前面加个固定的前缀,例如: + +``` js +{ + "satoken": "Bearer xxxx-xxxx-xxxx-xxxx" +} +``` + +此时后端如果不做任何特殊处理,框架将会把`Bearer `视为token的一部分,无法正常读取token信息,导致鉴权失败。 + +为此,我们需要在yml中添加如下配置: + + + +``` yaml +sa-token: + # 指定 token 提交时的前缀 + token-prefix: Bearer +``` + +``` properties +# token前缀 +sa-token.token-prefix=Bearer +``` + + + +此时 Sa-Token 便可在读取 Token 时裁剪掉 `Bearer`,成功获取`xxxx-xxxx-xxxx-xxxx`。 + +注:**Token前缀 与 Token值 之间必须有一个空格** + + +### Cookie 模式自动填充前缀 + +由于`Cookie`中无法存储空格字符,所以配置 Token 前缀后,Cookie 模式将会失效,无法成功提交带有前缀的 token。 + +如果需要在这种场景下仍然使用 Cookie 模式验证 token,可以使用 `cookieAutoFillPrefix` 配置项打开 Cookie 模式自动填充前缀: + + + +``` yaml +sa-token: + # 指定 Cookie 模式下自动填充 token 提交前缀 + cookie-auto-fill-prefix: true +``` + +``` properties +# 指定 Cookie 模式下自动填充 token 提交前缀 +sa-token.cookie-auto-fill-prefix=true +``` + + + diff --git a/sa-token-doc/up/token-style.md b/sa-token-doc/up/token-style.md new file mode 100644 index 000000000..6593756a7 --- /dev/null +++ b/sa-token-doc/up/token-style.md @@ -0,0 +1,68 @@ +# 自定义 Token 风格 + +本篇介绍token生成的各种风格,以及自定义token生成策略。 + +--- + + +## 内置风格 + +Sa-Token 默认的 token 生成策略是 uuid 风格,其模样类似于:`623368f0-ae5e-4475-a53f-93e4225f16ae`。
    +如果你对这种风格不太感冒,还可以将 token 生成设置为其他风格。 + +怎么设置呢?只需要在yml配置文件里设置 `sa-token.token-style=风格类型` 即可,其有多种取值: + +``` java +// 1. token-style=uuid —— uuid风格 (默认风格) +"623368f0-ae5e-4475-a53f-93e4225f16ae" + +// 2. token-style=simple-uuid —— 同上,uuid风格, 只不过去掉了中划线 +"6fd4221395024b5f87edd34bc3258ee8" + +// 3. token-style=random-32 —— 随机32位字符串 +"qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W" + +// 4. token-style=random-64 —— 随机64位字符串 +"v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc" + +// 5. token-style=random-128 —— 随机128位字符串 +"nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj" + +// 6. token-style=tik —— tik风格 +"gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__" +``` + + +## 自定义 Token 生成策略 + +如果你觉着以上风格都不是你喜欢的类型,那么你还可以**自定义token生成策略**,来定制化token生成风格。
    + +怎么做呢?只需要重写 `SaStrategy` 策略类的 `createToken` 算法即可: + + +#### 参考步骤如下: +1、在`SaTokenConfigure`配置类中添加代码: +``` java +@Configuration +public class SaTokenConfigure { + /** + * 重写 Sa-Token 框架内部算法策略 + */ + @PostConstruct + public void rewriteSaStrategy() { + // 重写 Token 生成策略 + SaStrategy.instance.createToken = (loginId, loginType) -> { + return SaFoxUtil.getRandomString(60); // 随机60位长度字符串 + }; + } +} +``` + +2、再次调用 `StpUtil.login(10001)`方法进行登录,观察其生成的token样式: +``` java +gfuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH +``` + +> [!WARNING| label:更改了 token 生成策略但是不生效?] +> 把 Redis 中的旧数据清除掉再试试 + diff --git a/sa-token-doc/use/at-check.md b/sa-token-doc/use/at-check.md new file mode 100644 index 000000000..2bf9f05b7 --- /dev/null +++ b/sa-token-doc/use/at-check.md @@ -0,0 +1,254 @@ +# 注解鉴权 + + +### 注解鉴权 + +有同学表示:尽管使用代码鉴权非常方便,但是我仍希望把鉴权逻辑和业务逻辑分离开来,我可以使用注解鉴权吗?当然可以!
    + +注解鉴权 —— 优雅的将鉴权与业务代码分离! + +- `@SaCheckLogin`: 登录校验 —— 只有登录之后才能进入该方法。 +- `@SaCheckRole("admin")`: 角色校验 —— 必须具有指定角色标识才能进入该方法。 +- `@SaCheckPermission("user:add")`: 权限校验 —— 必须具有指定权限才能进入该方法。 +- `@SaCheckSafe`: 二级认证校验 —— 必须二级认证之后才能进入该方法。 +- `@SaCheckHttpBasic`: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。 +- `@SaCheckHttpDigest`: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。 +- `@SaCheckDisable("comment")`:账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。 +- `@SaCheckSign`:API 签名校验 —— 用于跨系统的 API 签名参数校验。 +- `@SaIgnore`:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。 + +Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态。 +因此,为了使用注解鉴权,**你必须手动将 Sa-Token 的全局拦截器注册到你项目中**。 + + +### 1、注册拦截器 +以 SpringBoot2 项目为例,新建配置类`SaTokenConfigure.java` + +``` java +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + // 注册 Sa-Token 拦截器,打开注解式鉴权功能 + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册 Sa-Token 拦截器,打开注解式鉴权功能 + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); + } +} +``` +保证此类被`springboot`启动类扫描到即可 + + + + +### 2、使用注解鉴权 +然后我们就可以愉快的使用注解鉴权了: + +``` java +// 登录校验:只有登录之后才能进入该方法 +@SaCheckLogin +@RequestMapping("info") +public String info() { + return "查询用户信息"; +} + +// 角色校验:必须具有指定角色才能进入该方法 +@SaCheckRole("super-admin") +@RequestMapping("add") +public String add() { + return "用户增加"; +} + +// 权限校验:必须具有指定权限才能进入该方法 +@SaCheckPermission("user-add") +@RequestMapping("add") +public String add() { + return "用户增加"; +} + +// 二级认证校验:必须二级认证之后才能进入该方法 +@SaCheckSafe() +@RequestMapping("add") +public String add() { + return "用户增加"; +} + +// Http Basic 校验:只有通过 Http Basic 认证后才能进入该方法 +@SaCheckHttpBasic(account = "sa:123456") +@RequestMapping("add") +public String add() { + return "用户增加"; +} + +// Http Digest 校验:只有通过 Http Digest 认证后才能进入该方法 +@SaCheckHttpDigest(value = "sa:123456") +@RequestMapping("add") +public String add() { + return "用户增加"; +} + +// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 +@SaCheckDisable("comment") +@RequestMapping("send") +public String send() { + return "查询用户信息"; +} +``` + +注:以上注解都可以加在类上,代表为这个类所有方法进行鉴权 + + +### 3、设定校验模式 +`@SaCheckRole`与`@SaCheckPermission`注解可设置校验模式,例如: +``` java +// 注解式鉴权:只要具有其中一个权限即可通过校验 +@RequestMapping("atJurOr") +@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) +public SaResult atJurOr() { + return SaResult.data("用户信息"); +} +``` + +mode有两种取值: +- `SaMode.AND`,标注一组权限,会话必须全部具有才可通过校验。 +- `SaMode.OR`,标注一组权限,会话只要具有其一即可通过校验。 + + +### 4、角色权限双重 “or校验” +假设有以下业务场景:一个接口在具有权限 `user.add` 或角色 `admin` 时可以调通。怎么写? + +``` java +// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验 +@RequestMapping("userAdd") +@SaCheckPermission(value = "user.add", orRole = "admin") +public SaResult userAdd() { + return SaResult.data("用户信息"); +} +``` + +orRole 字段代表权限校验未通过时的次要选择,两者只要其一校验成功即可进入请求方法,其有三种写法: +- 写法一:`orRole = "admin"`,代表需要拥有角色 admin 。 +- 写法二:`orRole = {"admin", "manager", "staff"}`,代表具有三个角色其一即可。 +- 写法三:`orRole = {"admin, manager, staff"}`,代表必须同时具有三个角色。 + + +### 5、忽略认证 + +使用 `@SaIgnore` 可表示一个接口忽略认证: + +``` java +@SaCheckLogin +@RestController +public class TestController { + + // ... 其它方法 + + // 此接口加上了 @SaIgnore 可以游客访问 + @SaIgnore + @RequestMapping("getList") + public SaResult getList() { + // ... + return SaResult.ok(); + } +} +``` + +如上代码表示:`TestController` 中的所有方法都需要登录后才可以访问,但是 `getList` 接口可以匿名游客访问。 + +- @SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。 +- @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。 +- @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权,在下面的 [路由拦截鉴权] 章节中我们会讲到。 + + + +### 6、批量注解鉴权 + +使用 `@SaCheckOr` 表示批量注解鉴权: + +``` java +// 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。 +@SaCheckOr( + login = @SaCheckLogin, + role = @SaCheckRole("admin"), + permission = @SaCheckPermission("user.add"), + safe = @SaCheckSafe("update-password"), + httpBasic = @SaCheckHttpBasic(account = "sa:123456"), + disable = @SaCheckDisable("submit-orders") +) +@RequestMapping("test") +public SaResult test() { + // ... + return SaResult.ok(); +} +``` + +每一项属性都可以写成数组形式,例如: + +``` java +// 当前客户端只要有 [ login 账号登录] 或者 [user 账号登录] 其一,就可以通过验证进入方法。 +// 注意:`type = "login"` 和 `type = "user"` 是多账号模式章节的扩展属性,此处你可以先略过这个知识点。 +@SaCheckOr( + login = { @SaCheckLogin(type = "login"), @SaCheckLogin(type = "user") } +) +@RequestMapping("test") +public SaResult test() { + // ... + return SaResult.ok(); +} +``` + +疑问:既然有了 `@SaCheckOr`,为什么没有与之对应的 `@SaCheckAnd` 呢? + +因为当你写多个注解时,其天然就是 `and` 校验关系,例如: + +``` java +// 当你在一个方法上写多个注解鉴权时,其默认就是要满足所有注解规则后,才可以进入方法,只要有一个不满足,就会抛出异常 +@SaCheckLogin +@SaCheckRole("admin") +@SaCheckPermission("user.add") +@RequestMapping("test") +public SaResult test() { + // ... + return SaResult.ok(); +} +``` + + +使用 append 字段追加抓取扩展包里的注解,例如: +``` java +// 测试:只有通过登录校验,或者提供了正确的 ApiKey,才可以进入方法 +@RequestMapping("/test") +@SaCheckOr(login = @SaCheckLogin, append = { SaCheckApiKey.class }) +@SaCheckApiKey +public SaResult test() { + // ... + return SaResult.ok(); +} +``` + + +### 7、扩展阅读 + +- 在业务逻辑层使用鉴权注解:[AOP注解鉴权](/plugin/aop-at) + +- 制作自定义鉴权注解注入到框架:[自定义注解](/fun/custom-annotations) + + + + + +--- + + + 本章代码示例:Sa-Token 注解鉴权 —— [ AtCheckController.java ] + +本章小练习:Sa-Token 基础 - 注解鉴权,章节测试 + + + + + + + diff --git a/sa-token-doc/use/config.md b/sa-token-doc/use/config.md new file mode 100644 index 000000000..bcb78e4f9 --- /dev/null +++ b/sa-token-doc/use/config.md @@ -0,0 +1,592 @@ +# 框架配置 + +你可以**零配置启动框架**,但同时你也可以通过一定的参数配置,定制性使用框架,`Sa-Token`支持多种方式配置框架信息 + +--- + +### 1、配置方式 + +##### 方式1、在 application.yml 配置 + + + + +``` yaml +############## Sa-Token 配置 (文档: https://sa-token.com) ############## +sa-token: + # token 名称(同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true +``` + + +``` properties +############## Sa-Token 配置 (文档: https://sa-token.com) ############## + +# token 名称(同时也是 cookie 名称) +sa-token.token-name=satoken +# token 有效期(单位:秒) 默认30天,-1 代表永久有效 +sa-token.timeout=2592000 +# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 +sa-token.active-timeout=-1 +# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) +sa-token.is-concurrent=true +# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) +sa-token.is-share=false +# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) +sa-token.token-style=uuid +# 是否输出操作日志 +sa-token.is-log=true +``` + + + + + +##### 方式2、通过代码配置 + + + +``` java +/** + * Sa-Token 配置类 + */ +@Configuration +public class SaTokenConfigure { + // Sa-Token 参数配置,参考文档:https://sa-token.com + // 此配置会覆盖 application.yml 中的配置 + @Bean + @Primary + public SaTokenConfig getSaTokenConfigPrimary() { + SaTokenConfig config = new SaTokenConfig(); + config.setTokenName("satoken"); // token 名称(同时也是 cookie 名称) + config.setTimeout(30 * 24 * 60 * 60); // token 有效期(单位:秒),默认30天,-1代表永不过期 + config.setActiveTimeout(-1); // token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + config.setIsConcurrent(true); // 是否允许同一账号多地同时登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录) + config.setIsShare(false); // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token) + config.setTokenStyle("uuid"); // token 风格 + config.setIsLog(false); // 是否输出操作日志 + return config; + } +} +``` + +``` java +/** + * Sa-Token 配置类 + */ +@Configuration +public class SaTokenConfigure { + // Sa-Token 参数配置,参考文档:https://sa-token.com + // 此配置会与 application.yml 中的配置合并 (代码配置优先) + @Autowired + public void configSaToken(SaTokenConfig config) { + config.setTokenName("satoken"); // token 名称(同时也是 cookie 名称) + config.setTimeout(30 * 24 * 60 * 60); // token 有效期(单位:秒),默认30天,-1代表永不过期 + config.setActiveTimeout(-1); // token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + config.setIsConcurrent(true); // 是否允许同一账号多地同时登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录) + config.setIsShare(false); // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token) + config.setTokenStyle("uuid"); // token 风格 + config.setIsLog(false); // 是否输出操作日志 + } +} +``` + + +两者的区别在于: +- 模式 1 会覆盖 application.yml 中的配置。 +- 模式 2 会与 application.yml 中的配置合并(代码配置优先)。 + + +--- +### 2、核心包所有可配置项 + +#### 2.1、核心模块配置 + +你不必立刻掌握整个表格,只需要在用到某个功能时再详细查阅它即可 + +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| tokenName | String | satoken | Token 名称 (同时也是 Cookie 名称、数据持久化前缀) | +| timeout | long | 2592000 | Token 有效期(单位:秒),默认30天,-1代表永不过期 [参考:token有效期详解](/fun/token-timeout) | +| activeTimeout | long | -1 | Token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结(例如可以设置为1800代表30分钟内无操作就冻结) [参考:token有效期详解](/fun/token-timeout) | +| dynamicActiveTimeout | Boolean | false | 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数 | +| isConcurrent | Boolean | true | 是否允许同一账号并发登录 (为 true 时允许一起登录,为 false 时新登录挤掉旧登录) | +| isShare | Boolean | false | 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token,login 时提供了 Extra 数据后,即使配置了为 true 也不能复用旧 Token,必须创建新 Token) | +| replacedLoginExitMode | SaReplacedLoginExitMode | OLD_DEVICE | 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) | +| replacedRange | SaReplacedRange | CURR_DEVICE_TYPE | 在 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) | +| maxLoginCount | int | 12 | 同一账号最大登录数量,-1代表不限 (只有在 `isConcurrent=true`,`isShare=false` 时此配置才有效),[详解](/use/config?id=配置项详解:maxlogincount) | +| overflowLogoutMode | SaLogoutMode | LOGOUT | 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) | +| maxTryTimes | int | 12 | 在每次创建 Token 时的最高循环次数,用于保证 Token 唯一性(-1=不循环重试,直接使用) | +| isReadBody | Boolean | true | 是否尝试从 请求体 里读取 Token | +| isReadHeader | Boolean | true | 是否尝试从 header 里读取 Token | +| isReadCookie | Boolean | true | 是否尝试从 cookie 里读取 Token,此值为 false 后,`StpUtil.login(id)` 登录时也不会再往前端注入Cookie | +| isLastingCookie | Boolean | true | 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) | +| isWriteHeader | Boolean | false | 是否在登录后将 Token 写入到响应头 | +| logoutRange | SaLogoutRange | TOKEN | 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) (此参数只在调用 StpUtil.logout() 时有效) | +| isLogoutKeepFreezeOps | Boolean | false | 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) (此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token") 时有效) | +| isLogoutKeepTokenSession | Boolean | false | 在注销 token 后,是否保留其对应的 Token-Session | +| rightNowCreateTokenSession| Boolean | false | 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) | +| tokenStyle | String | uuid | token风格, [参考:自定义Token风格](/up/token-style) | +| dataRefreshPeriod | int | 30 | 默认数据持久组件实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理 | +| tokenSessionCheckLogin | Boolean | true | 获取 `Token-Session` 时是否必须登录 (如果配置为true,会在每次获取 `Token-Session` 时校验是否登录),[详解](/use/config?id=配置项详解:tokenSessionCheckLogin) | +| autoRenew | Boolean | true | 是否打开自动续签 (如果此值为true,框架会在每次直接或间接调用 `getLoginId()` 时进行一次过期检查与续签操作),[参考:token有效期详解](/fun/token-timeout) | +| tokenPrefix | String | null | token前缀,例如填写 `Bearer` 实际传参 `satoken: Bearer xxxx-xxxx-xxxx-xxxx` [参考:自定义Token前缀](/up/token-prefix) | +| cookieAutoFillPrefix | Boolean | false | cookie 模式是否自动填充 token 提交前缀 | +| isPrint | Boolean | true | 是否在初始化配置时打印版本字符画 | +| isLog | Boolean | false | 是否打印操作日志 | +| logLevel | String | trace | 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动 | +| logLevelInt | int | 1 | 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动 | +| isColorLog | Boolean | null | 是否打印彩色日志,true=打印彩色日志,false=打印黑白日志,null=框架根据运行终端自行判断是否打印彩色日志 | +| jwtSecretKey | String | null | jwt秘钥 (只有集成 `sa-token-temp-jwt` 模块时此参数才会生效),[参考:和 jwt 集成](/plugin/jwt-extend) | +| sameTokenTimeout | long | 86400 | Same-Token的有效期 (单位: 秒),[参考:内部服务外网隔离](/micro/same-token) | +| basic | String | "" | Http Basic 认证的账号和密码 [参考:Http Basic 认证](/up/basic-auth) | +| currDomain | String | null | 配置当前项目的网络访问地址 | +| checkSameToken | Boolean | false | 是否校验Same-Token(部分rpc插件有效) | +| cookie | Object | new SaCookieConfig() | Cookie 配置对象 | +| sign | Object | new SaSignConfig() | API 签名配置对象 | + +#### 2.2、Cookie相关配置: + +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| domain | String | null | 作用域(写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景) | +| path | String | / | 路径,默认写在域名根路径下 | +| secure | Boolean | false | 是否只在 https 协议下有效 | +| httpOnly | Boolean | false | 是否禁止 js 操作 Cookie | +| sameSite | String | Lax | 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) | +| extraAttrs | String | new LinkedHashMap() | 额外扩展属性 | + + +Cookie 配置示例: + + + +``` yaml +# Sa-Token 配置 +sa-token: + # Cookie 相关配置 + cookie: + # 基础属性 + domain: stp.com + path: / + secure: false + httpOnly: true + sameSite: Lax + # 额外扩展属性 + extraAttrs: + # Cookie 优先级 + Priority: Medium + # Cookie 独立分区 + Partitioned: "" + # 可以是任意键值对 + # abc: def +``` + +``` properties +# Cookie 相关配置 +# ---- 基础属性 +sa-token.cookie.domain=stp.com +sa-token.cookie.path=/ +sa-token.cookie.secure=false +sa-token.cookie.httpOnly=true +sa-token.cookie.sameSite=Lax +# ---- 额外扩展属性 +# Cookie 优先级 +sa-token.cookie.extraAttrs.Priority=Medium +# Cookie 独立分区 +sa-token.cookie.extraAttrs.Partitioned="" +# 可以是任意键值对 +# sa-token.cookie.extraAttrs.abc=def +``` + + + +#### 2.3、Sign 参数签名相关配置 + +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| secretKey | String | null | API 调用签名秘钥 | +| timestampDisparity | long | 900000 | 接口调用时的时间戳允许的差距(单位:ms),-1 代表不校验差距,默认15分钟 | +| digestAlgo | String | md5 | 对 fullStr 的摘要算法 | + +示例: + + + +``` yaml +# Sa-Token 配置 +sa-token: + # 参数签名配置 + sign: + # API 接口调用签名秘钥 + secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +``` properties +# API 接口调用签名秘钥 +sa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + + + +#### 2.4、API Key 相关配置 + +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| prefix | String | AK- | API Key 前缀 | +| timeout | long | 2592000 | API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) | +| isRecordIndex | String | true | 框架是否记录索引信息 | + +示例: + + + +``` yaml +# Sa-Token 配置 +sa-token: + # API Key 相关配置 + api-key: + # API Key 前缀 + prefix: AK- + # API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) + timeout: 2592000 + # 框架是否记录索引信息 + is-record-index: true +``` + +``` properties +# API Key 前缀 +sa-token.pi-key.prefix=AK- +# API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) +sa-token.pi-key.timeout=2592000 +# 框架是否记录索引信息 +sa-token.pi-key.is-record-index=true +``` + + + + + + + +### 3、单点登录相关配置 + +#### 3.1、SSO-Server 端配置 + +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| mode | String | | 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) | +| ticketTimeout | long | 300 | ticket 有效期 (单位: 秒) | +| homeRoute | String | | 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 | +| isSlo | Boolean | true | 是否打开单点注销功能 | +| autoRenewTimeout | Bolean | false | 是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) | +| maxRegClient | int | 32 | 在 Access-Session 上记录 Client 信息的最高数量(-1=无限),超过此值将进行自动清退处理,先进先出 | +| isCheckSign | Boolean | true | 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) | +| clients | Map | new LinkedHashMap<>(); | 以 Map 格式配置 Client 列表 | +| allowAnonClient | Boolean | false | 是否允许匿名 Client 接入。参考: [匿名 client 接入](/sso/anon-client) | +| allowUrl | String | | 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用),参考:[SSO整合:配置域名校验](/sso/sso-check-domain) | +| secretKey | String | | API 调用签名秘钥 (全局默认 + 匿名 client 使用) | + +配置示例: + + + +``` yml +# Sa-Token 配置 +sa-token: + # SSO-Server 配置 + sso-server: + # Ticket有效期 (单位: 秒),默认五分钟 + ticket-timeout: 300 + # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 + home-route: /home +``` + +``` properties +# SSO-Server 配置 +# Ticket有效期 (单位: 秒),默认五分钟 +sa-token.sso-server.ticket-timeout=300 +# 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 +sa-token.sso-server.home-route=/home +``` + + + + +#### 3.2、SSO-Client 端配置 + +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| mode | String | | 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) | +| client | String | "" | 当前 Client 名称标识,用于和 ticket 码的互相锁定 | +| serverUrl | String | null | 配置 Server 端主机总地址,拼接在 `authUrl`、`signoutUrl`、`pushUrl` 属性前面,用以简化各种 url 配置,参考:[详解](/sso/sso-questions?id=问:模式三配置一堆-xxx-url-,有办法简化一下吗?) | +| authUrl | String | /sso/auth | 配置 Server 端单点登录授权地址 | +| signoutUrl | String | /sso/signout | 配置 Server 端单点注销地址 | +| pushUrl | String | /sso/pushS | 配置 Server 端的推送消息地址 | +| getDataUrl | String | /sso/getData | 配置 Server 端的 拉取数据 地址 | +| currSsoLogin | String | null | 配置当前 Client 端的登录地址(为空时自动获取) | +| currSsoLogoutCall | String | null | 配置当前 Client 端的单点注销回调URL (为空时自动获取) | +| isHttp | Boolean | false | 是否打开模式三(此值为 true 时将使用 http 请求:校验 ticket 值、单点注销、拉取数据getData),参考:[详解](/use/config?id=配置项详解:isHttp) | +| isSlo | Boolean | true | 是否打开单点注销功能 | +| regLogoutCall | Boolean | false | 是否注册单点登录注销回调 (为 true 时,登录时附带单点登录回调地址,并且开放 /sso/logoutCall 地址) | +| secretKey | String | "" | API 调用签名秘钥 | +| isCheckSign | Boolean | true | 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) | + +配置示例: + + + +``` yaml +# Sa-Token 配置 +sa-token: + # SSO-相关配置 + sso-client: + # sso-server 端主机地址 + server-url: http://sa-sso-server.com:9000 + # 是否打开单点注销功能 + is-slo: true +``` + +``` properties +# sso-server 端主机地址 +sa-token.sso-client.server-url=http://sa-sso-server.com:9000 +# 是否打开单点注销功能 +sa-token.sso-client.is-slo=true +``` + + + + +#### 3.3、SaSsoClientModel 配置 + +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| client | String | "" | 当前 Client 名称标识,用于和 ticket 码的互相锁定 | +| allowUrl | String | | 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用),参考:[SSO整合:配置域名校验](/sso/sso-check-domain) | +| isPush | Boolean | false | 是否接收推送消息 | +| isSlo | Boolean | true | 是否打开单点注销功能 | +| secretKey | String | "" | API 调用签名秘钥 | +| serverUrl | String | null | 配置此 Client 端主机总地址,拼接在 `pushUrl` 属性前面(默认 `/sso/pushC`),用以简化推送地址配置 | +| pushUrl | String | /sso/pushC | 配置此 Client 端的推送消息地址 | + +配置示例: + + + +``` yml +# Sa-Token 配置 +sa-token: + # SSO-Server 配置 + sso-server: + # 应用列表:配置接入的应用信息 + clients: + # 应用 sso-client1 + sso-client1: + client: sso-client1 + allow-url: "*" + secret-key: SSO-C1-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + # 应用 sso-client2 + sso-client2: + client: sso-client2 + allow-url: "*" + secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + +``` properties +# 应用列表:配置接入的应用信息 +# 应用 sso-client1 +sa-token.sso-server.clients.sso-client1.client=sso-client1 +sa-token.sso-server.clients.sso-client1.allow-url=* +sa-token.sso-server.clients.sso-client1.secret-key=SSO-C1-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor + +# 应用 sso-client2 +sa-token.sso-server.clients.sso-client2.client=sso-client2 +sa-token.sso-server.clients.sso-client2.allow-url=* +sa-token.sso-server.clients.sso-client2.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor +``` + + + + + + + +### 4、OAuth2.0相关配置 + +#### 4.1、OAuth2-Server 相关配置 + +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| enableAuthorizationCode | Boolean | true | 是否打开模式:授权码(`Authorization Code`) | +| enableImplicit | Boolean | true | 是否打开模式:隐藏式(`Implicit`) | +| enablePassword | Boolean | true | 是否打开模式:密码式(`Password`) | +| enableClientCredentials | Boolean | true | 是否打开模式:凭证式(`Client Credentials`) | +| codeTimeout | long | 300 | Code授权码 保存的时间(单位:秒) 默认五分钟 | +| accessTokenTimeout | long | 7200 | 全局默认配置所有应用:`Access-Token` 保存的时间(单位:秒)默认两个小时 | +| refreshTokenTimeout | long | 2592000 | 全局默认配置所有应用:`Refresh-Token` 保存的时间(单位:秒) 默认30 天 | +| clientTokenTimeout | long | 7200 | 全局默认配置所有应用:`Client-Token` 保存的时间(单位:秒) 默认两个小时 | +| maxAccessTokenCount | int | 12 | 全局默认配置所有应用:单个应用单个用户最多同时存在的 Access-Token 数量 | +| maxRefreshTokenCount | int | 12 | 全局默认配置所有应用:单个应用单个用户最多同时存在的 Refresh-Token 数量 | +| maxClientTokenCount | int | 12 | 全局默认配置所有应用:单个应用最多同时存在的 Client-Token 数量 | +| isNewRefresh | Boolean | false | 全局默认配置所有应用:是否在每次 `Refresh-Token` 刷新 `Access-Token` 时,产生一个新的 `Refresh-Token` | +| openidDigestPrefix | String | openid_default_digest_prefix | 默认 openid 生成算法中使用的摘要前缀 | +| unionidDigestPrefix | String | unionid_default_digest_prefix | 默认 unionid 生成算法中使用的摘要前缀 | +| higherScope | String | | 指定高级权限,多个用逗号隔开 | +| lowerScope | String | | 指定低级权限,多个用逗号隔开 | +| mode4ReturnAccessToken | Boolean | false | 模式4是否返回 AccessToken 字段,用于兼容OAuth2标准协议 | +| hideStatusField | Boolean | false | 是否在返回值中隐藏默认的状态字段 (code、msg、data) | +| oidc | SaOAuth2OidcConfig | new SaOAuth2OidcConfig() | OIDC 相关配置 | +| clients | Map | 配置 SaClientModel 列表信息 | + +配置示例: + + +``` yaml +# Sa-Token 配置 +sa-token: + token-name: sa-token-oauth2-server + # OAuth2.0 配置 + oauth2-server: + enable-authorization-code: true + enable-implicit: true + enable-password: true + enable-client-credentials: true +``` + +``` properties +# Sa-Token 配置 +sa-token.token-name=sa-token-oauth2-server +# OAuth2.0 配置 +sa-token.oauth2-server.enable-authorization-code=true +sa-token.oauth2-server.enable-implicit=true +sa-token.oauth2-server.enable-password=true +sa-token.oauth2-server.enable-client-credentials=true +``` + + + +#### 4.2、OIDC 相关配置 +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| iss | String | | iss 值,如不配置则自动计算 | +| idTokenTimeout | long | 600 | idToken 有效期(单位秒) 默认十分钟 | + + + +``` yaml +# Sa-Token 配置 +sa-token: + oauth2-server: + oidc: + iss: xxx + idTokenTimeout: 600 +``` + +``` properties +sa-token.oauth2-server.oidc.iss=xxx +sa-token.oauth2-server.oidc.idTokenTimeout=600 +``` + + + + +#### 4.3、SaClientModel属性定义 +| 参数名称 | 类型 | 默认值 | 说明 | +| :-------- | :-------- | :-------- | :-------- | +| clientId | String | null | 应用id,应该全局唯一 | +| clientSecret | String | null | 应用秘钥 | +| contractScopes | List | [] | 应用签约的所有权限 | +| allowRedirectUris | List | [] | 应用允许授权的所有URL(可以使用 `*` 号通配符) | +| allowGrantTypes | List | [] | 应用允许的所有 `grant_type` | +| subjectId | String | null | 应用主体id | +| accessTokenTimeout | long | 取全局配置 (7200) | 此应用`Access-Token` 保存的时间(单位:秒) [默认取全局配置] | +| refreshTokenTimeout | long | 取全局配置 (2592000)| 此应用`Refresh-Token` 保存的时间(单位:秒) [默认取全局配置] | +| clientTokenTimeout | Boolean | 取全局配置 (7200)| 此应用`Client-Token` 保存的时间(单位:秒) [默认取全局配置] | +| maxAccessTokenCount | long | 取全局配置 (12)| 此应用单个用户最多同时存在的 Access-Token 数量 | +| maxRefreshTokenCount | long | 取全局配置 (12)| 此应用单个用户最多同时存在的 Refresh-Token 数量 | +| maxClientTokenCount | long | 取全局配置 (12)| 此应用最多同时存在的 Client-Token 数量 | +| isNewRefresh | Boolean | 取全局配置 | 单独配置此 Client:是否在每次 `Refresh-Token` 刷新 `Access-Token` 时,产生一个新的 Refresh-Token [ 默认取全局配置 ] | +| isAutoConfirm | Boolean | false | 是否允许此应用自动确认授权 (高危配置,禁止向不被信任的第三方开启此选项) | + + + + + +### 5、部分配置项详解 + +对部分配置项做一下详解 + +#### 配置项详解:maxLoginCount + +配置含义:同一账号最大登录数量。 + +在配置 `isConcurrent=true`, `isShare=false` 时,Sa-Token 将允许同一账号并发登录,且每次登录都会产生一个新Token, +这些 Token 都会以 `SaTerminalInfo` 的形式记录在其 `Account-Session` 之上,这就造成一个问题: + +随着同一账号登录的次数越来越多,SaTerminalInfo 的列表也会越来越大,极端情况下,列表长度可能达到成百上千以上,严重拖慢数据处理速度, +为此 Sa-Token 对这个 SaTerminalInfo 列表的大小设定一个上限值,也就是 `maxLoginCount`,默认值=12。 + +假设一个账号的登录数量超过 `maxLoginCount` 后,将会主动注销第一个登录的会话(先进先出),以此保证队列中的有效会话数量始终 `<= maxLoginCount` 值。 + + +#### 配置项详解:tokenSessionCheckLogin +配置含义:获取 `Token-Session` 时是否必须登录 (如果配置为true,会在每次获取 `Token-Session` 时校验是否登录)。 + +在调用 `StpUtil.login(id)` 登录后, + +- 调用 `StpUtil.getSession()` 可以获取这个会话的 `Account-Session` 对象。 +- 调用 `StpUtil.getTokenSession()` 可以获取这个会话 `Token-Session` 对象。 + +关于两种 Session 有何区别,可以参考这篇:[Session模型详解](/fun/session-model),此处暂不赘述。 + +从设计上讲,无论会话是否已经登录,只要前端提供了Token,我们就可以找到这个 Token 的专属 `Token-Session` 对象,**这非常灵活但不安全**, +因为前端提交的 Token 可能是任意伪造的。 + +为了解决这个问题,`StpUtil.getTokenSession()` 方法在获取 `Token-Session` 时,会率先检测一下这个 Token 是否是一个有效Token: +- 如果是有效Token,正常返回 `Token-Session` 对象 +- 如果是无效Token,则抛出异常。 + +这样就保证了伪造的 Token 是无法获取 `Token-Session` 对象的。 + +但是 —— 有的场景下我们又确实需要在登录之前就使用 Token-Session 对象,这时候就把配置项 `tokenSessionCheckLogin` 值改为 `false` 即可。 + + + +#### 配置项详解:isHttp + +配置含义:是否打开单点登录模式三。 + +- 此配置项为 false 时,代表使用SSO模式二:使用 Redis 校验 ticket 值、删除 Redis 数据做到单点注销、使用 Redis 同步 Userinfo 数据。 +- 此配置项为 true 时,代表使用SSO模式三:使用 Http 请求校验 ticket 值、使用 Http 请求做到单点注销、使用 Http 请求同步 Userinfo 数据。 + + + + +--- + + + 本章代码示例:Sa-Token 框架配置 —— [ application.yml ] + +本章小练习:Sa-Token 基础 - 框架配置,章节测试 + diff --git a/sa-token-doc/use/dao-extend.md b/sa-token-doc/use/dao-extend.md new file mode 100644 index 000000000..b402bc6ad --- /dev/null +++ b/sa-token-doc/use/dao-extend.md @@ -0,0 +1,87 @@ +# 持久层扩展 +--- + +Sa-token默认将会话数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如:重启后数据会丢失,无法在集群模式下共享数据 + +为此,Sa-Token将数据持久操作全部抽象到 `SaTokenDao` 接口中,保证大家对框架进行灵活扩展,比如我们可以将会话数据存储在 `Redis`、`Memcached`等专业的缓存中间件中,做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性 + +除了框架内部对`SaTokenDao`提供的基于内存的默认实现,官方仓库还提供了以下扩展方案:
    + + +### 1. Sa-Token 整合 Redis (使用jdk默认序列化方式) +``` xml + + + cn.dev33 + sa-token-redis + ${sa.top.version} + +``` +优点:兼容性好,缺点:Session序列化后基本不可读,对开发者来讲等同于乱码 + + +### 2. Sa-Token 整合 RedisTemplate +``` xml + + + cn.dev33 + sa-token-redis-template + ${sa.top.version} + +``` +优点:Session 序列化后可读性强(默认 JSON 格式),可灵活手动修改 + + +
    + +### 集成Redis请注意: + + +**1. 无论使用哪种序列化方式,你都必须为项目提供一个Redis实例化方案,例如:** +``` xml + + + org.apache.commons + commons-pool2 + +``` + +**2. 引入了依赖,我还需要为Redis配置连接信息吗?**
    +需要!只有项目初始化了正确的Redis实例,`Sa-Token`才可以使用Redis进行数据持久化,参考以下`yml配置`: +``` java +# 端口 +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + # password: + # 连接超时时间(毫秒) + timeout: 1000ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 +``` + + +**3. 集成Redis后,是我额外手动保存数据,还是框架自动保存?**
    +框架自动保存。集成`Redis`只需要引入对应的`pom依赖`即可,框架所有上层API保持不变 + + +

    +更多框架的集成方案正在更新中... (欢迎大家提交pr) + + + diff --git a/sa-token-doc/use/jur-auth.md b/sa-token-doc/use/jur-auth.md new file mode 100644 index 000000000..dab5e66a8 --- /dev/null +++ b/sa-token-doc/use/jur-auth.md @@ -0,0 +1,212 @@ +# 权限认证 + +--- + +### 1、设计思路 + +权限认证的最终目的在于:规定哪些用户可以访问哪些 接口/页面/资源。 + +例如对于同一个页面: +- 管理员账号访问:正常返回数据。 +- 普通账号访问:权限不足,拒绝访问。 + + + + +那么框架是如何判断,一个账号是否有权限访问某个接口的呢? + +从底层数据的角度来讲,**每个账号都会拥有一组权限码集合,框架要做的就是校验这个集合中是否包含指定的权限码。** + +- 有,就让你通过。 +- 没有?那么禁止访问! + + + + +所以现在问题的核心就是两个: +1. 如何定义一个账号所拥有的权限码集合? +2. 本次操作需要校验的权限码是哪个? + + +### 2、获取当前账号权限码集合 + +在进行具体的权限校验之前,你需要实现 `StpInterface`接口,告诉框架指定账号拥有的权限码集合是哪些: + +``` java +/** + * 自定义权限加载接口实现类 + */ +@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展 +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限 + List list = new ArrayList(); + list.add("101"); + list.add("user.add"); + list.add("user.update"); + list.add("user.get"); + // list.add("user.delete"); + list.add("art.*"); + return list; + } + + /** + * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) + */ + @Override + public List getRoleList(Object loginId, String loginType) { + // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色 + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } + +} +``` + +**参数解释:** +- loginId:账号id,即你在调用 `StpUtil.login(id)` 时写入的`唯一标识`值。 +- loginType:账号体系标识,此处可以暂时忽略,在 [ 多账户认证 ] 章节下会对这个概念做详细的解释。 + +可参考代码:[码云:StpInterfaceImpl.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpInterfaceImpl.java) + +> [!WARNING| label:有同学会产生疑问:我实现了此接口,但是程序启动时好像并没有执行,是不是我写错了?] +> 答:不执行是正常现象,程序启动时不会执行这个接口的方法,在每次调用鉴权代码时,才会执行到此。 + + +### 3、权限校验 +然后就可以用以下 api 来鉴权了 + +``` java +// 获取:当前账号所拥有的权限集合 +StpUtil.getPermissionList(); + +// 判断:当前账号是否含有指定权限, 返回 true 或 false +StpUtil.hasPermission("user.add"); + +// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException +StpUtil.checkPermission("user.add"); + +// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过] +StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get"); + +// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] +StpUtil.checkPermissionOr("user.add", "user.delete", "user.get"); +``` + +扩展:`NotPermissionException` 异常对象可通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常 + + +### 4、角色校验 +在 Sa-Token 中,角色和权限可以分开独立验证 + +``` java +// 获取:当前账号所拥有的角色集合 +StpUtil.getRoleList(); + +// 判断:当前账号是否拥有指定角色, 返回 true 或 false +StpUtil.hasRole("super-admin"); + +// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException +StpUtil.checkRole("super-admin"); + +// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] +StpUtil.checkRoleAnd("super-admin", "shop-admin"); + +// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] +StpUtil.checkRoleOr("super-admin", "shop-admin"); +``` + +扩展:NotRoleException 异常对象可通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常 + + + +### 5、拦截全局异常 +有同学要问,鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?**当然不可以!** + +你可以创建一个全局异常拦截器,统一返回给前端的格式,参考: + +``` java +@RestControllerAdvice +public class GlobalExceptionHandler { + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } +} +``` + +可参考:[码云:GlobalException.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java) + + +### 6、权限通配符 +Sa-Token允许你根据通配符指定**泛权限**,例如当一个账号拥有`art.*`的权限时,`art.add`、`art.delete`、`art.update`都将匹配通过 + +``` java +// 当拥有 art.* 权限时 +StpUtil.hasPermission("art.add"); // true +StpUtil.hasPermission("art.update"); // true +StpUtil.hasPermission("goods.add"); // false + +// 当拥有 *.delete 权限时 +StpUtil.hasPermission("art.delete"); // true +StpUtil.hasPermission("user.delete"); // true +StpUtil.hasPermission("user.update"); // false + +// 当拥有 *.js 权限时 +StpUtil.hasPermission("index.js"); // true +StpUtil.hasPermission("index.css"); // false +StpUtil.hasPermission("index.html"); // false +``` + +> [!WARNING| label:上帝权限] +> 当一个账号拥有 `"*"` 权限时,他可以验证通过任何权限码 (角色认证同理) + + +### 7、如何把权限精确到按钮级? +权限精确到按钮级的意思就是指:**权限范围可以控制到页面上的每一个按钮是否显示**。 + + + +思路:如此精确的范围控制只依赖后端已经难以完成,此时需要前端进行一定的逻辑判断。 + +如果是前后端一体项目,可以参考:[Thymeleaf 标签方言](/plugin/thymeleaf-extend),如果是前后端分离项目,则: + +1. 在登录时,把当前账号拥有的所有权限码一次性返回给前端。 +2. 前端将权限码集合保存在`localStorage`或其它全局状态管理对象中。 +3. 在需要权限控制的按钮上,使用 js 进行逻辑判断,例如在`Vue`框架中我们可以使用如下写法: +``` js +// `arr`是当前用户拥有的权限码数组 +// `user.delete`是显示按钮需要拥有的权限码 +// `删除按钮`是用户拥有权限码才可以看到的内容。 +

    + + + +
    +``` + +以上写法只为提供一个参考示例,不同框架有不同写法,大家可根据项目技术栈灵活封装进行调用。 + + +> [!ATTENTION| label:前端有了鉴权后端还需要鉴权吗?] +> **需要!**
    +> 前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全:**无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验!** + + + +--- + + + 本章代码示例:Sa-Token 权限认证 —— [ JurAuthController.java ] + +本章小练习:Sa-Token 基础 - 权限认证,章节测试 diff --git a/sa-token-doc/use/kick.md b/sa-token-doc/use/kick.md new file mode 100644 index 000000000..2642fe63a --- /dev/null +++ b/sa-token-doc/use/kick.md @@ -0,0 +1,50 @@ +# 踢人下线 + +所谓踢人下线,核心操作就是找到指定 `loginId` 对应的 `Token`,并设置其失效。 + +踢下线 + +--- + + +### 1、强制注销 +``` java +StpUtil.logout(10001); // 强制指定账号注销下线 +StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线 +StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线 +``` + + +### 2、踢人下线 +``` java +StpUtil.kickout(10001); // 将指定账号踢下线 +StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线 +StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线 +``` + +强制注销 和 踢人下线 的区别在于: +- 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。 +- 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。 + + + + + +### 3、顶人下线 +“顶人下线” 操作发生在框架登录时顶退旧登录设备,属于框架内部操作,一般情形下你不会调用到此 API: +``` java +StpUtil.replaced(10001); // 将指定账号顶下线 +StpUtil.replaced(10001, "PC"); // 将指定账号指定端顶下线 +StpUtil.replacedByTokenValue("token"); // 将指定 Token 顶下线 +``` + + +--- + + + 本章代码示例:Sa-Token 踢人下线 —— [ KickoutController.java ] + +本章小练习:Sa-Token 基础 - 踢人下线,章节测试 + + diff --git a/sa-token-doc/use/login-auth.md b/sa-token-doc/use/login-auth.md new file mode 100644 index 000000000..a926b58d8 --- /dev/null +++ b/sa-token-doc/use/login-auth.md @@ -0,0 +1,249 @@ +# 登录认证 + +--- + + +### 1、开始登录 + +一个完整的登录认证包含哪些步骤?让我们代入用户视角:在打开 网站/APP 后,用户的操作流程大致可以概括为: +1. 打开 网站/APP,进入登录页。 +2. 输入 账号+密码 进行登录。 +3. 进入首页,进行业务相关操作。 +4. 注销登录,关闭 网站/APP。 + +在整个流程中,Sa-Token 负责哪些部分呢? 下图可以帮助你理解: + + + +如上图所示:**无论用户采用何种登录方式,本质上都是通过提交一定的认证信息,使系统可以定位到 Ta 的唯一标识 —— userId**。 + +当我们拿到 userId 后,便可以调用框架提供的 API 进行登录: + +``` java +// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等 +StpUtil.login(Object userId); +``` + +只此一句代码,便可以使会话登录成功。实际上,Sa-Token 在背后做了大量的工作,包括但不限于: + +1. 检查此账号是否之前已有登录; +2. 为账号生成 Token 凭证与 Session 会话; +3. 记录 Token 活跃时间; +4. 通知全局侦听器,xx 账号登录成功; +5. 检查此账号登录数量是否已达上限; +6. 将 Token 注入到请求上下文; +7. 等等其它工作…… + +你暂时不需要完整了解完整过程,你只需要记住关键一点:**Sa-Token 为这个账号创建了一个 token 凭证,且通过 Cookie 上下文返回给了前端**。 + +所以一般情况下,我们的登录接口代码,会大致类似如下: + +``` java +// 会话登录接口 +@RequestMapping("doLogin") +public SaResult doLogin(String name, String pwd) { + // 第一步:比对前端提交的账号名称、密码 + if("zhang".equals(name) && "123456".equals(pwd)) { + // 第二步:根据账号id,进行登录 + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); +} +``` + +如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:**此处仅仅做了会话登录,但并没有主动向前端返回 token 信息。** + +是因为不需要吗?严格来讲是需要的,只不过 `StpUtil.login(id)` 方法利用了 Cookie 自动注入的特性,省略了你手写返回 token 的代码。 + +> [!TIP| label:Cookie 是什么?] +> 如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点: +> +> - Cookie 可以从后端控制往浏览器中写入 token 值。 +> - Cookie 会在前端每次发起请求时自动提交 token 值。 +> +> 因此,在 Cookie 功能的加持下,我们可以仅靠 `StpUtil.login(id)` 一句代码就完成登录认证。 +> +> 在浏览器打开 f12 控制台,即可看到被注入的 Cookie 值: +> +> + + +### 2、校验是否登录 + +对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验: + +- 如果校验通过,则:正常返回数据。 +- 如果校验未通过,则:抛出异常,告知其需要先进行登录。 + + + + + +使用以下方法判断当前会话是否已登录: + +``` java +// 判断当前会话是否已经登录,返回 true=已登录,false=未登录 +StpUtil.isLogin(); + +// 检验当前会话是否已经登录, 如果已登录代码会安全通过,未登录则抛出异常:`NotLoginException` +StpUtil.checkLogin(); +``` + +例如我们可以在接口内,根据是否登录返回不同的信息: + +``` java +// 获取我的资料信息 +@RequestMapping("myInfo") +public String myInfo() { + if( StpUtil.isLogin() ) { + // ... + return "我的资料信息..."; + } else { + return "未登录,请先登录"; + } +} +``` + +或者在未登录时直接抛出全局异常: + +``` java +// 获取我的资料信息 +@RequestMapping("myInfo") +public String myInfo() { + StpUtil.checkLogin(); // 如果当前未登录,这句代码会直接抛出异常 `NotLoginException` + return "我的资料信息"; +} +``` + +配合全局异常处理器,统一返回固定格式数据到前端: +``` java +@RestControllerAdvice +public class GlobalException { + @ExceptionHandler(NotLoginException.class) + public SaResult handlerException(NotLoginException e) { + return SaResult.error(e.getMessage()); + } +} +``` + +异常 `NotLoginException` 代表当前会话暂未登录,可能的原因有很多: +- 前端没有提交 token。 +- 前端提交的 token 是无效的。 +- 前端提交的 token 已经过期。 +- …… + +可参照此篇:[未登录场景值](/fun/not-login-scene),了解如何获取未登录的场景值。 + + +### 3、会话查询 + +如果你想要获取当前登录的是谁: + +``` java +// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException` +StpUtil.getLoginId(); + +// 类似查询API还有: +StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型 +StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型 +StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型 + +// ---------- 以下方法可以指定未登录情形下返回的默认值 ---------- + +// 获取当前会话账号id, 如果未登录,则返回 null +StpUtil.getLoginIdDefaultNull(); + +// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型) +StpUtil.getLoginId(T defaultValue); +``` + + +### 4、token 查询 +``` java +// 获取当前会话的 token 值 +StpUtil.getTokenValue(); + +// 获取当前`StpLogic`的 token 名称 +StpUtil.getTokenName(); + +// 获取指定 token 对应的账号id,如果未登录,则返回 null +StpUtil.getLoginIdByToken(String tokenValue); + +// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效) +StpUtil.getTokenTimeout(); + +// 获取当前会话的 token 信息参数 +StpUtil.getTokenInfo(); +``` + +有关`TokenInfo`参数详解,请参考:[TokenInfo参数详解](/fun/token-info) + + +### 5、会话注销 +``` java +// 当前会话注销登录 +StpUtil.logout(); +``` + + +### 6、来个小测试,加深一下理解 +新建 `LoginController`,复制或手动敲出以下代码 +``` java +/** + * 登录测试 + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功"); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} +``` + + +--- + + + 本章代码示例:Sa-Token 登录认证 —— [ LoginAuthController.java ] + +本章小练习:Sa-Token 基础 - 登录认证,章节测试 + + + + + + + + + diff --git a/sa-token-doc/use/route-check.md b/sa-token-doc/use/route-check.md new file mode 100644 index 000000000..196c16424 --- /dev/null +++ b/sa-token-doc/use/route-check.md @@ -0,0 +1,270 @@ +# 路由拦截鉴权 + +假设我们有如下需求:*项目中所有接口均需要登录校验,只有 “登录接口” 本身对外开放*。 + +如果给每个接口都手动加上注解鉴权,将会是一件比较麻烦的事情,这时候使用拦截器鉴权模式将大大降低我们的代码量。 + + + + + +如上图所示,拦截器将拦截除登录以外的所以请求,并进行一道前置审核决定是否通过。 + +--- + + +### 1、注册 Sa-Token 路由拦截器 +以`SpringBoot2.0`为例,新建配置类`SaTokenConfigure.java` +``` java +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + // 注册拦截器 + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。 + registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())) + .addPathPatterns("/**") + .excludePathPatterns("/user/doLogin"); + } +} +``` +以上代码,我们注册了一个基于 `StpUtil.checkLogin()` 的登录校验拦截器,并且排除了`/user/doLogin`接口用来开放登录(除了`/user/doLogin`以外的所有接口都需要登录才能访问)。 + +> [!WARNING| label:版本升级] +> `SaInterceptor` 是新版本提供的拦截器,点此 [查看旧版本代码迁移示例](https://blog.csdn.net/shengzhang_/article/details/126458949)。 + +### 2、校验函数详解 +自定义认证规则:`new SaInterceptor(handle -> StpUtil.checkLogin())` 是最简单的写法,代表只进行登录校验功能。 + +我们可以往构造函数塞一个完整的 lambda 函数,来定义详细的校验规则,例如: + +``` java +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册 Sa-Token 拦截器,定义详细认证规则 + registry.addInterceptor(new SaInterceptor(handler -> { + // 指定一条 match 规则 + SaRouter + .match("/**") // 拦截的 path 列表,可以写多个 */ + .notMatch("/user/doLogin") // 排除掉的 path 列表,可以写多个 + .check(r -> StpUtil.checkLogin()); // 要执行的校验动作,可以写完整的 lambda 表达式 + + // 根据路由划分模块,不同模块不同鉴权 + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); + })).addPathPatterns("/**"); + } +} +``` + +SaRouter.match() 匹配函数有两个参数: +- 参数一:要匹配的path路由。 +- 参数二:要执行的校验函数。 + +在校验函数内不只可以使用 `StpUtil.checkPermission("xxx")` 进行权限校验,你还可以写任意代码,例如: + +``` java +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + // 注册 Sa-Token 的拦截器 + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册路由拦截器,自定义认证规则 + registry.addInterceptor(new SaInterceptor(handler -> { + + // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 + SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin()); + + // 角色校验 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证 + SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin")); + + // 权限校验 -- 不同模块校验不同权限 + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); + SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); + + // 甚至你可以随意的写一个打印语句 + SaRouter.match("/**", r -> System.out.println("----啦啦啦----")); + + // 连缀写法 + SaRouter.match("/**").check(r -> System.out.println("----啦啦啦----")); + + })).addPathPatterns("/**"); + } +} +``` + + +### 3、匹配特征详解 + +除了上述示例的 path 路由匹配,还可以根据很多其它特征进行匹配,以下是所有可匹配的特征: + +``` java +// 基础写法样例:匹配一个path,执行一个校验函数 +SaRouter.match("/user/**").check(r -> StpUtil.checkLogin()); + +// 根据 path 路由匹配 ——— 支持写多个path,支持写 restful 风格路由 +// 功能说明: 使用 /user , /goods 或者 /art/get 开头的任意路由都将进入 check 方法 +SaRouter.match("/user/**", "/goods/**", "/art/get/{id}").check( /* 要执行的校验函数 */ ); + +// 根据 path 路由排除匹配 +// 功能说明: 使用 .html , .css 或者 .js 结尾的任意路由都将跳过, 不会进入 check 方法 +SaRouter.match("/**").notMatch("*.html", "*.css", "*.js").check( /* 要执行的校验函数 */ ); + +// 根据请求类型匹配 +SaRouter.match(SaHttpMethod.GET).check( /* 要执行的校验函数 */ ); + +// 根据一个 boolean 条件进行匹配 +SaRouter.match( StpUtil.isLogin() ).check( /* 要执行的校验函数 */ ); + +// 根据一个返回 boolean 结果的lambda表达式匹配 +SaRouter.match( r -> StpUtil.isLogin() ).check( /* 要执行的校验函数 */ ); + +// 多个条件一起使用 +// 功能说明: 必须是 Get 请求 并且 请求路径以 `/user/` 开头 +SaRouter.match(SaHttpMethod.GET).match("/user/**").check( /* 要执行的校验函数 */ ); + +// 可以无限连缀下去 +// 功能说明: 同时满足 Get 方式请求, 且路由以 /admin 开头, 路由中间带有 /send/ 字符串, 路由结尾不能是 .js 和 .css +SaRouter + .match(SaHttpMethod.GET) + .match("/admin/**") + .match("/**/send/**") + .notMatch("/**/*.js") + .notMatch("/**/*.css") + // .... + .check( /* 只有上述所有条件都匹配成功,才会执行最后的check校验函数 */ ); +``` + + + +### 4、提前退出匹配链 +使用 `SaRouter.stop()` 可以提前退出匹配链,例: + +``` java +registry.addInterceptor(new SaInterceptor(handler -> { + SaRouter.match("/**").check(r -> System.out.println("进入1")); + SaRouter.match("/**").check(r -> System.out.println("进入2")).stop(); + SaRouter.match("/**").check(r -> System.out.println("进入3")); + SaRouter.match("/**").check(r -> System.out.println("进入4")); + SaRouter.match("/**").check(r -> System.out.println("进入5")); +})).addPathPatterns("/**"); +``` +如上示例,代码运行至第2条匹配链时,会在stop函数处提前退出整个匹配函数,从而忽略掉剩余的所有match匹配 + +除了`stop()`函数,`SaRouter`还提供了 `back()` 函数,用于:停止匹配,结束执行,直接向前端返回结果 +``` java +// 执行back函数后将停止匹配,也不会进入Controller,而是直接将 back参数 作为返回值输出到前端 +SaRouter.match("/user/back").back("要返回到前端的内容"); +``` + +stop() 与 back() 函数的区别在于: +- `SaRouter.stop()` 会停止匹配,进入Controller。 +- `SaRouter.back()` 会停止匹配,直接返回结果到前端。 + + +### 5、使用free打开一个独立的作用域 + +``` java +// 进入 free 独立作用域 +SaRouter.match("/**").free(r -> { + SaRouter.match("/a/**").check(/* --- */); + SaRouter.match("/b/**").check(/* --- */).stop(); + SaRouter.match("/c/**").check(/* --- */); +}); +// 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配 +SaRouter.match("/**").check(/* --- */); +``` + +free() 的作用是:打开一个独立的作用域,使内部的 stop() 不再一次性跳出整个 Auth 函数,而是仅仅跳出当前 free 作用域。 + + +### 6、使用注解忽略掉路由拦截校验 + +我们可以使用 `@SaIgnore` 注解,忽略掉路由拦截认证: + +1、先配置好了拦截规则: +``` java +@Override +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handler -> { + // 根据路由划分模块,不同模块不同鉴权 + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + // ... + })).addPathPatterns("/**"); +} +``` + +2、然后在 `Controller` 里又添加了忽略校验的注解 +``` java +@SaIgnore +@RequestMapping("/user/getList") +public SaResult getList() { + System.out.println("------------ 访问进来方法"); + return SaResult.ok(); +} +``` + +请求将会跳过拦截器的校验,直接进入 Controller 的方法中。 + +> [!WARNING| label:注意点] +> 注解 `@SaIgnore` 的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效,对自定义拦截器与过滤器不生效。 + + + + +### 7、关闭注解校验 + +`SaInterceptor` 只要注册到项目中,默认就会打开注解校验,如果要关闭此能力,需要指定 `isAnnotation` 为 false: + +``` java +@Override +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor( + new SaInterceptor(handle -> { + SaRouter.match("/**").check(r -> StpUtil.checkLogin()); + }).isAnnotation(false) // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了 + ).addPathPatterns("/**"); +} +``` + + +你也可以使用 `setBeforeAuth` 注册认证前置函数: + +``` java +@Override +public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SaInterceptor(handle -> { + System.out.println(1); + }) + .setBeforeAuth(handle -> { + System.out.println(2); + }) + ).addPathPatterns("/**"); +} +``` + +如上代码,先执行 2,再执行注解鉴权,再执行 1,如果 beforeAuth 里包含 `SaRouter.stop()` 将跳过后续的注解鉴权和 auth 认证环节。 + + + +--- + + + 本章代码示例:Sa-Token 路由拦截鉴权 —— [ SaTokenConfigure.java ] + +本章小练习:Sa-Token 基础 - 路由拦截鉴权,章节测试 + + diff --git a/sa-token-doc/use/session.md b/sa-token-doc/use/session.md new file mode 100644 index 000000000..b8af57c65 --- /dev/null +++ b/sa-token-doc/use/session.md @@ -0,0 +1,182 @@ +# Session会话 + +--- + +### 1、Session是什么? + +Session 是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据,提高程序性能,例如: + +``` java +// 在登录时缓存 user 对象 +StpUtil.getSession().set("user", user); + +// 然后我们就可以在任意处使用这个 user 对象 +SysUser user = (SysUser) StpUtil.getSession().get("user"); +``` + +在 Sa-Token 中,Session 分为三种,分别是: + +- `Account-Session`: 指的是框架为每个 账号id 分配的 Session +- `Token-Session`: 指的是框架为每个 token 分配的 Session +- `Custom-Session`: 指的是以一个 特定的值 作为SessionId,来分配的 Session + +> [!TIP| style:callout] +> 有关 Account-Session 与 Token-Session 的详细区别,可参考:[Session模型详解](/fun/session-model) + + +### 2、Account-Session +有关 账号-Session 的 API 如下: +``` java +// 获取当前账号 id 的 Account-Session (必须是登录后才能调用) +StpUtil.getSession(); + +// 获取当前账号 id 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回 +StpUtil.getSession(true); + +// 获取账号 id 为 10001 的 Account-Session +StpUtil.getSessionByLoginId(10001); + +// 获取账号 id 为 10001 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回 +StpUtil.getSessionByLoginId(10001, true); + +// 获取 SessionId 为 xxxx-xxxx 的 Account-Session, 在 Session 尚未创建时, 返回 null +StpUtil.getSessionBySessionId("xxxx-xxxx"); +``` + + +### 3、Token-Session +有关 令牌-Session 的 API 如下: +``` java +// 获取当前 Token 的 Token-Session 对象 +StpUtil.getTokenSession(); + +// 获取指定 Token 的 Token-Session 对象 +StpUtil.getTokenSessionByToken(token); +``` + + +### 4、Custom-Session +自定义 Session 指的是以一个`特定的值`作为 SessionId 来分配的`Session`, 借助自定义Session,你可以为系统中的任意元素分配相应的session
    +例如以商品 id 作为 key 为每个商品分配一个Session,以便于缓存和商品相关的数据,其相关API如下: +``` java +// 查询指定key的Session是否存在 +SaSessionCustomUtil.isExists("goods-10001"); + +// 获取指定key的Session,如果没有,则新建并返回 +SaSessionCustomUtil.getSessionById("goods-10001"); + +// 获取指定key的Session,如果没有,第二个参数决定是否新建并返回 +SaSessionCustomUtil.getSessionById("goods-10001", false); + +// 删除指定key的Session +SaSessionCustomUtil.deleteSessionById("goods-10001"); +``` + + +### 5、在 Session 上存取值 + +以上三种 Session 均为框架设计概念上的区分,实际上在获取它们时,返回的都是 SaSession 对象,你可以使用以下 API 在 SaSession 对象上存取值: + +``` java +// 写值 +session.set("name", "zhang"); + +// 写值 (只有在此key原本无值的时候才会写入) +session.setDefaultValue("name", "zhang"); + +// 取值 +session.get("name"); + +// 取值 (指定默认值) +session.get("name", ""); + +// 取值 (若无值则执行参数方法, 之后将结果保存到此键名下,并返回此结果 若有值则直接返回, 无需执行参数方法) +session.get("name", () -> { + return ...; + }); + +// ---------- 数据类型转换: ---------- +session.getInt("age"); // 取值 (转int类型) +session.getLong("age"); // 取值 (转long类型) +session.getString("name"); // 取值 (转String类型) +session.getDouble("result"); // 取值 (转double类型) +session.getFloat("result"); // 取值 (转float类型) +session.getModel("key", Student.class); // 取值 (指定转换类型) +session.getModel("key", Student.class, ); // 取值 (指定转换类型, 并指定值为Null时返回的默认值) + +// 是否含有某个key (返回 true 或 false) +session.has("key"); + +// 删值 +session.delete('name'); + +// 清空所有值 +session.clear(); + +// 获取此 Session 的所有key (返回Set) +session.keys(); +``` + + +### 6、其它操作 + +``` java +// 返回此 Session 的id +session.getId(); + +// 返回此 Session 的创建时间 (时间戳) +session.getCreateTime(); + +// 返回此 Session 会话上的底层数据对象(如果更新map里的值,请调用session.update()方法避免产生脏数据) +session.getDataMap(); + +// 将这个 Session 从持久库更新一下 +session.update(); + +// 注销此 Session 会话 (从持久库删除此Session) +session.logout(); +``` + + +### 7、避免与 HttpSession 混淆使用 +经常有同学会把 `SaSession` 与 `HttpSession` 进行混淆,例如: +``` java +@PostMapping("/resetPoints") +public void reset(HttpSession session) { + // 在 HttpSession 上写入一个值 + session.setAttribute("name", 66); + // 在 SaSession 进行取值 + System.out.println(StpUtil.getSession().get("name")); // 输出null +} +``` +**要点:** +1. `SaSession` 与 `HttpSession` 没有任何关系,在`HttpSession`上写入的值,在`SaSession`中无法取出 +2. `HttpSession`并未被框架接管,在使用Sa-Token时,请在任何情况下均使用`SaSession`,不要使用`HttpSession` + + +### 8、未登录场景下获取 Token-Session + +默认场景下,只有登录后才能通过 `StpUtil.getTokenSession()` 获取 `Token-Session`。 + +如果想要在未登录场景下获取 Token-Session ,有两种方法: + +- 方法一:将全局配置项 `tokenSessionCheckLogin` 改为 false,详见:[框架配置](/use/config?id=所有可配置项) +- 方法二:使用匿名 Token-Session + +``` java +// 获取当前 Token 的匿名 Token-Session (可在未登录情况下使用的 Token-Session) +StpUtil.getAnonTokenSession(); +``` + +注意点:如果前端没有提交 Token ,或者提交的 Token 是一个无效 Token 的话,框架将不会根据此 Token 创建 `Token-Session` 对象, +而是随机一个新的 Token 值来创建 `Token-Session` 对象,此 Token 值可以通过 `StpUtil.getTokenValue()` 获取到。 + + +--- + + + 本章代码示例:Sa-Token Session 会话 —— [ SaSessionController.java ] + +本章小练习:Sa-Token 基础 - Session 会话,章节测试 + diff --git a/sa-token-plugin/.gitignore b/sa-token-plugin/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-plugin/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-plugin/pom.xml b/sa-token-plugin/pom.xml index 8cdeb6ac0..3c77f3a29 100644 --- a/sa-token-plugin/pom.xml +++ b/sa-token-plugin/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-parent - 1.24.0 + ${revision} + ../pom.xml pom @@ -17,22 +18,58 @@ + + sa-token-jackson + sa-token-jackson3 + sa-token-fastjson + sa-token-fastjson2 + sa-token-snack3 + sa-token-snack4 + sa-token-hutool-timed-cache + sa-token-caffeine + sa-token-thymeleaf + sa-token-freemarker + sa-token-dubbo + sa-token-dubbo3 + sa-token-temp-jwt + sa-token-jwt + sa-token-sso + sa-token-oauth2 + sa-token-apikey + sa-token-sign + sa-token-redisson + sa-token-redisx + sa-token-serializer-features + sa-token-forest + sa-token-okhttps + + + sa-token-redis-template + sa-token-redis-template-jdk-serializer + sa-token-redis-jackson sa-token-alone-redis - sa-token-dao-redis - sa-token-dao-redis-jackson - sa-token-oauth2 - sa-token-quick-login + sa-token-alone-redis-by-spring-boot4 + sa-token-alone-redisson sa-token-spring-aop - sa-token-temp-jwt + sa-token-spring-el + sa-token-grpc + sa-token-quick-login + sa-token-redisson-spring-boot-starter - - - - org.springframework.boot - spring-boot-configuration-processor - 2.0.0.RELEASE - true - - + + + + + + cn.dev33 + sa-token-spring-boot2-dependencies + ${revision} + pom + import + + + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/pom.xml b/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/pom.xml new file mode 100644 index 000000000..1c2377689 --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-alone-redis-by-spring-boot4 + sa-token-alone-redis-by-spring-boot4 + sa-token-alone-redis for Spring Boot 4 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + cn.dev33 + sa-token-redis-template + true + + + cn.dev33 + sa-token-redis-template-jdk-serializer + true + + + + org.apache.commons + commons-pool2 + true + + + + + + + cn.dev33 + sa-token-spring-boot4-dependencies + ${revision} + pom + import + + + + + diff --git a/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java b/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java new file mode 100644 index 000000000..df4e3642f --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java @@ -0,0 +1,228 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.alone; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; +import cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate; +import cn.dev33.satoken.exception.SaTokenException; +import jakarta.annotation.PostConstruct; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.data.redis.connection.*; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 为 SaToken 单独设置 Redis 连接信息,使权限缓存与业务缓存分离 (springboot4 版本专用) + * + *

    + * 使用方式:在引入 sa-token redis 集成相关包的前提下,继续引入当前依赖

    + * 注意事项:目前本依赖仅对以下插件有 Redis 分离效果:
    + * sa-token-redis-template
    + * sa-token-redis-template-jdk-serializer
    + *

    + * + * + * @author click33 + * @since 1.45.0 + */ +@Configuration +public class SaAloneRedisInject { + + /** + * 配置信息的前缀 + */ + public static final String ALONE_PREFIX = "sa-token.alone-redis"; + + /** + * Sa-Token 持久层接口 + */ + private final SaTokenDao saTokenDao; + private final Environment environment; + + public SaAloneRedisInject(SaTokenDao saTokenDao, Environment environment) { + this.saTokenDao = saTokenDao; + this.environment = environment; + } + + /** + * 开始注入 + */ + @PostConstruct + public void init() { + try { + // 如果 saTokenDao 为空或者为默认实现,则不进行任何操作 + if(saTokenDao == null || saTokenDao instanceof SaTokenDaoDefaultImpl) { + return; + } + + // ------------------- 开始注入 + + // 获取cfg对象,解析开发者配置的 sa-token.alone-redis 相关信息 + DataRedisProperties cfg = Binder.get(environment).bind(ALONE_PREFIX, DataRedisProperties.class).get(); + + // 1. Redis配置 + RedisConfiguration redisAloneConfig; + String pattern = environment.getProperty(ALONE_PREFIX + ".pattern", "single"); + if (pattern.equals("single")) { + // 单体模式 + RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); + redisConfig.setHostName(cfg.getHost()); + redisConfig.setPort(cfg.getPort()); + redisConfig.setDatabase(cfg.getDatabase()); + redisConfig.setPassword(RedisPassword.of(cfg.getPassword())); + redisConfig.setUsername(cfg.getUsername()); + redisAloneConfig = redisConfig; + + } else if (pattern.equals("cluster")){ + // 普通集群模式 + RedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration(); + redisClusterConfig.setUsername(cfg.getUsername()); + redisClusterConfig.setPassword(RedisPassword.of(cfg.getPassword())); + + DataRedisProperties.Cluster cluster = cfg.getCluster(); + if (cluster == null || cluster.getNodes() == null) { + throw new SaTokenException("Alone-Redis 集群模式需要配置 cluster.nodes"); + } + List serverList = cluster.getNodes().stream().map(node -> { + String[] ipAndPort = node.split(":"); + return new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])); + }).collect(Collectors.toList()); + redisClusterConfig.setClusterNodes(serverList); + if (cluster.getMaxRedirects() != null) { + redisClusterConfig.setMaxRedirects(cluster.getMaxRedirects()); + } + redisAloneConfig = redisClusterConfig; + } else if (pattern.equals("sentinel")) { + // 哨兵集群模式 + RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); + redisSentinelConfiguration.setDatabase(cfg.getDatabase()); + redisSentinelConfiguration.setUsername(cfg.getUsername()); + redisSentinelConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); + + DataRedisProperties.Sentinel sentinel = cfg.getSentinel(); + if (sentinel == null || sentinel.getNodes() == null) { + throw new SaTokenException("Alone-Redis 哨兵模式需要配置 sentinel.nodes"); + } + redisSentinelConfiguration.setMaster(sentinel.getMaster()); + redisSentinelConfiguration.setSentinelPassword(sentinel.getPassword()); + List serverList = sentinel.getNodes().stream().map(node -> { + String[] ipAndPort = node.split(":"); + return new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])); + }).collect(Collectors.toList()); + redisSentinelConfiguration.setSentinels(serverList); + + redisAloneConfig = redisSentinelConfiguration; + } else if (pattern.equals("socket")) { + // socket 连接单体 Redis + RedisSocketConfiguration redisSocketConfiguration = new RedisSocketConfiguration(); + redisSocketConfiguration.setDatabase(cfg.getDatabase()); + redisSocketConfiguration.setUsername(cfg.getUsername()); + redisSocketConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); + String socket = environment.getProperty(ALONE_PREFIX + ".socket", ""); + redisSocketConfiguration.setSocket(socket); + + redisAloneConfig = redisSocketConfiguration; + } else if (pattern.equals("aws")) { + // AWS ElastiCache + // AWS Redis 远程主机地址: String hoseName = "****.***.****.****.cache.amazonaws.com"; + String hostName = cfg.getHost(); + int port = cfg.getPort(); + RedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(hostName, port); + redisStaticMasterReplicaConfiguration.setDatabase(cfg.getDatabase()); + redisStaticMasterReplicaConfiguration.setUsername(cfg.getUsername()); + redisStaticMasterReplicaConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); + + redisAloneConfig = redisStaticMasterReplicaConfiguration; + } else { + // 模式无法识别 + throw new SaTokenException("SaToken 无法识别 Alone-Redis 配置的模式: " + pattern); + } + + // 2. 连接池配置 + GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); + // pool配置 + DataRedisProperties.Lettuce lettuce = cfg.getLettuce(); + if(lettuce.getPool() != null) { + DataRedisProperties.Pool pool = cfg.getLettuce().getPool(); + // 连接池最大连接数 + poolConfig.setMaxTotal(pool.getMaxActive()); + // 连接池中的最大空闲连接 + poolConfig.setMaxIdle(pool.getMaxIdle()); + // 连接池中的最小空闲连接 + poolConfig.setMinIdle(pool.getMinIdle()); + // 连接池最大阻塞等待时间(使用负值表示没有限制) + poolConfig.setMaxWaitMillis(pool.getMaxWait().toMillis()); + } + LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder(); + // timeout + if(cfg.getTimeout() != null) { + builder.commandTimeout(cfg.getTimeout()); + } + // shutdownTimeout + builder.shutdownTimeout(lettuce.getShutdownTimeout()); + // 创建Factory对象 + LettuceClientConfiguration clientConfig = builder.poolConfig(poolConfig).build(); + LettuceConnectionFactory factory = new LettuceConnectionFactory(redisAloneConfig, clientConfig); + factory.afterPropertiesSet(); + + // 3. 开始初始化 SaTokenDao ,此处需要依次判断开发者引入的是哪个 redis 库 + + // 如果开发者引入的是:sa-token-redis-template-jdk-serializer 或 sa-token-redis-template + try { + Class.forName("cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer"); + SaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate) saTokenDao; + dao.isInit = false; + dao.init(factory); + return; + } catch (ClassNotFoundException ignored) { + } + try { + Class.forName("cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate"); + SaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate) saTokenDao; + dao.isInit = false; + dao.init(factory); + return; + } catch (ClassNotFoundException ignored) { + } + + // 至此,说明开发者一个 redis 插件也没引入,或者引入的 redis 插件不在 sa-token-alone-redis 的支持范围内 + throw new SaTokenException("未引入 sa-token-redis-xxx 相关插件,或引入的插件不在 Alone-Redis 支持范围内"); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 骗过编辑器,增加配置文件代码提示 + * @return 配置对象 + */ + @ConfigurationProperties(prefix = ALONE_PREFIX) + public DataRedisProperties getSaAloneRedisConfig() { + return new DataRedisProperties(); + } + +} diff --git a/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..74cac595a --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.dao.alone.SaAloneRedisInject diff --git a/sa-token-plugin/sa-token-alone-redis/.gitignore b/sa-token-plugin/sa-token-alone-redis/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-plugin/sa-token-alone-redis/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-plugin/sa-token-alone-redis/pom.xml b/sa-token-plugin/sa-token-alone-redis/pom.xml index b95cab337..483c76df9 100644 --- a/sa-token-plugin/sa-token-alone-redis/pom.xml +++ b/sa-token-plugin/sa-token-alone-redis/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-plugin - 1.24.0 + ${revision} + ../pom.xml jar @@ -19,21 +20,18 @@ cn.dev33 - sa-token-dao-redis - ${sa-token-version} + sa-token-redis-template true - cn.dev33 - sa-token-dao-redis-jackson - ${sa-token-version} + cn.dev33 + sa-token-redis-template-jdk-serializer true - + org.apache.commons commons-pool2 - 2.5.0 true diff --git a/sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java b/sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java index 69dc94c6e..ae587ecc9 100644 --- a/sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java +++ b/sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java @@ -1,5 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.alone; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; +import cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate; +import cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer; +import cn.dev33.satoken.exception.SaTokenException; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; @@ -9,20 +29,27 @@ import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.data.redis.connection.RedisPassword; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.*; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; -import cn.dev33.satoken.dao.SaTokenDao; -import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; -import cn.dev33.satoken.dao.SaTokenDaoRedis; -import cn.dev33.satoken.dao.SaTokenDaoRedisJackson; +import java.util.List; +import java.util.stream.Collectors; /** - * 为 SaTokenDao 单独设置Redis连接信息 - * @author kong + * 为 SaToken 单独设置 Redis 连接信息,使权限缓存与业务缓存分离 + * + *

    + * 使用方式:在引入 sa-token redis 集成相关包的前提下,继续引入当前依赖

    + * 注意事项:目前本依赖仅对以下插件有 Redis 分离效果:
    + * sa-token-redis-template
    + * sa-token-redis-template-jdk-serializer
    + *

    + * + * + * @author click33 + * @since 1.21.0 */ @Configuration public class SaAloneRedisInject implements EnvironmentAware{ @@ -44,27 +71,113 @@ public class SaAloneRedisInject implements EnvironmentAware{ @Override public void setEnvironment(Environment environment) { try { - // 如果为空或者默认实现,则不进行任何操作 + // 如果 saTokenDao 为空或者为默认实现,则不进行任何操作 if(saTokenDao == null || saTokenDao instanceof SaTokenDaoDefaultImpl) { return; } - // 如果配置文件不包含相关配置,则不进行任何操作 - if(environment.getProperty(ALONE_PREFIX + ".host") == null) { - return; - } // ------------------- 开始注入 - // 获取cfg对象 + // 获取cfg对象,解析开发者配置的 sa-token.alone-redis 相关信息 RedisProperties cfg = Binder.get(environment).bind(ALONE_PREFIX, RedisProperties.class).get(); - - // 1. Redis配置 - RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); - redisConfig.setHostName(cfg.getHost()); - redisConfig.setPort(cfg.getPort()); - redisConfig.setDatabase(cfg.getDatabase()); - redisConfig.setPassword(RedisPassword.of(cfg.getPassword())); - + + // 1. Redis配置 + RedisConfiguration redisAloneConfig; + String pattern = environment.getProperty(ALONE_PREFIX + ".pattern", "single"); + if (pattern.equals("single")) { + // 单体模式 + RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); + redisConfig.setHostName(cfg.getHost()); + redisConfig.setPort(cfg.getPort()); + redisConfig.setDatabase(cfg.getDatabase()); + redisConfig.setPassword(RedisPassword.of(cfg.getPassword())); + redisConfig.setDatabase(cfg.getDatabase()); + // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 + try { + redisConfig.setUsername(cfg.getUsername()); + } catch (NoSuchMethodError e){ + System.err.println(e.getMessage()); + } + redisAloneConfig = redisConfig; + + } else if (pattern.equals("cluster")){ + // 普通集群模式 + RedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration(); + // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 + try { + redisClusterConfig.setUsername(cfg.getUsername()); + } catch (NoSuchMethodError e){ + System.err.println(e.getMessage()); + } + redisClusterConfig.setPassword(RedisPassword.of(cfg.getPassword())); + + RedisProperties.Cluster cluster = cfg.getCluster(); + List serverList = cluster.getNodes().stream().map(node -> { + String[] ipAndPort = node.split(":"); + return new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])); + }).collect(Collectors.toList()); + redisClusterConfig.setClusterNodes(serverList); + redisClusterConfig.setMaxRedirects(cluster.getMaxRedirects()); + + redisAloneConfig = redisClusterConfig; + } else if (pattern.equals("sentinel")) { + // 哨兵集群模式 + RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); + redisSentinelConfiguration.setDatabase(cfg.getDatabase()); + // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 + try { + redisSentinelConfiguration.setUsername(cfg.getUsername()); + } catch (NoSuchMethodError e){ + System.err.println(e.getMessage()); + } + redisSentinelConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); + + RedisProperties.Sentinel sentinel = cfg.getSentinel(); + redisSentinelConfiguration.setMaster(sentinel.getMaster()); + redisSentinelConfiguration.setSentinelPassword(sentinel.getPassword()); + List serverList = sentinel.getNodes().stream().map(node -> { + String[] ipAndPort = node.split(":"); + return new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])); + }).collect(Collectors.toList()); + redisSentinelConfiguration.setSentinels(serverList); + + redisAloneConfig = redisSentinelConfiguration; + } else if (pattern.equals("socket")) { + // socket 连接单体 Redis + RedisSocketConfiguration redisSocketConfiguration = new RedisSocketConfiguration(); + redisSocketConfiguration.setDatabase(cfg.getDatabase()); + // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 + try { + redisSocketConfiguration.setUsername(cfg.getUsername()); + } catch (NoSuchMethodError e){ + System.err.println(e.getMessage()); + } + redisSocketConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); + String socket = environment.getProperty(ALONE_PREFIX + ".socket", ""); + redisSocketConfiguration.setSocket(socket); + + redisAloneConfig = redisSocketConfiguration; + } else if (pattern.equals("aws")) { + // AWS ElastiCache + // AWS Redis 远程主机地址: String hoseName = "****.***.****.****.cache.amazonaws.com"; + String hostName = cfg.getHost(); + int port = cfg.getPort(); + RedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(hostName, port); + redisStaticMasterReplicaConfiguration.setDatabase(cfg.getDatabase()); + // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 + try { + redisStaticMasterReplicaConfiguration.setUsername(cfg.getUsername()); + } catch (NoSuchMethodError e){ + System.err.println(e.getMessage()); + } + redisStaticMasterReplicaConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); + + redisAloneConfig = redisStaticMasterReplicaConfiguration; + } else { + // 模式无法识别 + throw new SaTokenException("SaToken 无法识别 Alone-Redis 配置的模式: " + pattern); + } + // 2. 连接池配置 GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); // pool配置 @@ -89,30 +202,35 @@ public void setEnvironment(Environment environment) { if(lettuce.getShutdownTimeout() != null) { builder.shutdownTimeout(lettuce.getShutdownTimeout()); } - // 创建Factory对象 + // 创建Factory对象 LettuceClientConfiguration clientConfig = builder.poolConfig(poolConfig).build(); - LettuceConnectionFactory factory = new LettuceConnectionFactory(redisConfig, clientConfig); + LettuceConnectionFactory factory = new LettuceConnectionFactory(redisAloneConfig, clientConfig); factory.afterPropertiesSet(); - // 3. 开始初始化 SaTokenDao - // 如果是SaTokenDaoRedis + // 3. 开始初始化 SaTokenDao ,此处需要依次判断开发者引入的是哪个 redis 库 + + // 如果开发者引入的是:sa-token-redis-template-jdk-serializer try { - Class.forName("cn.dev33.satoken.dao.SaTokenDaoRedis"); - SaTokenDaoRedis dao = (SaTokenDaoRedis)saTokenDao; + Class.forName("cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer"); + SaTokenDaoForRedisTemplateUseJdkSerializer dao = (SaTokenDaoForRedisTemplateUseJdkSerializer)saTokenDao; dao.isInit = false; dao.init(factory); return; - } catch (ClassNotFoundException e) { + } catch (ClassNotFoundException ignored) { } - // 如果是SaTokenDaoRedisJackson + // 如果开发者引入的是:sa-token-redis-template try { - Class.forName("cn.dev33.satoken.dao.SaTokenDaoRedisJackson"); - SaTokenDaoRedisJackson dao = (SaTokenDaoRedisJackson)saTokenDao; + Class.forName("cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate"); + SaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate)saTokenDao; dao.isInit = false; dao.init(factory); return; - } catch (ClassNotFoundException e) { + } catch (ClassNotFoundException ignored) { } + + // 至此,说明开发者一个 redis 插件也没引入,或者引入的 redis 插件不在 sa-token-alone-redis 的支持范围内 + throw new SaTokenException("未引入 sa-token-redis-xxx 相关插件,或引入的插件不在 Alone-Redis 支持范围内"); + } catch (Exception e) { e.printStackTrace(); } diff --git a/sa-token-plugin/sa-token-alone-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-alone-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..0d10a3990 --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.dao.alone.SaAloneRedisInject \ No newline at end of file diff --git a/sa-token-plugin/sa-token-alone-redisson/pom.xml b/sa-token-plugin/sa-token-alone-redisson/pom.xml new file mode 100644 index 000000000..c6265b148 --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redisson/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-alone-redisson + sa-token-alone-redisson + sa-token-alone-redisson + + + + + cn.dev33 + sa-token-redisson + + + + + org.springframework.boot + spring-boot-starter + true + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + diff --git a/sa-token-plugin/sa-token-alone-redisson/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedissonProperties.java b/sa-token-plugin/sa-token-alone-redisson/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedissonProperties.java new file mode 100644 index 000000000..d62c0e42c --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redisson/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedissonProperties.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.alone; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Sa-Token 独立 Redisson 连接配置(Redisson 原生 yaml) + * + * @author click33 + * @since 1.45.0 + */ +@ConfigurationProperties(prefix = SaAloneRedissonProperties.PREFIX) +public class SaAloneRedissonProperties { + + public static final String PREFIX = "sa-token.alone-redisson"; + + /** + * Redisson 配置文件,例如 classpath:sa-redisson.yml + */ + private String file; + + /** + * Redisson 原生 yaml 配置内容(与 file 二选一,优先 config) + */ + private String config; + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public String getConfig() { + return config; + } + + public void setConfig(String config) { + this.config = config; + } + +} diff --git a/sa-token-plugin/sa-token-alone-redisson/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedissonRegister.java b/sa-token-plugin/sa-token-alone-redisson/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedissonRegister.java new file mode 100644 index 000000000..b30ac826f --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redisson/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedissonRegister.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao.alone; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisson; +import cn.dev33.satoken.exception.SaTokenException; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + +import java.io.InputStream; + +/** + * 为 Sa-Token 单独创建 RedissonClient,使权限缓存与业务缓存分离 + * + *

    + * 使用方式:引入本插件并配置 sa-token.alone-redisson.config 或 sa-token.alone-redisson.file。 + * 无需再引入 sa-token-redisson / sa-token-redisson-spring-boot-starter。 + *

    + * + * @author click33 + * @since 1.45.0 + */ +@Configuration +@EnableConfigurationProperties(SaAloneRedissonProperties.class) +public class SaAloneRedissonRegister implements DisposableBean { + + private RedissonClient aloneClient; + + /** + * 使用独立连接注册 SaTokenDao,覆盖业务 RedissonClient 对应的 Dao + */ + @Bean + @Primary + public SaTokenDao saTokenDaoForAloneRedisson(SaAloneRedissonProperties properties) { + this.aloneClient = Redisson.create(buildConfig(properties)); + return new SaTokenDaoForRedisson(aloneClient); + } + + @Override + public void destroy() { + if (aloneClient != null && !aloneClient.isShuttingDown()) { + aloneClient.shutdown(); + } + } + + /** + * 解析 Redisson 原生 yaml:优先 config,其次 file + */ + public static Config buildConfig(SaAloneRedissonProperties properties) { + try { + Config config; + if (StringUtils.hasText(properties.getConfig())) { + config = Config.fromYAML(properties.getConfig()); + } else if (StringUtils.hasText(properties.getFile())) { + try (InputStream in = openFile(properties.getFile())) { + config = Config.fromYAML(in); + } + } else { + throw new SaTokenException("请配置 sa-token.alone-redisson.config 或 sa-token.alone-redisson.file"); + } + return config; + } catch (SaTokenException e) { + throw e; + } catch (Exception e) { + throw new SaTokenException("解析 sa-token.alone-redisson 配置失败", e); + } + } + + private static InputStream openFile(String file) throws Exception { + String path = file.trim(); + Resource resource; + if (path.startsWith("classpath:")) { + resource = new ClassPathResource(path.substring("classpath:".length())); + } else if (path.startsWith("file:")) { + resource = new FileSystemResource(path.substring("file:".length())); + } else { + resource = new ClassPathResource(path); + } + if (!resource.exists()) { + throw new SaTokenException("未找到 Redisson 配置文件: " + file); + } + return resource.getInputStream(); + } + +} diff --git a/sa-token-plugin/sa-token-alone-redisson/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-alone-redisson/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..2286710c3 --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redisson/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.alone.SaAloneRedissonRegister diff --git a/sa-token-plugin/sa-token-alone-redisson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-alone-redisson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..1d7ffd24f --- /dev/null +++ b/sa-token-plugin/sa-token-alone-redisson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.dao.alone.SaAloneRedissonRegister diff --git a/sa-token-plugin/sa-token-apikey/pom.xml b/sa-token-plugin/sa-token-apikey/pom.xml new file mode 100644 index 000000000..efe65396a --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + sa-token-plugin + cn.dev33 + ${revision} + ../pom.xml + + jar + + sa-token-apikey + sa-token-apikey + sa-token-apikey + + + + cn.dev33 + sa-token-core + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/SaApiKeyManager.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/SaApiKeyManager.java new file mode 100644 index 000000000..b5b398472 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/SaApiKeyManager.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey; + +import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; +import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoaderDefaultImpl; +import cn.dev33.satoken.apikey.config.SaApiKeyConfig; +import cn.dev33.satoken.apikey.template.SaApiKeyTemplate; +import cn.dev33.satoken.listener.SaTokenEventCenter; + +/** + * 管理 Sa-Token API Key 所有全局组件 + * + * @author click33 + * @since 1.43.0 + */ +public class SaApiKeyManager { + + /** + * API Key 配置 Bean + */ + private static volatile SaApiKeyConfig config; + public static SaApiKeyConfig getConfig() { + if (config == null) { + // 初始化默认值 + synchronized (SaApiKeyManager.class) { + if (config == null) { + setConfig(new SaApiKeyConfig()); + } + } + } + return config; + } + public static void setConfig(SaApiKeyConfig config) { + SaApiKeyManager.config = config; + } + + /** + * ApiKey 数据加载器 + */ + private volatile static SaApiKeyDataLoader apiKeyDataLoader; + public static void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) { + SaApiKeyManager.apiKeyDataLoader = apiKeyDataLoader; + SaTokenEventCenter.doRegisterComponent("SaApiKeyDataLoader", apiKeyDataLoader); + } + public static SaApiKeyDataLoader getSaApiKeyDataLoader() { + if (apiKeyDataLoader == null) { + synchronized (SaApiKeyManager.class) { + if (apiKeyDataLoader == null) { + SaApiKeyManager.apiKeyDataLoader = new SaApiKeyDataLoaderDefaultImpl(); + } + } + } + return apiKeyDataLoader; + } + + /** + * ApiKey 操作类 + */ + private volatile static SaApiKeyTemplate apiKeyTemplate; + public static void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) { + SaApiKeyManager.apiKeyTemplate = apiKeyTemplate; + SaTokenEventCenter.doRegisterComponent("SaApiKeyTemplate", apiKeyTemplate); + } + public static SaApiKeyTemplate getSaApiKeyTemplate() { + if (apiKeyTemplate == null) { + synchronized (SaApiKeyManager.class) { + if (apiKeyTemplate == null) { + SaApiKeyManager.apiKeyTemplate = new SaApiKeyTemplate(); + } + } + } + return apiKeyTemplate; + } + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/SaCheckApiKey.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/SaCheckApiKey.java new file mode 100644 index 000000000..bf977d1a1 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/SaCheckApiKey.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.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; + +/** + * API Key 校验:指定请求中必须包含有效的 ApiKey ,并且包含指定的 scope + * + *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.42.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD,ElementType.TYPE}) +public @interface SaCheckApiKey { + + /** + * 指定 API key 必须包含的权限 [ 数组 ] + * + * @return / + */ + String [] scope() default {}; + + /** + * 验证模式:AND | OR,默认AND + * + * @return / + */ + SaMode mode() default SaMode.AND; + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/handle/SaCheckApiKeyHandler.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/handle/SaCheckApiKeyHandler.java new file mode 100644 index 000000000..892558645 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/handle/SaCheckApiKeyHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.annotation.handle; + +import cn.dev33.satoken.annotation.SaMode; +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.apikey.annotation.SaCheckApiKey; +import cn.dev33.satoken.apikey.template.SaApiKeyUtil; +import cn.dev33.satoken.context.SaHolder; + +import java.lang.reflect.AnnotatedElement; + +/** + * 注解 SaCheckApiKey 的处理器 + * + * @author click33 + * @since 1.42.0 + */ +public class SaCheckApiKeyHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckApiKey.class; + } + + @Override + public void checkMethod(SaCheckApiKey at, AnnotatedElement element) { + _checkMethod(at.scope(), at.mode()); + } + + public static void _checkMethod(String[] scope, SaMode mode) { + String apiKey = SaApiKeyUtil.readApiKeyValue(SaHolder.getRequest()); + if(mode == SaMode.AND) { + SaApiKeyUtil.checkApiKeyScope(apiKey, scope); + } else { + SaApiKeyUtil.checkApiKeyScopeOr(apiKey, scope); + } + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/config/SaApiKeyConfig.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/config/SaApiKeyConfig.java new file mode 100644 index 000000000..9d49add82 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/config/SaApiKeyConfig.java @@ -0,0 +1,110 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.config; + +/** + * Sa-Token API Key 相关配置 + * + * @author click33 + * @since 1.42.0 + */ +public class SaApiKeyConfig { + + /** + * API Key 前缀 + */ + private String prefix = "AK-"; + + /** + * API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) + */ + private long timeout = 2592000; + + /** + * 框架是否记录索引信息 + */ + private Boolean isRecordIndex = true; + + /** + * 获取 API Key 前缀 + * + * @return / + */ + public String getPrefix() { + return this.prefix; + } + + /** + * 设置 API Key 前缀 + * + * @param prefix / + * @return 对象自身 + */ + public SaApiKeyConfig setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + /** + * 获取 API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) + * + * @return / + */ + public long getTimeout() { + return this.timeout; + } + + /** + * 设置 API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) + * + * @param timeout / + * @return 对象自身 + */ + public SaApiKeyConfig setTimeout(long timeout) { + this.timeout = timeout; + return this; + } + + /** + * 获取 框架是否保存索引信息 + * + * @return / + */ + public Boolean getIsRecordIndex() { + return this.isRecordIndex; + } + + /** + * 设置 框架是否保存索引信息 + * + * @param isRecordIndex / + * @return 对象自身 + */ + public SaApiKeyConfig setIsRecordIndex(Boolean isRecordIndex) { + this.isRecordIndex = isRecordIndex; + return this; + } + + @Override + public String toString() { + return "SaApiKeyConfig{" + + "prefix='" + prefix + '\'' + + ", timeout=" + timeout + + ", isRecordIndex=" + isRecordIndex + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/error/SaApiKeyErrorCode.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/error/SaApiKeyErrorCode.java new file mode 100644 index 000000000..2841815a8 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/error/SaApiKeyErrorCode.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.error; + +/** + * 定义 sa-token-apikey 模块所有异常细分状态码 + * + * @author click33 + * @since 1.43.0 + */ +public interface SaApiKeyErrorCode { + + /** 无效 API Key */ + int CODE_12301 = 12301; + + /** API Key 已过期 */ + int CODE_12302 = 12302; + + /** API Key 已被禁用 */ + int CODE_12303 = 12303; + + /** API Key 字段自检未通过 */ + int CODE_12304 = 12304; + + /** 未开启索引记录功能却调用了相关 API */ + int CODE_12305 = 12305; + + /** API Key 不具有指定 Scope */ + int CODE_12311 = 12311; + + /** API Key 不属于指定用户 */ + int CODE_12312 = 12312; + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyException.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyException.java new file mode 100644 index 000000000..dc0969f13 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyException.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.exception; + +import cn.dev33.satoken.exception.SaTokenException; + +/** + * 一个异常:代表 ApiKey 相关错误 + * + * @author click33 + * @since 1.42.0 + */ +public class ApiKeyException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 ApiKey 相关错误 + * @param cause 根异常原因 + */ + public ApiKeyException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 ApiKey 相关错误 + * @param message 异常描述 + */ + public ApiKeyException(String message) { + super(message); + } + + /** + * 具体引起异常的 ApiKey 值 + */ + public String apiKey; + + public String getApiKey() { + return apiKey; + } + + public ApiKeyException setApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new ApiKeyException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyScopeException.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyScopeException.java new file mode 100644 index 000000000..6d5841797 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyScopeException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.exception; + +/** + * 一个异常:代表 ApiKey Scope 相关错误 + * + * @author click33 + * @since 1.42.0 + */ +public class ApiKeyScopeException extends ApiKeyException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 ApiKey Scope 相关错误 + * @param cause 根异常原因 + */ + public ApiKeyScopeException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 ApiKey Scope 相关错误 + * @param message 异常描述 + */ + public ApiKeyScopeException(String message) { + super(message); + } + + /** + * 具体引起异常的 ApiKey 值 + */ + public String apiKey; + + /** + * 具体引起异常的 scope 值 + */ + public String scope; + + public String getApiKey() { + return apiKey; + } + + public ApiKeyScopeException setApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public String getScope() { + return scope; + } + + public ApiKeyScopeException setScope(String scope) { + this.scope = scope; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new ApiKeyScopeException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoader.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoader.java new file mode 100644 index 000000000..05d3acaf5 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoader.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.loader; + +import cn.dev33.satoken.apikey.SaApiKeyManager; +import cn.dev33.satoken.apikey.model.ApiKeyModel; + +/** + * ApiKey 数据加载器 + * + * @author click33 + * @since 1.42.0 + */ +public interface SaApiKeyDataLoader { + + /** + * 获取:框架是否保存索引信息 + * + * @return / + */ + default Boolean getIsRecordIndex() { + return SaApiKeyManager.getConfig().getIsRecordIndex(); + } + + /** + * 根据 apiKey 从数据库获取 ApiKeyModel 信息 (实现此方法无需为数据做缓存处理,框架内部已包含缓存逻辑) + * + * @param namespace / + * @param apiKey / + * @return ApiKeyModel + */ + default ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) { + return null; + } + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoaderDefaultImpl.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoaderDefaultImpl.java new file mode 100644 index 000000000..df62780d2 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoaderDefaultImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.loader; + +/** + * ApiKey 数据加载器 默认实现类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaApiKeyDataLoaderDefaultImpl implements SaApiKeyDataLoader { + + // be empty of + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/model/ApiKeyModel.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/model/ApiKeyModel.java new file mode 100644 index 000000000..93b6a6c2a --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/model/ApiKeyModel.java @@ -0,0 +1,380 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.model; + +import cn.dev33.satoken.apikey.error.SaApiKeyErrorCode; +import cn.dev33.satoken.apikey.exception.ApiKeyException; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.io.Serializable; +import java.util.*; + +/** + * Model: API Key + * + * @author click33 + * @since 1.41.0 + */ +public class ApiKeyModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 名称 + */ + private String title; + + /** + * 介绍 + */ + private String intro; + + /** + * ApiKey 值 + */ + private String apiKey; + + /** + * 账号 id + */ + private Object loginId; + + /** + * ApiKey 创建时间,13位时间戳 + */ + private long createTime; + + /** + * ApiKey 到期时间,13位时间戳 (-1=永不过期) + */ + private long expiresTime; + + /** + * 是否有效 (true=生效, false=禁用) + */ + private Boolean isValid = true; + + /** + * 授权范围 + */ + private List scopes = new ArrayList<>(); + + /** + * 扩展数据 + */ + private Map extraData; + + /** + * 构造函数 + */ + public ApiKeyModel() { + this.createTime = System.currentTimeMillis(); + } + + + // method + + /** + * 添加 Scope + * @param scope / + * @return / + */ + public ApiKeyModel addScope(String ...scope) { + if (this.scopes == null) { + this.scopes = new ArrayList<>(); + } + this.scopes.addAll(Arrays.asList(scope)); + return this; + } + + /** + * 添加 扩展数据 + * @param key / + * @param value / + * @return / + */ + public ApiKeyModel addExtra(String key, Object value) { + if (this.extraData == null) { + this.extraData = new LinkedHashMap<>(); + } + this.extraData.put(key, value); + return this; + } + + /** + * 查询扩展数据 + */ + public Object getExtra(String key) { + if (this.extraData == null) { + return null; + } + return this.extraData.get(key); + } + + /** + * 删除扩展数据 + */ + public Object removeExtra(String key) { + if (this.extraData == null) { + return null; + } + return this.extraData.remove(key); + } + + /** + * 数据自检,判断是否可以保存入库 + */ + public void checkByCanSaved() { + if (SaFoxUtil.isEmpty(this.apiKey)) { + throw new ApiKeyException("ApiKey 值不可为空").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); + } + if (this.loginId == null) { + throw new ApiKeyException("无效 ApiKey: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); + } + if (this.createTime == 0) { + throw new ApiKeyException("请指定 createTime 创建时间").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); + } + if (this.expiresTime == 0) { + throw new ApiKeyException("请指定 expiresTime 过期时间").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); + } + if (this.isValid == null) { + throw new ApiKeyException("请指定 isValid 是否生效").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); + } + } + + /** + * 获取:此 ApiKey 的剩余有效期(秒), -1=永不过期 + * @return / + */ + public long expiresIn() { + if (expiresTime == SaTokenDao.NEVER_EXPIRE) { + return SaTokenDao.NEVER_EXPIRE; + } + long s = (expiresTime - System.currentTimeMillis()) / 1000; + return s < 1 ? -2 : s; + } + + /** + * 判断:此 ApiKey 是否已超时 + * @return / + */ + public boolean timeExpired() { + if (expiresTime == SaTokenDao.NEVER_EXPIRE) { + return false; + } + return System.currentTimeMillis() > expiresTime; + } + + + // get and set + + /** + * 获取 名称 + * + * @return title 名称 + */ + public String getTitle() { + return this.title; + } + + /** + * 设置 名称 + * + * @param title 名称 + * @return 对象自身 + */ + public ApiKeyModel setTitle(String title) { + this.title = title; + return this; + } + + /** + * 获取 介绍 + * + * @return intro 介绍 + */ + public String getIntro() { + return this.intro; + } + + /** + * 设置 介绍 + * + * @param intro 介绍 + * @return 对象自身 + */ + public ApiKeyModel setIntro(String intro) { + this.intro = intro; + return this; + } + + /** + * 获取 ApiKey 值 + * + * @return apiKey ApiKey 值 + */ + public String getApiKey() { + return this.apiKey; + } + + /** + * 设置 ApiKey 值 + * + * @param apiKey ApiKey 值 + * @return 对象自身 + */ + public ApiKeyModel setApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + /** + * 获取 账号 id + * + * @return loginId 账号 id + */ + public Object getLoginId() { + return this.loginId; + } + + /** + * 设置 账号 id + * + * @param loginId 账号 id + * @return 对象自身 + */ + public ApiKeyModel setLoginId(Object loginId) { + this.loginId = loginId; + return this; + } + + /** + * 获取 ApiKey 创建时间,13位时间戳 + * + * @return createTime ApiKey 创建时间,13位时间戳 + */ + public long getCreateTime() { + return this.createTime; + } + + /** + * 设置 ApiKey 创建时间,13位时间戳 + * + * @param createTime ApiKey 创建时间,13位时间戳 + * @return 对象自身 + */ + public ApiKeyModel setCreateTime(long createTime) { + this.createTime = createTime; + return this; + } + + /** + * 获取 ApiKey 到期时间,13位时间戳 (-1=永不过期) + * + * @return expiresTime ApiKey 到期时间,13位时间戳 (-1=永不过期) + */ + public long getExpiresTime() { + return this.expiresTime; + } + + /** + * 设置 ApiKey 到期时间,13位时间戳 (-1=永不过期) + * + * @param expiresTime ApiKey 到期时间,13位时间戳 (-1=永不过期) + * @return 对象自身 + */ + public ApiKeyModel setExpiresTime(long expiresTime) { + this.expiresTime = expiresTime; + return this; + } + + /** + * 获取 是否有效 (true=生效 false=禁用) + * + * @return / + */ + public Boolean getIsValid() { + return this.isValid; + } + + /** + * 设置 是否有效 (true=生效 false=禁用) + * + * @param isValid / + * @return 对象自身 + */ + public ApiKeyModel setIsValid(Boolean isValid) { + this.isValid = isValid; + return this; + } + + /** + * 获取 授权范围 + * + * @return scopes 授权范围 + */ + public List getScopes() { + return this.scopes; + } + + /** + * 设置 授权范围 + * + * @param scopes 授权范围 + * @return 对象自身 + */ + public ApiKeyModel setScopes(List scopes) { + this.scopes = scopes; + return this; + } + + /** + * 获取 扩展数据 + * + * @return extraData 扩展数据 + */ + public Map getExtraData() { + return this.extraData; + } + + /** + * 设置 扩展数据 + * + * @param extraData 扩展数据 + * @return 对象自身 + */ + public ApiKeyModel setExtraData(Map extraData) { + this.extraData = extraData; + return this; + } + + + @Override + public String toString() { + return "ApiKeyModel{" + + "title='" + title + + ", intro='" + intro + + ", apiKey='" + apiKey + + ", loginId=" + loginId + + ", createTime=" + createTime + + ", expiresTime=" + expiresTime + + ", isValid=" + isValid + + ", scopes=" + scopes + + ", extraData=" + extraData + + '}'; + } +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyTemplate.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyTemplate.java new file mode 100644 index 000000000..fd91cc718 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyTemplate.java @@ -0,0 +1,560 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.template; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.apikey.SaApiKeyManager; +import cn.dev33.satoken.apikey.error.SaApiKeyErrorCode; +import cn.dev33.satoken.apikey.exception.ApiKeyException; +import cn.dev33.satoken.apikey.exception.ApiKeyScopeException; +import cn.dev33.satoken.apikey.model.ApiKeyModel; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.raw.SaRawSessionDelegator; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * API Key 操作类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaApiKeyTemplate { + + /** + *默认命名空间 + */ + public static final String DEFAULT_NAMESPACE = "apikey"; + + /** + * 命名空间 + */ + public String namespace; + + /** + * Raw Session 读写委托 + */ + public SaRawSessionDelegator rawSessionDelegator; + + /** + * 在 raw-session 中的保存索引列表使用的 key + */ + public static final String API_KEY_LIST = "__HD_API_KEY_LIST"; + + public SaApiKeyTemplate(){ + this(DEFAULT_NAMESPACE); + } + + /** + * 实例化 + * @param namespace 命名空间,用于多实例隔离 + */ + public SaApiKeyTemplate(String namespace){ + if(SaFoxUtil.isEmpty(namespace)) { + throw new ApiKeyException("namespace 不能为空"); + } + this.namespace = namespace; + this.rawSessionDelegator = new SaRawSessionDelegator(namespace); + } + + // ------------------- ApiKey + + /** + * 根据 apiKey 从 Cache 获取 ApiKeyModel 信息 + * @param apiKey / + * @return / + */ + public ApiKeyModel getApiKeyModelFromCache(String apiKey) { + return getSaTokenDao().getObject(splicingApiKeySaveKey(apiKey), ApiKeyModel.class); + } + + /** + * 根据 apiKey 从 Database 获取 ApiKeyModel 信息 + * @param apiKey / + * @return / + */ + public ApiKeyModel getApiKeyModelFromDatabase(String apiKey) { + return SaApiKeyManager.getSaApiKeyDataLoader().getApiKeyModelFromDatabase(namespace, apiKey); + } + + /** + * 获取 ApiKeyModel,无效的 ApiKey 会返回 null + * @param apiKey / + * @return / + */ + public ApiKeyModel getApiKey(String apiKey) { + if(apiKey == null) { + return null; + } + // 先从缓存中获取,缓存中找不到就尝试从数据库获取 + ApiKeyModel apiKeyModel = getApiKeyModelFromCache(apiKey); + if(apiKeyModel == null) { + apiKeyModel = getApiKeyModelFromDatabase(apiKey); + saveApiKey(apiKeyModel); + } + return apiKeyModel; + } + + /** + * 校验 ApiKey,成功返回 ApiKeyModel,失败则抛出异常 + * @param apiKey / + * @return / + */ + public ApiKeyModel checkApiKey(String apiKey) { + ApiKeyModel ak = getApiKey(apiKey); + if(ak == null) { + throw new ApiKeyException("无效 API Key: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12301); + } + if(ak.timeExpired()) { + throw new ApiKeyException("API Key 已过期: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12302); + } + if(! ak.getIsValid()) { + throw new ApiKeyException("API Key 已被禁用: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12303); + } + return ak; + } + + /** + * 持久化:ApiKeyModel + * @param ak / + */ + public void saveApiKey(ApiKeyModel ak) { + if(ak == null) { + return; + } + // 数据自检 + ak.checkByCanSaved(); + + // 保存 ApiKeyModel + String saveKey = splicingApiKeySaveKey(ak.getApiKey()); + if(ak.timeExpired()) { + getSaTokenDao().deleteObject(saveKey); + } else { + getSaTokenDao().setObject(saveKey, ak, ak.expiresIn()); + } + + // 记录索引 + if (getIsRecordIndex()) { + // 添加索引 + SaSession session = rawSessionDelegator.getSessionById(ak.getLoginId()); + ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); + if(! apiKeyList.contains(ak.getApiKey())) { + apiKeyList.add(ak.getApiKey()); + session.set(API_KEY_LIST, apiKeyList); + } + + // 调整 ttl + adjustIndex(ak.getLoginId(), session); + } + + } + + /** + * 获取 ApiKey 所代表的 LoginId + * @param apiKey ApiKey + * @return LoginId + */ + public Object getLoginIdByApiKey(String apiKey) { + return checkApiKey(apiKey).getLoginId(); + } + + /** + * 删除 ApiKey + * @param apiKey ApiKey + */ + public void deleteApiKey(String apiKey) { + // 删 ApiKeyModel + ApiKeyModel ak = getApiKeyModelFromCache(apiKey); + if(ak == null) { + return; + } + getSaTokenDao().deleteObject(splicingApiKeySaveKey(apiKey)); + + // 删索引 + if(getIsRecordIndex()) { + // RawSession 中不存在,提前退出 + SaSession session = rawSessionDelegator.getSessionById(ak.getLoginId(), false); + if(session == null) { + return; + } + // 索引无记录,提前退出 + ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); + if(! apiKeyList.contains(apiKey)) { + return; + } + + // 如果只有一个 ApiKey,则整个 RawSession 删掉 + if (apiKeyList.size() == 1) { + rawSessionDelegator.deleteSessionById(ak.getLoginId()); + } else { + // 否则移除此 ApiKey 并保存 + apiKeyList.remove(apiKey); + session.set(API_KEY_LIST, apiKeyList); + } + } + } + + /** + * 删除指定 loginId 的所有 ApiKey + * @param loginId / + */ + public void deleteApiKeyByLoginId(Object loginId) { + // 先判断是否开启索引 + if(! getIsRecordIndex()) { + SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 deleteApiKeyByLoginId 操作"); + return; + } + + // RawSession 中不存在,提前退出 + SaSession session = rawSessionDelegator.getSessionById(loginId, false); + if(session == null) { + return; + } + + // 先删 ApiKeyModel + ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); + for (String apiKey : apiKeyList) { + getSaTokenDao().deleteObject(splicingApiKeySaveKey(apiKey)); + } + + // 再删索引 + rawSessionDelegator.deleteSessionById(loginId); + } + + // ------- 创建 + + /** + * 创建一个 ApiKeyModel 对象 + * + * @return / + */ + public ApiKeyModel createApiKeyModel() { + String apiKey = SaStrategy.instance.generateUniqueToken.execute( + "API Key", + SaManager.getConfig().getMaxTryTimes(), + this::randomApiKeyValue, + _apiKey -> getApiKey(_apiKey) == null + ); + return new ApiKeyModel().setApiKey(apiKey); + } + + /** + * 创建一个 ApiKeyModel 对象 + * + * @return / + */ + public ApiKeyModel createApiKeyModel(Object loginId) { + long timeout = SaApiKeyManager.getConfig().getTimeout(); + long expiresTime = (timeout == SaTokenDao.NEVER_EXPIRE) ? SaTokenDao.NEVER_EXPIRE : System.currentTimeMillis() + timeout * 1000; + return createApiKeyModel() + .setLoginId(loginId) + .setIsValid(true) + .setExpiresTime(expiresTime) + ; + } + + /** + * 随机一个 ApiKey 码 + * + * @return / + */ + public String randomApiKeyValue() { + return SaApiKeyManager.getConfig().getPrefix() + SaFoxUtil.getRandomString(36); + } + + + // ------------------- 校验 + + /** + * 判断:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),返回 true 或 false + * @param apiKey ApiKey + * @param scopes 需要校验的权限列表 + */ + public boolean hasApiKeyScope(String apiKey, String... scopes) { + try { + checkApiKeyScope(apiKey, scopes); + return true; + } catch (ApiKeyException e) { + return false; + } + } + + /** + * 校验:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),如果不具备则抛出异常 + * @param apiKey ApiKey + * @param scopes 需要校验的权限列表 + */ + public void checkApiKeyScope(String apiKey, String... scopes) { + ApiKeyModel ak = checkApiKey(apiKey); + if(SaFoxUtil.isEmptyArray(scopes)) { + return; + } + for (String scope : scopes) { + if(! ak.getScopes().contains(scope)) { + throw new ApiKeyScopeException("该 API Key 不具备 Scope:" + scope) + .setApiKey(apiKey) + .setScope(scope) + .setCode(SaApiKeyErrorCode.CODE_12311); + } + } + } + + /** + * 判断:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),返回 true 或 false + * @param apiKey ApiKey + * @param scopes 需要校验的权限列表 + */ + public boolean hasApiKeyScopeOr(String apiKey, String... scopes) { + try { + checkApiKeyScopeOr(apiKey, scopes); + return true; + } catch (ApiKeyException e) { + return false; + } + } + + /** + * 校验:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),如果不具备则抛出异常 + * @param apiKey ApiKey + * @param scopes 需要校验的权限列表 + */ + public void checkApiKeyScopeOr(String apiKey, String... scopes) { + ApiKeyModel ak = checkApiKey(apiKey); + if(SaFoxUtil.isEmptyArray(scopes)) { + return; + } + for (String scope : scopes) { + if(ak.getScopes().contains(scope)) { + return; + } + } + throw new ApiKeyScopeException("该 API Key 不具备 Scope:" + scopes[0]) + .setApiKey(apiKey) + .setScope(scopes[0]) + .setCode(SaApiKeyErrorCode.CODE_12311); + } + + /** + * 判断:指定 ApiKey 是否属于指定 LoginId,返回 true 或 false + * @param apiKey / + * @param loginId / + */ + public boolean isApiKeyLoginId(String apiKey, Object loginId) { + try { + checkApiKeyLoginId(apiKey, loginId); + return true; + } catch (ApiKeyException e) { + return false; + } + } + + /** + * 校验:指定 ApiKey 是否属于指定 LoginId,如果不是则抛出异常 + * + * @param apiKey / + * @param loginId / + */ + public void checkApiKeyLoginId(String apiKey, Object loginId) { + ApiKeyModel ak = getApiKey(apiKey); + if(ak == null) { + throw new ApiKeyException("无效 API Key: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12301); + } + if (SaFoxUtil.notEquals(String.valueOf(ak.getLoginId()), String.valueOf(loginId))) { + throw new ApiKeyException("该 API Key 不属于用户: " + loginId) + .setApiKey(apiKey) + .setCode(SaApiKeyErrorCode.CODE_12312); + } + } + + + // ------------------- 索引操作 + + /** + * 调整指定 SaSession 的 TTL 值,以保证最小化内存占用 + * @param loginId / + * @param session 可填写 null,代表使用 loginId 现场查询 + */ + public void adjustIndex(Object loginId, SaSession session) { + // 先判断是否开启索引 + if(! getIsRecordIndex()) { + SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 adjustIndex 操作"); + return; + } + + // 未提供则现场查询 + if(session == null) { + session = rawSessionDelegator.getSessionById(loginId, false); + if(session == null) { + return; + } + } + + // 重新整理索引列表 + ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); + ArrayList apiKeyNewList = new ArrayList<>(); + ArrayList apiKeyModelList = new ArrayList<>(); + for (String apikey : apiKeyList) { + ApiKeyModel ak = getApiKeyModelFromCache(apikey); + if(ak == null || ak.timeExpired()) { + continue; + } + apiKeyNewList.add(apikey); + apiKeyModelList.add(ak); + } + // 如果队列里已无有效值,则删除该 session + if(apiKeyNewList.isEmpty()) { + rawSessionDelegator.deleteSessionById(loginId); + return; + } + session.set(API_KEY_LIST, apiKeyNewList); + + // 调整 SaSession TTL + long maxTtl = 0; + for (ApiKeyModel ak : apiKeyModelList) { + long ttl = ak.expiresIn(); + if(ttl == SaTokenDao.NEVER_EXPIRE) { + maxTtl = SaTokenDao.NEVER_EXPIRE; + break; + } + if(ttl > maxTtl) { + maxTtl = ttl; + } + } + if(maxTtl != 0) { + session.updateTimeout(maxTtl); + } + } + + /** + * 获取指定 loginId 的 ApiKey 列表记录 + * @param loginId / + * @return / + */ + public List getApiKeyList(Object loginId) { + // 先判断是否开启索引 + if(! getIsRecordIndex()) { + SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 getApiKeyList 操作"); + return new ArrayList<>(); + } + + // 先查 RawSession + List apiKeyModelList = new ArrayList<>(); + SaSession session = rawSessionDelegator.getSessionById(loginId, false); + if(session == null) { + return apiKeyModelList; + } + + // 从 RawSession 遍历查询 + ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); + for (String apikey : apiKeyList) { + ApiKeyModel ak = getApiKeyModelFromCache(apikey); + if(ak == null || ak.timeExpired()) { + continue; + } + apiKeyModelList.add(ak); + } + return apiKeyModelList; + } + + + // ------------------- 请求查询 + + /** + * 数据读取:从请求对象中读取 ApiKey,获取不到返回 null + */ + public String readApiKeyValue(SaRequest request) { + + // 优先从请求参数中获取 + String apiKey = request.getParam(namespace); + if(SaFoxUtil.isNotEmpty(apiKey)) { + return apiKey; + } + + // 然后请求头 + apiKey = request.getHeader(namespace); + if(SaFoxUtil.isNotEmpty(apiKey)) { + return apiKey; + } + + // 最后从 Authorization 中获取 + apiKey = SaHttpBasicUtil.getAuthorizationValue(); + if(SaFoxUtil.isNotEmpty(apiKey)) { + if(apiKey.endsWith(":")) { + apiKey = apiKey.substring(0, apiKey.length() - 1); + } + return apiKey; + } + + return null; + } + + /** + * 数据读取:从请求对象中读取 ApiKey,并查询到 ApiKeyModel 信息 + */ + public ApiKeyModel currentApiKey() { + String readApiKeyValue = readApiKeyValue(SaHolder.getRequest()); + return checkApiKey(readApiKeyValue); + } + + + // ------------------- 拼接key + + /** + * 拼接key:ApiKey 持久化 + * @param apiKey ApiKey + * @return key + */ + public String splicingApiKeySaveKey(String apiKey) { + return getSaTokenConfig().getTokenName() + ":" + namespace + ":" + apiKey; + } + + + // -------- bean 对象代理 + + /** + * 获取使用的 getSaTokenDao 实例 + * + * @return / + */ + public SaTokenDao getSaTokenDao() { + return SaManager.getSaTokenDao(); + } + + /** + * 获取使用的 SaTokenConfig 实例 + * + * @return / + */ + public SaTokenConfig getSaTokenConfig() { + return SaManager.getConfig(); + } + + /** + * 是否保存索引信息 + */ + public boolean getIsRecordIndex() { + return SaApiKeyManager.getSaApiKeyDataLoader().getIsRecordIndex(); + } + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyUtil.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyUtil.java new file mode 100644 index 000000000..54802ae4c --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyUtil.java @@ -0,0 +1,200 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.apikey.template; + +import cn.dev33.satoken.apikey.SaApiKeyManager; +import cn.dev33.satoken.apikey.model.ApiKeyModel; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.session.SaSession; + +import java.util.List; + +/** + * API Key 操作工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaApiKeyUtil { + + /** + * 获取 ApiKeyModel,无效的 ApiKey 会返回 null + * @param apiKey / + * @return / + */ + public static ApiKeyModel getApiKey(String apiKey) { + return SaApiKeyManager.getSaApiKeyTemplate().getApiKey(apiKey); + } + + /** + * 校验 ApiKey,成功返回 ApiKeyModel,失败则抛出异常 + * @param apiKey / + * @return / + */ + public static ApiKeyModel checkApiKey(String apiKey) { + return SaApiKeyManager.getSaApiKeyTemplate().checkApiKey(apiKey); + } + + /** + * 持久化:ApiKeyModel + * @param ak / + */ + public static void saveApiKey(ApiKeyModel ak) { + SaApiKeyManager.getSaApiKeyTemplate().saveApiKey(ak); + } + + /** + * 获取 ApiKey 所代表的 LoginId + * @param apiKey ApiKey + * @return LoginId + */ + public static Object getLoginIdByApiKey(String apiKey) { + return SaApiKeyManager.getSaApiKeyTemplate().getLoginIdByApiKey(apiKey); + } + + /** + * 删除 ApiKey + * @param apiKey ApiKey + */ + public static void deleteApiKey(String apiKey) { + SaApiKeyManager.getSaApiKeyTemplate().deleteApiKey(apiKey); + } + + /** + * 删除指定 loginId 的所有 ApiKey + * @param loginId / + */ + public static void deleteApiKeyByLoginId(Object loginId) { + SaApiKeyManager.getSaApiKeyTemplate().deleteApiKeyByLoginId(loginId); + } + + // ------- 创建 + + /** + * 创建一个 ApiKeyModel 对象 + * + * @return / + */ + public static ApiKeyModel createApiKeyModel() { + return SaApiKeyManager.getSaApiKeyTemplate().createApiKeyModel(); + } + + /** + * 创建一个 ApiKeyModel 对象 + * + * @return / + */ + public static ApiKeyModel createApiKeyModel(Object loginId) { + return SaApiKeyManager.getSaApiKeyTemplate().createApiKeyModel(loginId); + } + + + // ------------------- Scope + + /** + * 判断:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),返回 true 或 false + * @param apiKey ApiKey + * @param scopes 需要校验的权限列表 + */ + public static boolean hasApiKeyScope(String apiKey, String... scopes) { + return SaApiKeyManager.getSaApiKeyTemplate().hasApiKeyScope(apiKey, scopes); + } + + /** + * 校验:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),如果不具备则抛出异常 + * @param apiKey ApiKey + * @param scopes 需要校验的权限列表 + */ + public static void checkApiKeyScope(String apiKey, String... scopes) { + SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyScope(apiKey, scopes); + } + + /** + * 判断:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),返回 true 或 false + * @param apiKey ApiKey + * @param scopes 需要校验的权限列表 + */ + public static boolean hasApiKeyScopeOr(String apiKey, String... scopes) { + return SaApiKeyManager.getSaApiKeyTemplate().hasApiKeyScopeOr(apiKey, scopes); + } + + /** + * 校验:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),如果不具备则抛出异常 + * @param apiKey ApiKey + * @param scopes 需要校验的权限列表 + */ + public static void checkApiKeyScopeOr(String apiKey, String... scopes) { + SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyScopeOr(apiKey, scopes); + } + + /** + * 判断:指定 ApiKey 是否属于指定 LoginId,返回 true 或 false + * @param apiKey / + * @param loginId / + */ + public static boolean isApiKeyLoginId(String apiKey, Object loginId) { + return SaApiKeyManager.getSaApiKeyTemplate().isApiKeyLoginId(apiKey, loginId); + } + + /** + * 校验:指定 ApiKey 是否属于指定 LoginId,如果不是则抛出异常 + * + * @param apiKey / + * @param loginId / + */ + public static void checkApiKeyLoginId(String apiKey, Object loginId) { + SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyLoginId(apiKey, loginId); + } + + + // ------------------- 请求查询 + + /** + * 数据读取:从请求对象中读取 ApiKey,获取不到返回 null + */ + public static String readApiKeyValue(SaRequest request) { + return SaApiKeyManager.getSaApiKeyTemplate().readApiKeyValue(request); + } + + /** + * 数据读取:从请求对象中读取 ApiKey,并查询到 ApiKeyModel 信息 + */ + public static ApiKeyModel currentApiKey() { + return SaApiKeyManager.getSaApiKeyTemplate().currentApiKey(); + } + + + // ------------------- 索引操作 + + /** + * 调整指定 SaSession 的 TTL 值,以保证最小化内存占用 + * @param loginId / + * @param session 可填写 null,代表使用 loginId 现场查询 + */ + public static void adjustIndex(Object loginId, SaSession session) { + SaApiKeyManager.getSaApiKeyTemplate().adjustIndex(loginId, session); + } + + /** + * 获取指定 loginId 的 ApiKey 列表记录 + * @param loginId / + * @return / + */ + public static List getApiKeyList(Object loginId) { + return SaApiKeyManager.getSaApiKeyTemplate().getApiKeyList(loginId); + } + +} diff --git a/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForApiKey.java b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForApiKey.java new file mode 100644 index 000000000..27d296ff1 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForApiKey.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.apikey.annotation.handle.SaCheckApiKeyHandler; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; + +/** + * SaToken 插件安装:API Key 组件 + * + * @author click33 + * @since 1.43.0 + */ +public class SaTokenPluginForApiKey implements SaTokenPlugin { + + @Override + public void install() { + // 安装 API Key 鉴权注解 + SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckApiKeyHandler()); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-apikey/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-apikey/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..027e94596 --- /dev/null +++ b/sa-token-plugin/sa-token-apikey/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForApiKey \ No newline at end of file diff --git a/sa-token-plugin/sa-token-caffeine/pom.xml b/sa-token-plugin/sa-token-caffeine/pom.xml new file mode 100644 index 000000000..92f32f678 --- /dev/null +++ b/sa-token-plugin/sa-token-caffeine/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-caffeine + sa-token-caffeine + sa-token integrate Caffeine + + + + cn.dev33 + sa-token-core + + + com.github.ben-manes.caffeine + caffeine + + + + diff --git a/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaMapPackageForCaffeine.java b/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaMapPackageForCaffeine.java new file mode 100644 index 000000000..dba9f5482 --- /dev/null +++ b/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaMapPackageForCaffeine.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; + +import cn.dev33.satoken.dao.timedcache.SaMapPackage; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Map 包装类 (Caffeine 版) + * + * @author click33 + * @since 1.41.0 + */ +public class SaMapPackageForCaffeine implements SaMapPackage { + + public Cache cache = Caffeine.newBuilder() + .expireAfterWrite(Long.MAX_VALUE, TimeUnit.SECONDS) + .maximumSize(Integer.MAX_VALUE) + .build(); + + @Override + public Object getSource() { + return cache; + } + + /** + * 读 + * + * @param key / + * @return / + */ + @Override + public V get(String key) { + return cache.getIfPresent(key); + } + + /** + * 写 + * + * @param key / + * @param value / + */ + @Override + public void put(String key, V value) { + cache.put(key, value); + } + + /** + * 删 + * @param key / + */ + @Override + public void remove(String key) { + cache.invalidate(key); + } + + /** + * 所有 key + */ + @Override + public Set keySet() { + return cache.asMap().keySet(); + } + +} diff --git a/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForCaffeine.java b/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForCaffeine.java new file mode 100644 index 000000000..1c99b547d --- /dev/null +++ b/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForCaffeine.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; + + +import cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject; +import cn.dev33.satoken.dao.timedcache.SaTimedCache; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.List; + +/** + * Sa-Token 持久层实现,基于 SaTimedCache - Caffeine (内存缓存,系统重启后数据丢失) + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenDaoForCaffeine implements SaTokenDaoByStringFollowObject, SaTokenDao { + + public SaTimedCache timedCache = new SaTimedCache( + new SaMapPackageForCaffeine<>(), + new SaMapPackageForCaffeine<>() + ); + + // ------------------------ Object 读写操作 + + @Override + public Object getObject(String key) { + return timedCache.getObject(key); + } + + @Override + @SuppressWarnings("unchecked") + public T getObject(String key, Class classType){ + return (T) getObject(key); + } + + @Override + public void setObject(String key, Object object, long timeout) { + timedCache.setObject(key, object, timeout); + } + + @Override + public void updateObject(String key, Object object) { + timedCache.updateObject(key, object); + } + + @Override + public void deleteObject(String key) { + timedCache.deleteObject(key); + } + + @Override + public long getObjectTimeout(String key) { + return timedCache.getObjectTimeout(key); + } + + @Override + public void updateObjectTimeout(String key, long timeout) { + timedCache.updateObjectTimeout(key, timeout); + } + + + // --------- 会话管理 + + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + return SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType); + } + + + // --------- 组件生命周期 + + /** + * 组件被安装时,开始刷新数据线程 + */ + @Override + public void init() { + timedCache.initRefreshThread(); + } + + /** + * 组件被卸载时,结束定时任务,不再定时清理过期数据 + */ + @Override + public void destroy() { + timedCache.endRefreshThread(); + } +} diff --git a/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForCaffeine.java b/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForCaffeine.java new file mode 100644 index 000000000..8caf53a8e --- /dev/null +++ b/sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForCaffeine.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDaoForCaffeine; + +/** + * SaToken 插件安装:DAO 扩展 - Caffeine 版 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginForCaffeine implements SaTokenPlugin { + + @Override + public void install() { + + SaManager.setSaTokenDao(new SaTokenDaoForCaffeine()); + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-caffeine/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-caffeine/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..077fa6080 --- /dev/null +++ b/sa-token-plugin/sa-token-caffeine/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForCaffeine \ No newline at end of file diff --git a/sa-token-plugin/sa-token-dao-redis-jackson/.gitignore b/sa-token-plugin/sa-token-dao-redis-jackson/.gitignore deleted file mode 100644 index 8122f47c0..000000000 --- a/sa-token-plugin/sa-token-dao-redis-jackson/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ -.iml \ No newline at end of file diff --git a/sa-token-plugin/sa-token-dao-redis-jackson/pom.xml b/sa-token-plugin/sa-token-dao-redis-jackson/pom.xml deleted file mode 100644 index fd237ae0d..000000000 --- a/sa-token-plugin/sa-token-dao-redis-jackson/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - 4.0.0 - - - cn.dev33 - sa-token-plugin - 1.24.0 - - jar - - sa-token-dao-redis-jackson - sa-token-dao-redis-jackson - sa-token integrate redis (to jackson) - - - - - cn.dev33 - sa-token-core - ${sa-token-version} - - - - org.springframework.boot - spring-boot-starter-data-redis - 2.3.3.RELEASE - - - - com.fasterxml.jackson.core - jackson-databind - 2.11.2 - true - - - - - - diff --git a/sa-token-plugin/sa-token-dao-redis-jackson/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedisJackson.java b/sa-token-plugin/sa-token-dao-redis-jackson/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedisJackson.java deleted file mode 100644 index dc230ec0a..000000000 --- a/sa-token-plugin/sa-token-dao-redis-jackson/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedisJackson.java +++ /dev/null @@ -1,246 +0,0 @@ -package cn.dev33.satoken.dao; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * Sa-Token持久层接口 [Redis版] (使用 jackson 序列化方式) - * - * @author kong - * - */ -@Component -public class SaTokenDaoRedisJackson implements SaTokenDao { - - /** - * ObjectMapper对象 (以public作用域暴露出此对象,方便开发者二次更改配置) - */ - public ObjectMapper objectMapper; - - /** - * String专用 - */ - public StringRedisTemplate stringRedisTemplate; - - /** - * Object专用 - */ - public RedisTemplate objectRedisTemplate; - - /** - * 标记:是否已初始化成功 - */ - public boolean isInit; - - @Autowired - public void init(RedisConnectionFactory connectionFactory) { - - // 指定相应的序列化方案 - StringRedisSerializer keySerializer = new StringRedisSerializer(); - GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(); - // 通过反射获取Mapper对象, 配置[忽略未知字段], 增强兼容性 - try { - Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper"); - field.setAccessible(true); - ObjectMapper objectMapper = (ObjectMapper) field.get(valueSerializer); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - this.objectMapper = objectMapper; - } catch (Exception e) { - System.err.println(e.getMessage()); - } - // 构建StringRedisTemplate - StringRedisTemplate stringTemplate = new StringRedisTemplate(); - stringTemplate.setConnectionFactory(connectionFactory); - stringTemplate.afterPropertiesSet(); - // 构建RedisTemplate - RedisTemplate template = new RedisTemplate(); - template.setConnectionFactory(connectionFactory); - template.setKeySerializer(keySerializer); - template.setHashKeySerializer(keySerializer); - template.setValueSerializer(valueSerializer); - template.setHashValueSerializer(valueSerializer); - template.afterPropertiesSet(); - - // 开始初始化相关组件 - if(this.isInit == false) { - this.stringRedisTemplate = stringTemplate; - this.objectRedisTemplate = template; - this.isInit = true; - } - } - - - /** - * 获取Value,如无返空 - */ - @Override - public String get(String key) { - return stringRedisTemplate.opsForValue().get(key); - } - - /** - * 写入Value,并设定存活时间 (单位: 秒) - */ - @Override - public void set(String key, String value, long timeout) { - if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - // 判断是否为永不过期 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - stringRedisTemplate.opsForValue().set(key, value); - } else { - stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); - } - } - - /** - * 修修改指定key-value键值对 (过期时间不变) - */ - @Override - public void update(String key, String value) { - long expire = getTimeout(key); - // -2 = 无此键 - if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - this.set(key, value, expire); - } - - /** - * 删除Value - */ - @Override - public void delete(String key) { - stringRedisTemplate.delete(key); - } - - /** - * 获取Value的剩余存活时间 (单位: 秒) - */ - @Override - public long getTimeout(String key) { - return stringRedisTemplate.getExpire(key); - } - - /** - * 修改Value的剩余存活时间 (单位: 秒) - */ - @Override - public void updateTimeout(String key, long timeout) { - // 判断是否想要设置为永久 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - long expire = getTimeout(key); - if(expire == SaTokenDao.NEVER_EXPIRE) { - // 如果其已经被设置为永久,则不作任何处理 - } else { - // 如果尚未被设置为永久,那么再次set一次 - this.set(key, this.get(key), timeout); - } - return; - } - stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); - } - - - - /** - * 获取Object,如无返空 - */ - @Override - public Object getObject(String key) { - return objectRedisTemplate.opsForValue().get(key); - } - - /** - * 写入Object,并设定存活时间 (单位: 秒) - */ - @Override - public void setObject(String key, Object object, long timeout) { - if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - // 判断是否为永不过期 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - objectRedisTemplate.opsForValue().set(key, object); - } else { - objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS); - } - } - - /** - * 更新Object (过期时间不变) - */ - @Override - public void updateObject(String key, Object object) { - long expire = getObjectTimeout(key); - // -2 = 无此键 - if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - this.setObject(key, object, expire); - } - - /** - * 删除Object - */ - @Override - public void deleteObject(String key) { - objectRedisTemplate.delete(key); - } - - /** - * 获取Object的剩余存活时间 (单位: 秒) - */ - @Override - public long getObjectTimeout(String key) { - return objectRedisTemplate.getExpire(key); - } - - /** - * 修改Object的剩余存活时间 (单位: 秒) - */ - @Override - public void updateObjectTimeout(String key, long timeout) { - // 判断是否想要设置为永久 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - long expire = getObjectTimeout(key); - if(expire == SaTokenDao.NEVER_EXPIRE) { - // 如果其已经被设置为永久,则不作任何处理 - } else { - // 如果尚未被设置为永久,那么再次set一次 - this.setObject(key, this.getObject(key), timeout); - } - return; - } - objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); - } - - - /** - * 搜索数据 - */ - @Override - public List searchData(String prefix, String keyword, int start, int size) { - Set keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*"); - List list = new ArrayList(keys); - return SaFoxUtil.searchList(list, start, size); - } - -} diff --git a/sa-token-plugin/sa-token-dao-redis-jackson/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-dao-redis-jackson/src/main/resources/META-INF/spring.factories deleted file mode 100644 index d792b9f92..000000000 --- a/sa-token-plugin/sa-token-dao-redis-jackson/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoRedisJackson \ No newline at end of file diff --git a/sa-token-plugin/sa-token-dao-redis/.gitignore b/sa-token-plugin/sa-token-dao-redis/.gitignore deleted file mode 100644 index 8122f47c0..000000000 --- a/sa-token-plugin/sa-token-dao-redis/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ -.iml \ No newline at end of file diff --git a/sa-token-plugin/sa-token-dao-redis/pom.xml b/sa-token-plugin/sa-token-dao-redis/pom.xml deleted file mode 100644 index 24f9423e6..000000000 --- a/sa-token-plugin/sa-token-dao-redis/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - - cn.dev33 - sa-token-plugin - 1.24.0 - - jar - - sa-token-dao-redis - sa-token-dao-redis - sa-token integrate redis - - - - - cn.dev33 - sa-token-core - ${sa-token-version} - - - - org.springframework.boot - spring-boot-starter-data-redis - 2.3.3.RELEASE - - - - - - diff --git a/sa-token-plugin/sa-token-dao-redis/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedis.java b/sa-token-plugin/sa-token-dao-redis/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedis.java deleted file mode 100644 index 532f844df..000000000 --- a/sa-token-plugin/sa-token-dao-redis/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedis.java +++ /dev/null @@ -1,227 +0,0 @@ -package cn.dev33.satoken.dao; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.stereotype.Component; - -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * Sa-Token持久层接口 [Redis版 (使用JDK默认序列化方式)] - * - * @author kong - * - */ -@Component -public class SaTokenDaoRedis implements SaTokenDao { - - /** - * String专用 - */ - public StringRedisTemplate stringRedisTemplate; - - /** - * Objecy专用 - */ - public RedisTemplate objectRedisTemplate; - - /** - * 标记:是否已初始化成功 - */ - public boolean isInit; - - @Autowired - public void init(RedisConnectionFactory connectionFactory) { - // 指定相应的序列化方案 - StringRedisSerializer keySerializer = new StringRedisSerializer(); - JdkSerializationRedisSerializer valueSerializer = new JdkSerializationRedisSerializer(); - // 构建StringRedisTemplate - StringRedisTemplate stringTemplate = new StringRedisTemplate(); - stringTemplate.setConnectionFactory(connectionFactory); - stringTemplate.afterPropertiesSet(); - // 构建RedisTemplate - RedisTemplate template = new RedisTemplate(); - template.setConnectionFactory(connectionFactory); - template.setKeySerializer(keySerializer); - template.setHashKeySerializer(keySerializer); - template.setValueSerializer(valueSerializer); - template.setHashValueSerializer(valueSerializer); - template.afterPropertiesSet(); - - // 开始初始化相关组件 - if(this.isInit == false) { - this.stringRedisTemplate = stringTemplate; - this.objectRedisTemplate = template; - this.isInit = true; - } - } - - - /** - * 获取Value,如无返空 - */ - @Override - public String get(String key) { - return stringRedisTemplate.opsForValue().get(key); - } - - /** - * 写入Value,并设定存活时间 (单位: 秒) - */ - @Override - public void set(String key, String value, long timeout) { - if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - // 判断是否为永不过期 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - stringRedisTemplate.opsForValue().set(key, value); - } else { - stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); - } - } - - /** - * 修改指定key-value键值对 (过期时间不变) - */ - @Override - public void update(String key, String value) { - long expire = getTimeout(key); - // -2 = 无此键 - if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - this.set(key, value, expire); - } - - /** - * 删除Value - */ - @Override - public void delete(String key) { - stringRedisTemplate.delete(key); - } - - /** - * 获取Value的剩余存活时间 (单位: 秒) - */ - @Override - public long getTimeout(String key) { - return stringRedisTemplate.getExpire(key); - } - - /** - * 修改Value的剩余存活时间 (单位: 秒) - */ - @Override - public void updateTimeout(String key, long timeout) { - // 判断是否想要设置为永久 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - long expire = getTimeout(key); - if(expire == SaTokenDao.NEVER_EXPIRE) { - // 如果其已经被设置为永久,则不作任何处理 - } else { - // 如果尚未被设置为永久,那么再次set一次 - this.set(key, this.get(key), timeout); - } - return; - } - stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); - } - - - /** - * 获取Object,如无返空 - */ - @Override - public Object getObject(String key) { - return objectRedisTemplate.opsForValue().get(key); - } - - /** - * 写入Object,并设定存活时间 (单位: 秒) - */ - @Override - public void setObject(String key, Object object, long timeout) { - if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - // 判断是否为永不过期 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - objectRedisTemplate.opsForValue().set(key, object); - } else { - objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS); - } - } - - /** - * 更新Object (过期时间不变) - */ - @Override - public void updateObject(String key, Object object) { - long expire = getObjectTimeout(key); - // -2 = 无此键 - if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { - return; - } - this.setObject(key, object, expire); - } - - /** - * 删除Object - */ - @Override - public void deleteObject(String key) { - objectRedisTemplate.delete(key); - } - - /** - * 获取Object的剩余存活时间 (单位: 秒) - */ - @Override - public long getObjectTimeout(String key) { - return objectRedisTemplate.getExpire(key); - } - - /** - * 修改Object的剩余存活时间 (单位: 秒) - */ - @Override - public void updateObjectTimeout(String key, long timeout) { - // 判断是否想要设置为永久 - if(timeout == SaTokenDao.NEVER_EXPIRE) { - long expire = getObjectTimeout(key); - if(expire == SaTokenDao.NEVER_EXPIRE) { - // 如果其已经被设置为永久,则不作任何处理 - } else { - // 如果尚未被设置为永久,那么再次set一次 - this.setObject(key, this.getObject(key), timeout); - } - return; - } - objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); - } - - - - /** - * 搜索数据 - */ - @Override - public List searchData(String prefix, String keyword, int start, int size) { - Set keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*"); - List list = new ArrayList(keys); - return SaFoxUtil.searchList(list, start, size); - } - - -} diff --git a/sa-token-plugin/sa-token-dao-redis/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-dao-redis/src/main/resources/META-INF/spring.factories deleted file mode 100644 index ce5c7e82d..000000000 --- a/sa-token-plugin/sa-token-dao-redis/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoRedis \ No newline at end of file diff --git a/sa-token-plugin/sa-token-dubbo/pom.xml b/sa-token-plugin/sa-token-dubbo/pom.xml new file mode 100644 index 000000000..9a9687ae5 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-dubbo + sa-token-dubbo + sa-token-dubbo + + + + + cn.dev33 + sa-token-core + + + + + org.apache.dubbo + dubbo + true + + + + + + + diff --git a/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboConsumerFilter.java b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboConsumerFilter.java new file mode 100644 index 000000000..b7c5e3040 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboConsumerFilter.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo.filter; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.SaTokenContextDefaultImpl; +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; + +/** + * Sa-Token 整合 Dubbo Consumer 端(调用端)过滤器 + * + * @author click33 + * @since 1.34.0 + */ +@Activate(group = {CommonConstants.CONSUMER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER) +public class SaTokenDubboConsumerFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + + // 追加 Same-Token 参数 + if(SaManager.getConfig().getCheckSameToken()) { + RpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()); + } + + // 无上下文时只做简单调用,不传递会话 token + if( ! SaHolder.getContext().isValid()) { + return invoker.invoke(invocation); + } + + // 1、调用前,向下传递会话Token + if(SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) { + RpcContext.getContext().setAttachment(SaTokenConsts.JUST_CREATED, StpUtil.getTokenValueNotCut()); + } + + // 2、开始调用 + Result invoke = invoker.invoke(invocation); + + // 3、调用后,解析回传的Token值 + StpUtil.setTokenValue(invoke.getAttachment(SaTokenConsts.JUST_CREATED_NOT_PREFIX)); + + // note + return invoke; + } + +} diff --git a/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboContextFilter.java b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboContextFilter.java new file mode 100644 index 000000000..a09ec3227 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboContextFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo.filter; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.dubbo.util.SaTokenContextDubboUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; + +/** + * Sa-Token 整合 Dubbo 上下文初始化过滤器 + * + * @author click33 + * @since 1.42.0 + */ +@Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_CONTEXT_FILTER_ORDER) +public class SaTokenDubboContextFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) { + if(SaHolder.getContext().isValid()) { + return invoker.invoke(invocation); + } + try { + SaTokenContextDubboUtil.setContext(RpcContext.getContext()); + return invoker.invoke(invocation); + } finally { + SaTokenContextDubboUtil.clearContext(); + } + } + +} diff --git a/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboProviderFilter.java b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboProviderFilter.java new file mode 100644 index 000000000..142840ab1 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboProviderFilter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo.filter; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.Filter; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Result; + +/** + * Sa-Token 整合 Dubbo Provider端(被调用端)过滤器 + * + * @author click33 + * @since 1.34.0 + */ +@Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER) +public class SaTokenDubboProviderFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) { + // RPC 调用鉴权 + if(SaManager.getConfig().getCheckSameToken()) { + String idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN); + + // dubbo部分协议会将参数变为小写,此处需要额外处理一下,详细参考:https://gitee.com/dromara/sa-token/issues/I4WXQG + if(idToken == null) { + idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN.toLowerCase()); + } + SaSameUtil.checkToken(idToken); + } + + // 开始调用 + return invoker.invoke(invocation); + } + +} diff --git a/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaRequestForDubbo.java b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaRequestForDubbo.java new file mode 100644 index 000000000..24f21c4ad --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaRequestForDubbo.java @@ -0,0 +1,159 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo.model; + +import cn.dev33.satoken.context.model.SaRequest; +import org.apache.dubbo.rpc.RpcContext; + +import java.util.Collection; +import java.util.Map; + +/** + * 对 SaRequest 包装类的实现(Dubbo 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaRequestForDubbo implements SaRequest { + + /** + * 底层对象 + */ + protected RpcContext rpcContext; + + /** + * 实例化 + * @param rpcContext rpcContext对象 + */ + public SaRequestForDubbo(RpcContext rpcContext) { + this.rpcContext = rpcContext; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return rpcContext; + } + + /** + * 在 [请求体] 里获取一个值 + */ + @Override + public String getParam(String name) { + // 不传播 url 参数 + return null; + } + + /** + * 获取 [请求体] 里提交的所有参数名称 + * @return 参数名称列表 + */ + @Override + public Collection getParamNames(){ + return null; + } + + /** + * 获取 [请求体] 里提交的所有参数 + * @return 参数列表 + */ + @Override + public Map getParamMap(){ + return null; + } + + /** + * 在 [请求头] 里获取一个值 + */ + @Override + public String getHeader(String name) { + // 不传播 header 参数 + return null; + } + + /** + * 在 [Cookie作用域] 里获取一个值 + */ + @Override + public String getCookieValue(String name) { + // 不传播 cookie 参数 + return null; + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name){ + // 不传播 cookie 参数 + return null; + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name){ + // 不传播 cookie 参数 + return null; + } + + /** + * 返回当前请求path (不包括上下文名称) + */ + @Override + public String getRequestPath() { + // 不传播 requestPath + return null; + } + + /** + * 返回当前请求的url,例:http://xxx.com/test + * @return see note + */ + public String getUrl() { + // 不传播 url + return null; + } + + /** + * 返回当前请求的类型 + */ + @Override + public String getMethod() { + // 不传播 method + return null; + } + + @Override + public String getHost() { + return null; + } + + /** + * 转发请求 + */ + @Override + public Object forward(String path) { + // 不传播 forward 动作 + return null; + } + +} diff --git a/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaResponseForDubbo.java b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaResponseForDubbo.java new file mode 100644 index 000000000..10c17d50e --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaResponseForDubbo.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo.model; + +import cn.dev33.satoken.context.model.SaResponse; +import org.apache.dubbo.rpc.RpcContext; + +/** + * 对 SaResponse 包装类的实现(Dubbo 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaResponseForDubbo implements SaResponse { + + /** + * 底层Request对象 + */ + protected RpcContext rpcContext; + + /** + * 实例化 + * @param rpcContext rpcContext对象 + */ + public SaResponseForDubbo(RpcContext rpcContext) { + this.rpcContext = rpcContext; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return rpcContext; + } + + /** + * 设置响应状态码 + */ + @Override + public SaResponse setStatus(int sc) { + // 不回传 status 状态 + return this; + } + + /** + * 在响应头里写入一个值 + */ + @Override + public SaResponse setHeader(String name, String value) { + // 不回传 header响应头 + return this; + } + + /** + * 在响应头里添加一个值 + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + public SaResponse addHeader(String name, String value) { + // 不回传 header响应头 + return this; + } + + /** + * 重定向 + */ + @Override + public Object redirect(String url) { + // 不回传 重定向 动作 + return null; + } + + +} diff --git a/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaStorageForDubbo.java b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaStorageForDubbo.java new file mode 100644 index 000000000..1d8780c17 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaStorageForDubbo.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo.model; + +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.util.SaTokenConsts; +import org.apache.dubbo.rpc.RpcContext; + +/** + * 对 SaStorage 包装类的实现(Dubbo 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaStorageForDubbo implements SaStorage { + + /** + * 底层对象 + */ + protected RpcContext rpcContext; + + /** + * 实例化 + * @param rpcContext rpcContext对象 + */ + public SaStorageForDubbo(RpcContext rpcContext) { + this.rpcContext = rpcContext; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return rpcContext; + } + + /** + * 在 [Request作用域] 里写入一个值 + */ + @Override + public SaStorageForDubbo set(String key, Object value) { + rpcContext.setObjectAttachment(key, value); + // 如果是token写入,则回传到Consumer端 + if(key.equals(SaTokenConsts.JUST_CREATED_NOT_PREFIX)) { + RpcContext.getServerContext().setAttachment(key, value); + } + return this; + } + + /** + * 在 [Request作用域] 里获取一个值 + */ + @Override + public Object get(String key) { + return rpcContext.getObjectAttachment(key); + } + + /** + * 在 [Request作用域] 里删除一个值 + */ + @Override + public SaStorageForDubbo delete(String key) { + rpcContext.removeAttachment(key); + return this; + } + +} diff --git a/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/util/SaTokenContextDubboUtil.java b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/util/SaTokenContextDubboUtil.java new file mode 100644 index 000000000..f0b910d3b --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/util/SaTokenContextDubboUtil.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo.util; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.dubbo.model.SaRequestForDubbo; +import cn.dev33.satoken.context.dubbo.model.SaResponseForDubbo; +import cn.dev33.satoken.context.dubbo.model.SaStorageForDubbo; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import org.apache.dubbo.rpc.RpcContext; + + +/** + * SaTokenContext 上下文读写工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenContextDubboUtil { + + /** + * 写入当前上下文 + * @param rpcContext / + */ + public static void setContext(RpcContext rpcContext) { + SaRequest saRequest = new SaRequestForDubbo(RpcContext.getContext()); + SaResponse saResponse = new SaResponseForDubbo(RpcContext.getContext()); + SaStorage saStorage = new SaStorageForDubbo(RpcContext.getContext()); + SaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage); + } + + /** + * 清除当前上下文 + */ + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); + } + +} diff --git a/sa-token-plugin/sa-token-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter b/sa-token-plugin/sa-token-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter new file mode 100644 index 000000000..6ab6247dd --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter @@ -0,0 +1,3 @@ +saTokenDubboConsumerFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboConsumerFilter +saTokenDubboProviderFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboProviderFilter +saTokenDubboContextFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboContextFilter \ No newline at end of file diff --git a/sa-token-plugin/sa-token-dubbo3/pom.xml b/sa-token-plugin/sa-token-dubbo3/pom.xml new file mode 100644 index 000000000..950cd0f61 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + + sa-token-dubbo3 + sa-token-dubbo3 + sa-token-dubbo3 + + + + 3.2.2 + 8 + 8 + UTF-8 + + + + + + cn.dev33 + sa-token-core + + + + + org.apache.dubbo + dubbo + ${dubbo3.version} + + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ConsumerFilter.java b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ConsumerFilter.java new file mode 100644 index 000000000..bc0ae52af --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ConsumerFilter.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo3.filter; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.SaTokenContextDefaultImpl; +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; + +/** + * Sa-Token 整合 Dubbo3 Consumer 端(调用端)过滤器 + * + * @author click33 + * @since 1.34.0 + */ +@Activate(group = {CommonConstants.CONSUMER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER) +public class SaTokenDubbo3ConsumerFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) { + + // 追加 Same-Token 参数 + if(SaManager.getConfig().getCheckSameToken()) { + RpcContext.getServiceContext().setAttachment(SaSameUtil.SAME_TOKEN,SaSameUtil.getToken()); + } + + // 无上下文时只做简单调用,不传递会话 token + if( ! SaHolder.getContext().isValid()) { + return invoker.invoke(invocation); + } + + // 1. 调用前,向下传递会话Token + if(SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) { + RpcContext.getServiceContext().setAttachment(SaTokenConsts.JUST_CREATED, StpUtil.getTokenValueNotCut()); + } + + // 2. 开始调用 + Result invoke = invoker.invoke(invocation); + + // 3. 调用后,解析回传的Token值 + StpUtil.setTokenValue(invoke.getAttachment(SaTokenConsts.JUST_CREATED_NOT_PREFIX)); + + // note + return invoke; + } + +} diff --git a/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ContextFilter.java b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ContextFilter.java new file mode 100644 index 000000000..74dc6e055 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ContextFilter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo3.filter; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.dubbo3.util.SaTokenContextDubbo3Util; +import cn.dev33.satoken.util.SaTokenConsts; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; + +/** + * Sa-Token 整合 Dubbo3 上下文初始化过滤器 + * + * @author click33 + * @since 1.42.0 + */ +@Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_CONTEXT_FILTER_ORDER) +public class SaTokenDubbo3ContextFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) { + if(SaHolder.getContext().isValid()) { + return invoker.invoke(invocation); + } + try { + SaTokenContextDubbo3Util.setContext(RpcContext.getServiceContext()); + return invoker.invoke(invocation); + } finally { + SaTokenContextDubbo3Util.clearContext(); + } + + } + +} diff --git a/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ProviderFilter.java b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ProviderFilter.java new file mode 100644 index 000000000..817b8614f --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ProviderFilter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo3.filter; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.Filter; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Result; + +/** + * Sa-Token 整合 Dubbo3 Provider端(被调用端)过滤器 + * + * @author click33 + * @since 1.34.0 + */ +@Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER) +public class SaTokenDubbo3ProviderFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) { + + // RPC 调用鉴权 + if(SaManager.getConfig().getCheckSameToken()) { + String idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN); + // dubbo部分协议会将参数变为小写,详细参考:https://gitee.com/dromara/sa-token/issues/I4WXQG + if(idToken == null) { + idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN.toLowerCase()); + } + SaSameUtil.checkToken(idToken); + } + + // 开始调用 + return invoker.invoke(invocation); + } + +} diff --git a/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaRequestForDubbo3.java b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaRequestForDubbo3.java new file mode 100644 index 000000000..9c1a9708d --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaRequestForDubbo3.java @@ -0,0 +1,159 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo3.model; + +import cn.dev33.satoken.context.model.SaRequest; +import org.apache.dubbo.rpc.RpcContext; + +import java.util.Collection; +import java.util.Map; + +/** + * 对 SaRequest 包装类的实现(Dubbo3 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaRequestForDubbo3 implements SaRequest { + + /** + * 底层对象 + */ + protected RpcContext rpcContext; + + /** + * 实例化 + * @param rpcContext rpcContext对象 + */ + public SaRequestForDubbo3(RpcContext rpcContext) { + this.rpcContext = rpcContext; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return rpcContext; + } + + /** + * 在 [请求体] 里获取一个值 + */ + @Override + public String getParam(String name) { + // 不传播 url 参数 + return null; + } + + /** + * 获取 [请求体] 里提交的所有参数名称 + * @return 参数名称列表 + */ + @Override + public Collection getParamNames(){ + return null; + } + + /** + * 获取 [请求体] 里提交的所有参数 + * @return 参数列表 + */ + @Override + public Map getParamMap(){ + return null; + } + + /** + * 在 [请求头] 里获取一个值 + */ + @Override + public String getHeader(String name) { + // 不传播 header 参数 + return null; + } + + /** + * 在 [Cookie作用域] 里获取一个值 + */ + @Override + public String getCookieValue(String name) { + // 不传播 cookie 参数 + return null; + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name){ + // 不传播 cookie 参数 + return null; + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name){ + // 不传播 cookie 参数 + return null; + } + + /** + * 返回当前请求path (不包括上下文名称) + */ + @Override + public String getRequestPath() { + // 不传播 requestPath + return null; + } + + /** + * 返回当前请求的url,例:http://xxx.com/test + * @return see note + */ + public String getUrl() { + // 不传播 url + return null; + } + + /** + * 返回当前请求的类型 + */ + @Override + public String getMethod() { + // 不传播 method + return null; + } + + @Override + public String getHost() { + return null; + } + + /** + * 转发请求 + */ + @Override + public Object forward(String path) { + // 不传播 forward 动作 + return null; + } + +} diff --git a/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaResponseForDubbo3.java b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaResponseForDubbo3.java new file mode 100644 index 000000000..3ecd51f58 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaResponseForDubbo3.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo3.model; + + +import cn.dev33.satoken.context.model.SaResponse; +import org.apache.dubbo.rpc.RpcContext; + +/** + * 对 SaResponse 包装类的实现(Dubbo3 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaResponseForDubbo3 implements SaResponse { + + /** + * 底层Request对象 + */ + protected RpcContext rpcContext; + + /** + * 实例化 + * @param rpcContext rpcContext对象 + */ + public SaResponseForDubbo3(RpcContext rpcContext) { + this.rpcContext = rpcContext; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return rpcContext; + } + + /** + * 设置响应状态码 + */ + @Override + public SaResponse setStatus(int sc) { + // 不回传 status 状态 + return this; + } + + /** + * 在响应头里写入一个值 + */ + @Override + public SaResponse setHeader(String name, String value) { + // 不回传 header响应头 + return this; + } + + /** + * 在响应头里添加一个值 + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + public SaResponse addHeader(String name, String value) { + // 不回传 header响应头 + return this; + } + + /** + * 重定向 + */ + @Override + public Object redirect(String url) { + // 不回传 重定向 动作 + return null; + } + + +} diff --git a/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaStorageForDubbo3.java b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaStorageForDubbo3.java new file mode 100644 index 000000000..815ca14c2 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaStorageForDubbo3.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo3.model; + +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.util.SaTokenConsts; +import org.apache.dubbo.rpc.RpcContext; + +/** + * 对 SaStorage 包装类的实现(Dubbo3 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaStorageForDubbo3 implements SaStorage { + + /** + * 底层对象 + */ + protected RpcContext rpcContext; + + /** + * 实例化 + * @param rpcContext rpcContext对象 + */ + public SaStorageForDubbo3(RpcContext rpcContext) { + this.rpcContext = rpcContext; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return rpcContext; + } + + /** + * 在 [Request作用域] 里写入一个值 + */ + @Override + public SaStorageForDubbo3 set(String key, Object value) { + rpcContext.setObjectAttachment(key, value); + // 如果是token写入,则回传到Consumer端 + if(key.equals(SaTokenConsts.JUST_CREATED_NOT_PREFIX)) { + RpcContext.getServerContext().setAttachment(key, value); + } + return this; + } + + /** + * 在 [Request作用域] 里获取一个值 + */ + @Override + public Object get(String key) { + return rpcContext.getObjectAttachment(key); + } + + /** + * 在 [Request作用域] 里删除一个值 + */ + @Override + public SaStorageForDubbo3 delete(String key) { + rpcContext.removeAttachment(key); + return this; + } + +} diff --git a/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/util/SaTokenContextDubbo3Util.java b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/util/SaTokenContextDubbo3Util.java new file mode 100644 index 000000000..1301a88c8 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/util/SaTokenContextDubbo3Util.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.dubbo3.util; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.dubbo3.model.SaRequestForDubbo3; +import cn.dev33.satoken.context.dubbo3.model.SaResponseForDubbo3; +import cn.dev33.satoken.context.dubbo3.model.SaStorageForDubbo3; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import org.apache.dubbo.rpc.RpcContext; + + +/** + * SaTokenContext 上下文读写工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenContextDubbo3Util { + + /** + * 写入当前上下文 + * @param rpcContext / + */ + public static void setContext(RpcContext rpcContext) { + SaRequest saRequest = new SaRequestForDubbo3(RpcContext.getServiceContext()); + SaResponse saResponse = new SaResponseForDubbo3(RpcContext.getServiceContext()); + SaStorage saStorage = new SaStorageForDubbo3(RpcContext.getServiceContext()); + SaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage); + } + + /** + * 清除当前上下文 + */ + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); + } + +} diff --git a/sa-token-plugin/sa-token-dubbo3/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter b/sa-token-plugin/sa-token-dubbo3/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter new file mode 100644 index 000000000..aed4501f4 --- /dev/null +++ b/sa-token-plugin/sa-token-dubbo3/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter @@ -0,0 +1,3 @@ +saTokenDubbo3ConsumerFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ConsumerFilter +saTokenDubbo3ProviderFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ProviderFilter +saTokenDubbo3ContextFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ContextFilter \ No newline at end of file diff --git a/sa-token-plugin/sa-token-fastjson/pom.xml b/sa-token-plugin/sa-token-fastjson/pom.xml new file mode 100644 index 000000000..43d898a67 --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson/pom.xml @@ -0,0 +1,27 @@ + + + + sa-token-plugin + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + + sa-token-fastjson + sa-token-fastjson + sa-token integrate Fastjson + + + + cn.dev33 + sa-token-core + + + com.alibaba + fastjson + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson.java b/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson.java new file mode 100644 index 000000000..d38a0ed14 --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.json; + +import cn.dev33.satoken.util.SaFoxUtil; +import com.alibaba.fastjson.JSON; + +/** + * JSON 转换器, Fastjson 版实现 + * + * @author click33 + * @since 1.34.0 + */ +public class SaJsonTemplateForFastjson implements SaJsonTemplate { + + /** + * 序列化:对象 -> json 字符串 + */ + @Override + public String objectToJson(Object obj) { + if(SaFoxUtil.isEmpty(obj)) { + return null; + } + return JSON.toJSONString(obj); + } + + /** + * 反序列化:json 字符串 → 对象 + */ + @Override + public T jsonToObject(String jsonStr, Class type) { + if(SaFoxUtil.isEmpty(jsonStr)) { + return null; + } + return JSON.parseObject(jsonStr, type); + } + +} diff --git a/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson.java b/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson.java new file mode 100644 index 000000000..ba2eda051 --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.json.SaJsonTemplateForFastjson; +import cn.dev33.satoken.session.SaSessionForFastjsonCustomized; +import cn.dev33.satoken.strategy.SaStrategy; + +/** + * SaToken 插件安装:JSON 转换器 - Fastjson 版 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginForFastjson implements SaTokenPlugin { + + @Override + public void install() { + + // 设置JSON转换器:Fastjson 版 + SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson()); + + // 重写 SaSession 生成策略 + SaStrategy.instance.createSession = SaSessionForFastjsonCustomized::new; + + // 指定 SaSession 类型 + SaStrategy.instance.sessionClassType = SaSessionForFastjsonCustomized.class; + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/session/SaSessionForFastjsonCustomized.java b/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/session/SaSessionForFastjsonCustomized.java new file mode 100644 index 000000000..c578bbd3a --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/session/SaSessionForFastjsonCustomized.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session; + +import cn.dev33.satoken.util.SaFoxUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +/** + * Fastjson 定制版 SaSession,重写类型转换API + * + * @author click33 + * @since 1.34.0 + */ +public class SaSessionForFastjsonCustomized extends SaSession { + + private static final long serialVersionUID = -7600983549653130681L; + + /** + * 构建一个 SaSession 对象 + */ + public SaSessionForFastjsonCustomized() { + super(); + } + + /** + * 构建一个 SaSession 对象 + * @param id Session 的 id + */ + public SaSessionForFastjsonCustomized(String id) { + super(id); + } + + /** + * 取值 (指定转换类型) + * @param 泛型 + * @param key key + * @param cs 指定转换类型 + * @return 值 + */ + @Override + public T getModel(String key, Class cs) { + // 如果是想取出为基础类型 + Object value = get(key); + if(SaFoxUtil.isBasicType(cs)) { + return SaFoxUtil.getValueByType(value, cs); + } + // 为空提前返回 + if(valueIsNull(value)) { + return null; + } + // 如果是 JSONObject 类型直接转,否则先转为 String 再转 + if(value instanceof JSONObject) { + JSONObject jo = (JSONObject) value; + return jo.toJavaObject(cs); + } else { + return JSON.parseObject(value.toString(), cs); + } + } + +} diff --git a/sa-token-plugin/sa-token-fastjson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-fastjson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..9eddc672d --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForFastjson \ No newline at end of file diff --git a/sa-token-plugin/sa-token-fastjson2/pom.xml b/sa-token-plugin/sa-token-fastjson2/pom.xml new file mode 100644 index 000000000..f16b3027e --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson2/pom.xml @@ -0,0 +1,27 @@ + + + + sa-token-plugin + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + + sa-token-fastjson2 + sa-token-fastjson2 + sa-token integrate Fastjson2 + + + + cn.dev33 + sa-token-core + + + com.alibaba.fastjson2 + fastjson2 + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson2.java b/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson2.java new file mode 100644 index 000000000..14ca1859b --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson2.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.json; + + +import cn.dev33.satoken.util.SaFoxUtil; +import com.alibaba.fastjson2.JSON; + +/** + * JSON 转换器, Fastjson2 版实现 + * + * @author click33 + * @since 1.34.0 + */ +public class SaJsonTemplateForFastjson2 implements SaJsonTemplate { + + /** + * 序列化:对象 -> json 字符串 + */ + @Override + public String objectToJson(Object obj) { + if(SaFoxUtil.isEmpty(obj)) { + return null; + } + return JSON.toJSONString(obj); + } + + /** + * 反序列化:json 字符串 → 对象 + */ + @Override + public T jsonToObject(String jsonStr, Class type) { + if(SaFoxUtil.isEmpty(jsonStr)) { + return null; + } + return JSON.parseObject(jsonStr, type); + } + +} diff --git a/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson2.java b/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson2.java new file mode 100644 index 000000000..5eb39e6c4 --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson2.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.json.SaJsonTemplateForFastjson2; +import cn.dev33.satoken.session.SaSessionForFastjson2Customized; +import cn.dev33.satoken.strategy.SaStrategy; + +/** + * SaToken 插件安装:JSON 转换器 - Fastjson2 版 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginForFastjson2 implements SaTokenPlugin { + + @Override + public void install() { + + // 设置 JSON 转换器:Fastjson2 版 + SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson2()); + + // 重写 SaSession 生成策略 + SaStrategy.instance.createSession = SaSessionForFastjson2Customized::new; + + // 指定 SaSession 类型 + SaStrategy.instance.sessionClassType = SaSessionForFastjson2Customized.class; + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/session/SaSessionForFastjson2Customized.java b/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/session/SaSessionForFastjson2Customized.java new file mode 100644 index 000000000..0da9848e0 --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/session/SaSessionForFastjson2Customized.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session; + +import cn.dev33.satoken.util.SaFoxUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; + +/** + * Fastjson2 定制版 SaSession,重写类型转换API + * + * @author click33 + * @since 1.34.0 + */ +public class SaSessionForFastjson2Customized extends SaSession { + + private static final long serialVersionUID = -7600983549653130681L; + + /** + * 构建一个 SaSession 对象 + */ + public SaSessionForFastjson2Customized() { + super(); + } + + /** + * 构建一个 SaSession 对象 + * @param id Session 的 id + */ + public SaSessionForFastjson2Customized(String id) { + super(id); + } + + /** + * 取值 (指定转换类型) + * @param 泛型 + * @param key key + * @param cs 指定转换类型 + * @return 值 + */ + @Override + public T getModel(String key, Class cs) { + // 如果是想取出为基础类型 + Object value = get(key); + if(SaFoxUtil.isBasicType(cs)) { + return SaFoxUtil.getValueByType(value, cs); + } + // 为空提前返回 + if(valueIsNull(value)) { + return null; + } + // 如果是 JSONObject 类型直接转,否则先转为 String 再转 + if(value instanceof JSONObject) { + JSONObject jo = (JSONObject) value; + return jo.to(cs); + } else { + return JSON.parseObject(value.toString(), cs); + } + } +} diff --git a/sa-token-plugin/sa-token-fastjson2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-fastjson2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..26c68f758 --- /dev/null +++ b/sa-token-plugin/sa-token-fastjson2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForFastjson2 \ No newline at end of file diff --git a/sa-token-plugin/sa-token-forest/pom.xml b/sa-token-plugin/sa-token-forest/pom.xml new file mode 100644 index 000000000..4323ef748 --- /dev/null +++ b/sa-token-plugin/sa-token-forest/pom.xml @@ -0,0 +1,27 @@ + + + + sa-token-plugin + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + + sa-token-forest + sa-token-forest + sa-token integrate Forest + + + + cn.dev33 + sa-token-core + + + com.dtflys.forest + forest-core + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForForest.java b/sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForForest.java new file mode 100644 index 000000000..808ab2cdc --- /dev/null +++ b/sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForForest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.http; + +import cn.dev33.satoken.SaManager; +import com.dtflys.forest.Forest; + +import java.util.Map; + +/** + * Http 请求处理器, Forest 版实现 + * + * @author click33 + * @since 1.43.0 + */ +public class SaHttpTemplateForForest implements SaHttpTemplate { + + @Override + public String get(String url) { + SaManager.log.debug("发起请求,GET:{}", url); + String res = Forest.get(url).executeAsString(); + SaManager.log.debug("返回结果:{}", res); + return res; + } + + @Override + public String postByFormData(String url, Map params) { + SaManager.log.debug("发起请求,POST:{}\t参数:{}", url, params); + String res = Forest.post(url).addBody(params).executeAsString(); + SaManager.log.debug("返回结果:{}", res); + return res; + } + +} diff --git a/sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForForest.java b/sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForForest.java new file mode 100644 index 000000000..a6f2db60a --- /dev/null +++ b/sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForForest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.http.SaHttpTemplateForForest; +import com.dtflys.forest.config.ForestConfiguration; + +/** + * SaToken 插件安装:Http 请求处理器 - Forest 版 + * + * @author click33 + * @since 1.43.0 + */ +public class SaTokenPluginForForest implements SaTokenPlugin { + + @Override + public void install() { + // 关闭 Forest 默认日志打印 + ForestConfiguration.getDefaultConfiguration().setLogEnabled(false); + + // 设置 Forest 作为 Http 请求处理器 + SaManager.setSaHttpTemplate(new SaHttpTemplateForForest()); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-forest/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-forest/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..586967392 --- /dev/null +++ b/sa-token-plugin/sa-token-forest/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForForest \ No newline at end of file diff --git a/sa-token-plugin/sa-token-freemarker/pom.xml b/sa-token-plugin/sa-token-freemarker/pom.xml new file mode 100644 index 000000000..542e008b2 --- /dev/null +++ b/sa-token-plugin/sa-token-freemarker/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-freemarker + sa-token-freemarker + sa-token-freemarker + + + + + cn.dev33 + sa-token-core + + + + org.freemarker + freemarker + true + + + + diff --git a/sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateDirectiveModel.java b/sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateDirectiveModel.java new file mode 100644 index 000000000..73529dc4d --- /dev/null +++ b/sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateDirectiveModel.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.freemarker.dialect; + +import freemarker.core.Environment; +import freemarker.template.TemplateDirectiveBody; +import freemarker.template.TemplateDirectiveModel; +import freemarker.template.TemplateException; +import freemarker.template.TemplateModel; + +import java.io.IOException; +import java.util.Map; +import java.util.function.Function; + +/** + * Sa-Token Freemarker 标签模板指令模型 + * + * @author click33 + * @since 1.40.0 + */ +public class SaTokenTemplateDirectiveModel implements TemplateDirectiveModel { + + /* + * 参考资料: + * - https://blog.csdn.net/m0_64210833/article/details/135994864 + * - https://blog.csdn.net/qq_35752835/article/details/111321893 + */ + + /** + * 使用标签指令模板时,指定值的属性名 + */ + String attrName; + + /** + * 断言函数,返回 true 时标签内容显示,返回 false 时标签内容不显示 + */ + Function fun; + + public SaTokenTemplateDirectiveModel(String attrName, Function fun) { + this.attrName = attrName; + this.fun = fun; + } + + @Override + public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) + throws TemplateException, IOException { + + // 获取 value + Object obj = map.get(attrName); + String value = obj == null ? null : obj.toString(); + + // 使用断言函数判断是否显示标签内容 + if(this.fun.apply(value)) { + templateDirectiveBody.render(environment.getOut()); + } + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateModel.java b/sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateModel.java new file mode 100644 index 000000000..ca71a445b --- /dev/null +++ b/sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateModel.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.freemarker.dialect; + +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import freemarker.template.SimpleHash; + +import java.util.List; + +/** + * Sa-Token Freemarker 标签模板模型 + * + * @author click33 + * @since 1.40.0 + */ +public class SaTokenTemplateModel extends SimpleHash { + + /** + * 默认值属性名 + */ + public static final String DEFAULT_ATTR_NAME = "value"; + + /** + * 底层使用的 StpLogic + */ + public StpLogic stpLogic; + + /** + * 使用默认参数注册标签模板模型 + */ + public SaTokenTemplateModel() { + this(DEFAULT_ATTR_NAME, StpUtil.stpLogic); + } + + /** + * 构造标签模板模型,使用自定义参数 + * + * @param stpLogic 使用的 StpLogic 对象 + */ + public SaTokenTemplateModel(StpLogic stpLogic) { + this(DEFAULT_ATTR_NAME, stpLogic); + } + + /** + * 构造标签模板模型,使用自定义参数 + * + * @param attrName 属性名 + * @param stpLogic 使用的 StpLogic 对象 + */ + public SaTokenTemplateModel(String attrName, StpLogic stpLogic) { + this.stpLogic = stpLogic; + + // 登录判断 + put("login", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.isLogin())); + put("notLogin", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.isLogin())); + + // 角色判断 + put("hasRole", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRole(value))); + put("hasRoleAnd", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRoleAnd(toArray(value)))); + put("hasRoleOr", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRoleOr(toArray(value)))); + put("notRole", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasRole(value))); + put("lackRole", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasRole(value))); + + // 权限判断 + put("hasPermission", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermission(value))); + put("hasPermissionAnd", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermissionAnd(toArray(value)))); + put("hasPermissionOr", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermissionOr(toArray(value)))); + put("notPermission", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasPermission(value))); + put("lackPermission", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasPermission(value))); + + } + + /** + * String 转 Array + * @param str 字符串 + * @return 数组 + */ + public String[] toArray(String str) { + List list = SaFoxUtil.convertStringToList(str); + return list.toArray(new String[0]); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-grpc/pom.xml b/sa-token-plugin/sa-token-grpc/pom.xml new file mode 100644 index 000000000..312f17858 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-grpc + sa-token-grpc + sa-token-grpc + + + 8 + 8 + UTF-8 + + + + + net.devh + grpc-spring-boot-starter + + + cn.dev33 + sa-token-core + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/constants/GrpcContextConstants.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/constants/GrpcContextConstants.java new file mode 100644 index 000000000..97887058d --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/constants/GrpcContextConstants.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.constants; + +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import io.grpc.Metadata; + +/** + * 常量 + * + * @author lym + * @since 1.34.0 + */ +public class GrpcContextConstants { + public static final Metadata.Key SA_SAME_TOKEN = + Metadata.Key.of(SaSameUtil.SAME_TOKEN, Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key SA_JUST_CREATED_NOT_PREFIX = + Metadata.Key.of(SaTokenConsts.JUST_CREATED_NOT_PREFIX, Metadata.ASCII_STRING_MARSHALLER); + +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/context/SaTokenGrpcContext.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/context/SaTokenGrpcContext.java new file mode 100644 index 000000000..107ae9d38 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/context/SaTokenGrpcContext.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.context; + +import io.grpc.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author lym + * @since 1.34.0 + **/ +public class SaTokenGrpcContext { + /** + * grpc请求上下文。请求完成后会由grpc自动清空 + * + * @see Contexts#interceptCall(Context, ServerCall, Metadata, ServerCallHandler) + */ + private static final Context.Key> SA_TOKEN_CONTEXT_KEY = + Context.key("sa-token-context"); + + public static Object get(String key) { + return SA_TOKEN_CONTEXT_KEY.get().get(key); + } + + public static void set(String key, Object value) { + SA_TOKEN_CONTEXT_KEY.get().put(key, value); + } + + public static void removeKey(String key) { + SA_TOKEN_CONTEXT_KEY.get().remove(key); + } + + public static Map getContext() { + return SA_TOKEN_CONTEXT_KEY.get(); + } + + public static boolean isNotNull() { + return SA_TOKEN_CONTEXT_KEY.get() != null; + } + + public static Context create() { + return Context.current().withValue(SaTokenGrpcContext.SA_TOKEN_CONTEXT_KEY, new HashMap<>()); + } +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenContextGrpcServerInterceptor.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenContextGrpcServerInterceptor.java new file mode 100644 index 000000000..668606c70 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenContextGrpcServerInterceptor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.interceptor; + +import cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext; +import io.grpc.*; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import org.springframework.core.Ordered; + +/** + * 处理请求前,创建上下文 + * + * @author lym + * @since 1.34.0 + */ +@GrpcGlobalServerInterceptor +public class SaTokenContextGrpcServerInterceptor implements ServerInterceptor, Ordered { + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + Context ctx = SaTokenGrpcContext.create(); + return Contexts.interceptCall(ctx, call, headers, next); + } + + /** + * 必须最先创建上下文,后面的拦截器才能获取到上下文 + */ + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcClientInterceptor.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcClientInterceptor.java new file mode 100644 index 000000000..2a43ecb6d --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcClientInterceptor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.interceptor; + +import org.springframework.core.Ordered; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaTokenContextDefaultImpl; +import cn.dev33.satoken.context.grpc.constants.GrpcContextConstants; +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; + + +/** + * 客户端请求的时候,带上token + * + * @author lym + * @since 1.34.0 + */ +@GrpcGlobalClientInterceptor +public class SaTokenGrpcClientInterceptor implements ClientInterceptor, Ordered { + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + + // 追加 Same-Token 参数 + if (SaManager.getConfig().getCheckSameToken()) { + headers.put(GrpcContextConstants.SA_SAME_TOKEN, SaSameUtil.getToken()); + } + + // 调用前,传递会话Token + String tokenValue = StpUtil.getTokenValue(); + if (SaFoxUtil.isNotEmpty(tokenValue) + && SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) { + headers.put(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX, tokenValue); + } + + super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { + /** + * 服务端结束响应后,解析回传的Token值 + */ + @Override + public void onClose(Status status, Metadata responseHeader) { + StpUtil.setTokenValue(responseHeader.get(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX)); + super.onClose(status, responseHeader); + } + }, headers); + } + }; + } + + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcServerInterceptor.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcServerInterceptor.java new file mode 100644 index 000000000..3a93f906d --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcServerInterceptor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.interceptor; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.grpc.constants.GrpcContextConstants; +import cn.dev33.satoken.context.grpc.util.SaTokenContextGrpcUtil; +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import io.grpc.*; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +/** + * 鉴权,设置token + * + * @author lym + * @since 1.34.0 + **/ +@GrpcGlobalServerInterceptor +public class SaTokenGrpcServerInterceptor implements ServerInterceptor { + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + try{ + // 初始化上下文 + SaTokenContextGrpcUtil.setContext(); + + // RPC 调用鉴权 + if (SaManager.getConfig().getCheckSameToken()) { + String sameToken = headers.get(GrpcContextConstants.SA_SAME_TOKEN); + SaSameUtil.checkToken(sameToken); + } + String tokenFromClient = headers.get(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX); + StpUtil.setTokenValue(tokenFromClient); + + return next.startCall(new ForwardingServerCall.SimpleForwardingServerCall(call) { + /** + * 结束响应时,若本服务生成了新token,将其传回客户端 + */ + @Override + public void close(Status status, Metadata responseHeaders) { + String justCreateToken = StpUtil.getTokenValue(); + if (!SaFoxUtil.equals(justCreateToken, tokenFromClient) && SaFoxUtil.isNotEmpty(justCreateToken)) { + responseHeaders.put(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX, justCreateToken); + } + super.close(status, responseHeaders); + } + }, headers); + }finally { + // 清除上下文 + SaTokenContextGrpcUtil.clearContext(); + } + } +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaRequestForGrpc.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaRequestForGrpc.java new file mode 100644 index 000000000..a3409d8b1 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaRequestForGrpc.java @@ -0,0 +1,147 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.model; + +import cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext; +import cn.dev33.satoken.context.model.SaRequest; + +import java.util.Collection; +import java.util.Map; + +/** + * Request for grpc + * + * @author lym + * @since 1.34.0 + */ +public class SaRequestForGrpc implements SaRequest { + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return SaTokenGrpcContext.getContext(); + } + + /** + * 在 [请求体] 里获取一个值 + */ + @Override + public String getParam(String name) { + // 不传播 url 参数 + return null; + } + + /** + * 获取 [请求体] 里提交的所有参数名称 + * @return 参数名称列表 + */ + @Override + public Collection getParamNames(){ + return null; + } + + /** + * 获取 [请求体] 里提交的所有参数 + * @return 参数列表 + */ + @Override + public Map getParamMap(){ + return null; + } + + /** + * 在 [请求头] 里获取一个值 + */ + @Override + public String getHeader(String name) { + // 不传播 header 参数 + return null; + } + + /** + * 在 [Cookie作用域] 里获取一个值 + */ + @Override + public String getCookieValue(String name) { + // 不传播 cookie 参数 + return null; + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name){ + // 不传播 cookie 参数 + return null; + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name){ + // 不传播 cookie 参数 + return null; + } + + /** + * 返回当前请求path (不包括上下文名称) + */ + @Override + public String getRequestPath() { + // 不传播 requestPath + return null; + } + + /** + * 返回当前请求的url,例:http://xxx.com/test + * + * @return see note + */ + public String getUrl() { + // 不传播 url + return null; + } + + /** + * 返回当前请求的类型 + */ + @Override + public String getMethod() { + // 不传播 method + return null; + } + + @Override + public String getHost() { + return null; + } + + /** + * 转发请求 + */ + @Override + public Object forward(String path) { + // 不传播 forward 动作 + return null; + } + +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaResponseForGrpc.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaResponseForGrpc.java new file mode 100644 index 000000000..c093346e5 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaResponseForGrpc.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.model; + +import cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext; +import cn.dev33.satoken.context.model.SaResponse; + +/** + * Response for grpc + * + * @author lym + * @since 1.34.0 + */ +public class SaResponseForGrpc implements SaResponse { + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return SaTokenGrpcContext.getContext(); + } + + /** + * 设置响应状态码 + */ + @Override + public SaResponse setStatus(int sc) { + // 不回传 status 状态 + return this; + } + + /** + * 在响应头里写入一个值 + */ + @Override + public SaResponse setHeader(String name, String value) { + // 不回传 header响应头 + return this; + } + + /** + * 在响应头里添加一个值 + * + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + public SaResponse addHeader(String name, String value) { + // 不回传 header响应头 + return this; + } + + /** + * 重定向 + */ + @Override + public Object redirect(String url) { + // 不回传 重定向 动作 + return null; + } + + +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaStorageForGrpc.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaStorageForGrpc.java new file mode 100644 index 000000000..b39cf5729 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaStorageForGrpc.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.model; + +import cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext; +import cn.dev33.satoken.context.model.SaStorage; + +/** + * Storage for grpc + * + * @author lym + * @since 1.34.0 + */ +public class SaStorageForGrpc implements SaStorage { + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return SaTokenGrpcContext.getContext(); + } + + /** + * 在 [Request作用域] 里写入一个值 + */ + @Override + public SaStorage set(String key, Object value) { + SaTokenGrpcContext.set(key, value); + return this; + } + + /** + * 在 [Request作用域] 里获取一个值 + */ + @Override + public Object get(String key) { + return SaTokenGrpcContext.get(key); + } + + /** + * 在 [Request作用域] 里删除一个值 + */ + @Override + public SaStorage delete(String key) { + SaTokenGrpcContext.removeKey(key); + return this; + } + +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/util/SaTokenContextGrpcUtil.java b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/util/SaTokenContextGrpcUtil.java new file mode 100644 index 000000000..42fe0b364 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/util/SaTokenContextGrpcUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.context.grpc.util; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.grpc.model.SaRequestForGrpc; +import cn.dev33.satoken.context.grpc.model.SaResponseForGrpc; +import cn.dev33.satoken.context.grpc.model.SaStorageForGrpc; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; + + +/** + * SaTokenContext 上下文读写工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenContextGrpcUtil { + + /** + * 写入当前上下文 + */ + public static void setContext() { + SaRequest saRequest = new SaRequestForGrpc(); + SaResponse saResponse = new SaResponseForGrpc(); + SaStorage saStorage = new SaStorageForGrpc(); + SaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage); + } + + /** + * 清除当前上下文 + */ + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); + } + +} diff --git a/sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..59ef7b396 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcClientInterceptor,\ + cn.dev33.satoken.context.grpc.interceptor.SaTokenContextGrpcServerInterceptor,\ + cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcServerInterceptor,\ + cn.dev33.satoken.context.grpc.SaTokenSecondContextCreatorForGrpc \ No newline at end of file diff --git a/sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..cf5cba505 --- /dev/null +++ b/sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcClientInterceptor +cn.dev33.satoken.context.grpc.interceptor.SaTokenContextGrpcServerInterceptor +cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcServerInterceptor +cn.dev33.satoken.context.grpc.SaTokenSecondContextCreatorForGrpc \ No newline at end of file diff --git a/sa-token-plugin/sa-token-hutool-timed-cache/pom.xml b/sa-token-plugin/sa-token-hutool-timed-cache/pom.xml new file mode 100644 index 000000000..a74238706 --- /dev/null +++ b/sa-token-plugin/sa-token-hutool-timed-cache/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-hutool-timed-cache + sa-token-hutool-timed-cache + sa-token integrate hutool-TimedCache + + + + cn.dev33 + sa-token-core + + + cn.hutool + hutool-cache + + + + diff --git a/sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForHutoolTimedCache.java b/sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForHutoolTimedCache.java new file mode 100644 index 000000000..991555bfd --- /dev/null +++ b/sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForHutoolTimedCache.java @@ -0,0 +1,173 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; + + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.CacheObj; +import cn.hutool.cache.impl.TimedCache; + +import java.util.Iterator; +import java.util.List; + +/** + * Sa-Token 持久层接口(基于 Hutool-TimedCache,系统重启后数据丢失) + * + * @author click33 + * @since 1.38.0 + */ +public class SaTokenDaoForHutoolTimedCache implements SaTokenDaoByStringFollowObject { + + // + /** + * 底层缓存对象: + * 参数填1000,代表默认ttl为1000毫秒,实际上此参数意义不大,因为后续每个值都会单独设置自己的ttl值 + */ + public TimedCache timedCache = CacheUtil.newTimedCache(1000); + + + // ------------------------ Object 读写操作 + + @Override + public Object getObject(String key) { + // 第二个参数代表:是否刷新最后访问时间 + // 设置为false,因为我们不需要刷新最后访问时间,只需要取值即可 + return timedCache.get(key, false); + } + + @Override + public T getObject(String key, Class classType) { + return (T) getObject(key); + } + + @Override + public void setObject(String key, Object object, long timeout) { + if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // 如果为永不过期 + // 在 sa-token 中,-1 代表永不过期 + // 在 hutool-TimedCache 中,0 代表永不过期 + // 为了适应 hutool-TimedCache 规范,这里将 -1 转换为 0 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + timedCache.put(key, object, 0); + return; + } + // 正常情况 + timedCache.put(key, object, timeout * 1000); + } + + @Override + public void updateObject(String key, Object object) { + long expire = getObjectTimeout(key); + // -2 = 无此键 + if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.setObject(key, object, expire); + } + + @Override + public void deleteObject(String key) { + timedCache.remove(key); + } + + @Override + public long getObjectTimeout(String key) { + return getKeyTimeout(key); + } + + @Override + public void updateObjectTimeout(String key, long timeout) { + // $$待优化:对一个不存在的key进行修改timeout操作时,可能会造成一些意外数据,待进一步测试 + this.setObject(key, this.getObject(key), timeout); + } + + + // ------------------------ Session 读写操作 + // 使用接口默认实现 + + + // --------- 会话管理 + + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + return SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType); + } + + + + // --------- 过期时间相关操作 + + /** + * 获取指定 key 的剩余存活时间 (单位:秒) + * @param key 指定 key + * @return 这个 key 的剩余存活时间,返回-1=永不过期,返回-2=无此键 + */ + long getKeyTimeout(String key) { + final Iterator> values = timedCache.cacheObjIterator(); + CacheObj co; + while (values.hasNext()) { + co = values.next(); + if(co.getKey().equals(key)) { + long ttl = co.getTtl(); + // 在 Hutool-TimedCache 中,ttl=0 (或<0) 代表永不过期,统一返回 Sa-Token 可以理解的 -1 + if(ttl <= 0) { + return NEVER_EXPIRE; + } + // 不为 0,那就计算一下剩余有效期 + // 单位:毫秒 + long timeout = ttl - (System.currentTimeMillis() - co.getLastAccess()); + if(timeout < 0) { + timeout = 0; + } + // 转秒返回 + return timeout / 1000; + } + } + // 代码至此,说明缓存中没有这个值 + return NOT_VALUE_EXPIRE; + } + + // --------- 定时清理过期数据 + + /** + * 组件被安装时,开始刷新数据线程 + */ + @Override + public void init() { + // 定时清理间隔 + int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod(); + // 配置为<=0代表不启用定时清理 + if(dataRefreshPeriod <= 0) { + return; + } + // 启用定时清理(转毫秒) + timedCache.schedulePrune(dataRefreshPeriod * 1000L); + } + + /** + * 组件被卸载时,结束定时任务,不再定时清理过期数据 + */ + @Override + public void destroy() { + timedCache.cancelPruneSchedule(); + } + +} diff --git a/sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForHutoolCache.java b/sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForHutoolCache.java new file mode 100644 index 000000000..7e063816b --- /dev/null +++ b/sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForHutoolCache.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDaoForHutoolTimedCache; + +/** + * SaToken 插件安装:DAO 扩展 - Hutool-TimedCache 版 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginForHutoolCache implements SaTokenPlugin { + + @Override + public void install() { + + SaManager.setSaTokenDao(new SaTokenDaoForHutoolTimedCache()); + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-hutool-timed-cache/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-hutool-timed-cache/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..c0673e0eb --- /dev/null +++ b/sa-token-plugin/sa-token-hutool-timed-cache/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForHutoolCache \ No newline at end of file diff --git a/sa-token-plugin/sa-token-jackson/pom.xml b/sa-token-plugin/sa-token-jackson/pom.xml new file mode 100644 index 000000000..291293aef --- /dev/null +++ b/sa-token-plugin/sa-token-jackson/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-jackson + sa-token-jackson + sa-token-jackson + + + + + cn.dev33 + sa-token-core + + + + com.fasterxml.jackson.core + jackson-databind + true + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + true + + + + + + diff --git a/sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson.java b/sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson.java new file mode 100644 index 000000000..9d216daa3 --- /dev/null +++ b/sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson.java @@ -0,0 +1,170 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.json; + +import cn.dev33.satoken.exception.SaJsonConvertException; +import cn.dev33.satoken.util.SaFoxUtil; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +/** + * JSON 转换器, Jackson 版实现 + * + * @author click33 + * @since 1.34.0 + */ +public class SaJsonTemplateForJackson implements SaJsonTemplate { + + public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + public static final String DATE_PATTERN = "yyyy-MM-dd"; + public static final String TIME_PATTERN = "HH:mm:ss"; + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN); + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN); + + /** + * 底层 Mapper 对象 + */ + public ObjectMapper objectMapper = new ObjectMapper(); + + public SaJsonTemplateForJackson() { + + // 1、使 objectMapper 序列化时带上类型信息,以便该 json 字符串可以成功反序列化 + // 构建反序列化限制器,此处可以限制只允许指定类型或指定包下的类型才可以反序列化,此处指定所有类型都可以反序列化 + PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder() + // 允许所有子类型反序列化(即反序列化时遇到的类) + .allowIfSubType(Object.class) + // 允许所有基类型反序列化(如 Object、自定义抽象类) + .allowIfBaseType(Object.class) + .build(); + // 启用全局默认类型(嵌入类型信息) + objectMapper.activateDefaultTyping( + ptv, + // 对非 final 类嵌入类型信息 + ObjectMapper.DefaultTyping.NON_FINAL, + // 类型信息以属性形式存在("@class") + JsonTypeInfo.As.PROPERTY + ); + + // 2、使空 bean 在序列化时也能记录类型信息,而不是只序列化成 {} + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + + // 3、配置 [ 忽略未知字段 ] + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + // 4、配置 [ 时间类型转换 ] + JavaTimeModule timeModule = new JavaTimeModule(); + // LocalDateTime序列化与反序列化 + timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + // LocalDate序列化与反序列化 + timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER)); + timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER)); + // LocalTime序列化与反序列化 + timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER)); + timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER)); + // + this.objectMapper.registerModule(timeModule); + + } + + /** + * 序列化:对象 -> json 字符串 + */ + @Override + public String objectToJson(Object obj) { + if(SaFoxUtil.isEmpty(obj)) { + return null; + } + try { + if(obj instanceof Map) { + return mapObjectMapper.writeValueAsString(obj); + } + return objectMapper.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new SaJsonConvertException(e); + } + } + + /** + * 反序列化:json 字符串 → 对象 + */ + @Override + public T jsonToObject(String jsonStr, Class type) { + if(SaFoxUtil.isEmpty(jsonStr)) { + return null; + } + try { + return objectMapper.readValue(jsonStr, type); + } catch (JsonProcessingException e) { + throw new SaJsonConvertException(e); + } + } + + /* + * 由于构造方法中的如下代码: + * ObjectMapper.DefaultTyping.NON_FINAL, + * 导致 objectMapper 对所有非 final 类型的反序列化均要求提供 @class 信息。 + * + * 例如: + * 一个简单的字符串 {"name": "zhangsan"} 将无法反序列化为 Map 对象,因为这个字符串上没有提供 @class 信息。 + * + * 尝试诸多方案,均未能解决此问题。 + * + * 因此,以下代码将为 Map 的反序列化提供一个独立干净的 mapObjectMapper 对象,保证其不受构造方法中关于类型配置的影响。 + * + */ + + /** + * 处理 Map 的序列化与反序列化 + */ + public ObjectMapper mapObjectMapper = new ObjectMapper(); + + /** + * 将 json 字符串解析为 Map + */ + @Override + public Map jsonToMap(String jsonStr) { + if(SaFoxUtil.isEmpty(jsonStr)) { + return null; + } + try { + @SuppressWarnings("unchecked") + Map map = mapObjectMapper.readValue(jsonStr, Map.class); + return map; + } catch (JsonProcessingException e) { + throw new SaJsonConvertException(e); + } + } +} diff --git a/sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson.java b/sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson.java new file mode 100644 index 000000000..c71873c95 --- /dev/null +++ b/sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; +import cn.dev33.satoken.json.SaJsonTemplateForJackson; + +/** + * SaToken 插件安装:JSON 转换器 (Jackson 版) + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginForJackson implements SaTokenPlugin { + + @Override + public void install() { + // 只有在未提供自定义的 json 解析器时才会生效,给于其较弱的优先级 + if(SaManager.getSaJsonTemplate().getClass() == SaJsonTemplateDefaultImpl.class){ + SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson()); + } + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-jackson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-jackson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..3e1545174 --- /dev/null +++ b/sa-token-plugin/sa-token-jackson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForJackson \ No newline at end of file diff --git a/sa-token-plugin/sa-token-jackson3/pom.xml b/sa-token-plugin/sa-token-jackson3/pom.xml new file mode 100644 index 000000000..343c4cc6d --- /dev/null +++ b/sa-token-plugin/sa-token-jackson3/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-jackson3 + sa-token-jackson3 + sa-token-jackson3: Sa-Token 与 Jackson 3 整合 + + + + + cn.dev33 + sa-token-core + + + + tools.jackson.core + jackson-databind + true + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + + diff --git a/sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson3.java b/sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson3.java new file mode 100644 index 000000000..febef68dd --- /dev/null +++ b/sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson3.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.json; + +import cn.dev33.satoken.exception.SaJsonConvertException; +import cn.dev33.satoken.util.SaFoxUtil; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.DefaultTyping; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import tools.jackson.databind.jsontype.PolymorphicTypeValidator; +import tools.jackson.databind.json.JsonMapper; + +import java.util.Map; + +/** + * JSON 转换器,Jackson 3 版实现 + * + * @author click33 + * @since 1.45.0 + */ +public class SaJsonTemplateForJackson3 implements SaJsonTemplate { + + /** + * 底层 Mapper 对象(带多态类型信息,用于 Session 等复杂对象序列化) + */ + public final JsonMapper objectMapper; + + /** + * 处理 Map 的 Mapper(无多态类型配置,用于简单 JSON 解析) + */ + public final JsonMapper mapObjectMapper; + + public SaJsonTemplateForJackson3() { + // 1、构建反序列化限制器(PolymorphicTypeValidator),限制哪些类型可以被自动多态反序列化 + // 这里允许所有 Object 类型和其子类型参与多态反序列化,从而支持复杂对象(如 SaSession)的序列化、反序列化 + PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder() + .allowIfSubType(Object.class) // 允许 Object.class 的子类型被反序列化 + .allowIfBaseType(Object.class) // 允许 Object.class 作为基类型(在启用多态时) + .build(); // 构建 Validator 实例 + + // 2、通过 JsonMapper 的 builder 模式创建 objectMapper(用于带多态类型信息的 JSON 处理,Jackson3 默认不可变需用构建器) + this.objectMapper = JsonMapper.builder() + // 启用默认多态类型处理,并以 "@class" 作为写入类型信息的属性名(即持久化时包含类型字段),仅对非 final 类型生效 + .activateDefaultTypingAsProperty(ptv, DefaultTyping.NON_FINAL, "@class") + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) // 序列化时如果 bean 没有属性,则不抛出异常 + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) // 反序列化时如果有未知属性,也不抛出异常(兼容性更好) + .build(); // 构建真正的 JsonMapper 实例 + + // 3、创建 mapObjectMapper,用于简单类型(如 Map)的序列化和反序列化,和 objectMapper 独立,且不启用多态类型 + this.mapObjectMapper = JsonMapper.builder() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) // 序列化时如果 bean 没有属性,则不抛出异常 + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) // 反序列化时如果有未知属性,也不抛出异常,避免出错 + .build(); // 构建用于处理简单场景的 JsonMapper 实例 + } + + /** + * 序列化:对象 -> json 字符串 + */ + @Override + public String objectToJson(Object obj) { + if (SaFoxUtil.isEmpty(obj)) { + return null; + } + try { + if (obj instanceof Map) { + return mapObjectMapper.writeValueAsString(obj); + } + return objectMapper.writeValueAsString(obj); + } catch (JacksonException e) { + throw new SaJsonConvertException(e); + } + } + + /** + * 反序列化:json 字符串 → 对象 + */ + @Override + public T jsonToObject(String jsonStr, Class type) { + if (SaFoxUtil.isEmpty(jsonStr)) { + return null; + } + try { + return objectMapper.readValue(jsonStr, type); + } catch (JacksonException e) { + throw new SaJsonConvertException(e); + } + } + + /** + * 将 json 字符串解析为 Map + */ + @Override + public Map jsonToMap(String jsonStr) { + if (SaFoxUtil.isEmpty(jsonStr)) { + return null; + } + try { + @SuppressWarnings("unchecked") + Map map = mapObjectMapper.readValue(jsonStr, Map.class); + return map; + } catch (JacksonException e) { + throw new SaJsonConvertException(e); + } + } + +} diff --git a/sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson3.java b/sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson3.java new file mode 100644 index 000000000..5041e6479 --- /dev/null +++ b/sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson3.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; +import cn.dev33.satoken.json.SaJsonTemplateForJackson3; + +/** + * SaToken 插件安装:JSON 转换器 (Jackson 3 版) + * + * @author click33 + * @since 1.45.0 + */ +public class SaTokenPluginForJackson3 implements SaTokenPlugin { + + @Override + public void install() { + // 只有在未提供自定义的 json 解析器时才会生效,给予其较弱的优先级 + if (SaManager.getSaJsonTemplate().getClass() == SaJsonTemplateDefaultImpl.class) { + SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3()); + } + } +} diff --git a/sa-token-plugin/sa-token-jackson3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-jackson3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..bee1a12e0 --- /dev/null +++ b/sa-token-plugin/sa-token-jackson3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForJackson3 diff --git a/sa-token-plugin/sa-token-jwt/pom.xml b/sa-token-plugin/sa-token-jwt/pom.xml new file mode 100644 index 000000000..eb878f6a9 --- /dev/null +++ b/sa-token-plugin/sa-token-jwt/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-jwt + sa-token-jwt + sa-token-jwt + + + + + cn.dev33 + sa-token-core + + + + cn.hutool + hutool-jwt + + + + diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java new file mode 100644 index 000000000..17eec8bf8 --- /dev/null +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java @@ -0,0 +1,332 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jwt; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.jwt.error.SaJwtErrorCode; +import cn.dev33.satoken.jwt.exception.SaJwtException; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTException; +import cn.hutool.jwt.signers.JWTSigner; +import cn.hutool.jwt.signers.JWTSignerUtil; + +import java.util.Map; +import java.util.Objects; + +/** + * jwt 操作模板方法封装 + * + * @author click33 + * @since 1.31.0 + */ +public class SaJwtTemplate { + + /** + * key:账号类型 + */ + public static final String LOGIN_TYPE = "loginType"; + + /** + * key:账号id + */ + public static final String LOGIN_ID = "loginId"; + + /** + * key:登录设备类型 + */ + public static final String DEVICE_TYPE = "deviceType"; + + /** + * key:有效截止期 (时间戳) + */ + public static final String EFF = "eff"; + + /** + * key:乱数 ( 混入随机字符串,防止每次生成的 token 都是一样的 ) + */ + public static final String RN_STR = "rnStr"; + + /** + * 当有效期被设为此值时,代表永不过期 + */ + public static final long NEVER_EXPIRE = SaTokenDao.NEVER_EXPIRE; + + /** + * 表示一个值不存在 + */ + public static final long NOT_VALUE_EXPIRE = SaTokenDao.NOT_VALUE_EXPIRE; + + // ------ 创建 + + /** + * 创建 jwt (简单方式) + * + * @param loginType 登录类型 + * @param loginId 账号id + * @param extraData 扩展数据 + * @param keyt 秘钥 + * @return jwt-token + */ + public String createToken(String loginType, Object loginId, Map extraData, String keyt) { + + // 构建 + JWT jwt = JWT.create() + .setPayload(LOGIN_TYPE, loginType) + .setPayload(LOGIN_ID, loginId) + // 塞入一个随机字符串,防止同账号下每次生成的 token 都一样的 + .setPayload(RN_STR, SaFoxUtil.getRandomString(32)) + .addPayloads(extraData) + ; + + // 返回 + return generateToken(jwt, keyt); + } + + /** + * 创建 jwt (全参数方式) + * + * @param loginType 账号类型 + * @param loginId 账号id + * @param deviceType 设备类型 + * @param timeout token有效期 (单位 秒) + * @param extraData 扩展数据 + * @param keyt 秘钥 + * @return jwt-token + */ + public String createToken(String loginType, Object loginId, String deviceType, + long timeout, Map extraData, String keyt) { + + // 计算 eff 有效期: + // 如果 timeout 指定为 -1,那么 eff 也为 -1,代表永不过期 + // 如果 timeout 指定为一个具体的值,那么 eff 为 13 位时间戳,代表此 token 到期的时间 + long effTime = timeout; + if(timeout != NEVER_EXPIRE) { + effTime = timeout * 1000 + System.currentTimeMillis(); + } + + // 创建 + JWT jwt = JWT.create() + .setPayload(LOGIN_TYPE, loginType) + .setPayload(LOGIN_ID, loginId) + .setPayload(DEVICE_TYPE, deviceType) + .setPayload(EFF, effTime) + // 塞入一个随机字符串,防止同账号同一毫秒下每次生成的 token 都一样的 + .setPayload(RN_STR, SaFoxUtil.getRandomString(32)) + .addPayloads(extraData); + + // 返回 + return generateToken(jwt, keyt); + } + + /** + * 为 JWT 对象和 keyt 秘钥,生成 token 字符串 + * + * @param jwt JWT构建对象 + * @param keyt 秘钥 + * @return 根据 JWT 对象和 keyt 秘钥,生成的 token 字符串 + */ + public String generateToken (JWT jwt, String keyt) { + return jwt.setSigner(createSigner(keyt)).sign(); + } + + /** + * 返回 jwt 使用的签名算法 + * + * @param keyt 秘钥 + * @return / + */ + public JWTSigner createSigner (String keyt) { + return JWTSignerUtil.hs256(keyt.getBytes()); + } + + // ------ 解析 + + /** + * jwt 解析 + * + * @param token Jwt-Token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @param isCheckTimeout 是否校验 timeout 字段 + * @return 解析后的jwt 对象 + */ + public JWT parseToken(String token, String loginType, String keyt, boolean isCheckTimeout) { + + // 秘钥不可以为空 + if(SaFoxUtil.isEmpty(keyt)) { + throw new SaJwtException("请配置 jwt 秘钥"); + } + + // 如果token为null + if(token == null) { + throw new SaJwtException("jwt 字符串不可为空"); + } + + // 解析 + JWT jwt; + try { + jwt = JWT.of(token); + } catch (JWTException | JSONException e) { + throw new SaJwtException("jwt 解析失败:" + token, e).setCode(SaJwtErrorCode.CODE_30201); + } + JSONObject payloads = jwt.getPayloads(); + + // 校验 Token 签名 + boolean verify = jwt.setSigner(createSigner(keyt)).verify(); + if( ! verify) { + throw new SaJwtException("jwt 签名无效:" + token).setCode(SaJwtErrorCode.CODE_30202); + } + + // 校验 loginType + if( ! Objects.equals(loginType, payloads.getStr(LOGIN_TYPE))) { + throw new SaJwtException("jwt loginType 无效:" + token).setCode(SaJwtErrorCode.CODE_30203); + } + + // 校验 Token 有效期 + if(isCheckTimeout) { + Long effTime = payloads.getLong(EFF, 0L); + if(effTime != NEVER_EXPIRE) { + if(effTime == null || effTime < System.currentTimeMillis()) { + throw new SaJwtException("jwt 已过期:" + token).setCode(SaJwtErrorCode.CODE_30204); + } + } + } + + // 返回 + return jwt; + } + + /** + * 获取 jwt 数据载荷 (校验 sign、loginType、timeout) + * @param token token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 载荷 + */ + public JSONObject getPayloads(String token, String loginType, String keyt) { + return parseToken(token, loginType, keyt, true).getPayloads(); + } + + /** + * 获取 jwt 数据载荷 (校验 sign、loginType,不校验 timeout) + * @param token token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 载荷 + */ + public JSONObject getPayloadsNotCheck(String token, String loginType, String keyt) { + return parseToken(token, loginType, keyt, false).getPayloads(); + } + + /** + * 获取 jwt 代表的账号id + * @param token Token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 值 + */ + public Object getLoginId(String token, String loginType, String keyt) { + return getPayloads(token, loginType, keyt).get(LOGIN_ID); + } + + /** + * 获取 jwt 代表的账号id (未登录时返回null) + * @param token Token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 值 + */ + public Object getLoginIdOrNull(String token, String loginType, String keyt) { + try { + return getPayloads(token, loginType, keyt).get(LOGIN_ID); + } catch (SaJwtException e) { + return null; + } + } + + /** + * 获取 jwt 剩余有效期 + * @param token JwtToken值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 值 + */ + public long getTimeout(String token, String loginType, String keyt) { + + // 如果token为null + if(token == null) { + return NOT_VALUE_EXPIRE; + } + + // 取出数据 + JWT jwt; + try { + jwt = JWT.of(token); + } catch (JWTException e) { + // 解析失败 + return NOT_VALUE_EXPIRE; + } + JSONObject payloads = jwt.getPayloads(); + + // 如果签名无效 + boolean verify = jwt.setSigner(createSigner(keyt)).verify(); + if( ! verify) { + return NOT_VALUE_EXPIRE; + } + + // 如果 loginType 无效 + if( ! Objects.equals(loginType, payloads.getStr(LOGIN_TYPE))) { + return NOT_VALUE_EXPIRE; + } + + // 如果被设置为:永不过期 + Long effTime = payloads.get(EFF, Long.class); + if(effTime == NEVER_EXPIRE) { + return NEVER_EXPIRE; + } + // 如果已经超时 + if(effTime == null || effTime < System.currentTimeMillis()) { + return NOT_VALUE_EXPIRE; + } + + // 计算timeout (转化为以秒为单位的有效时间) + return (effTime - System.currentTimeMillis()) / 1000; + } + + + + // -------------- 其它方法 + + /** + * 创建 jwt (Map 参数方式) + * + * @param map 扩展数据 + * @param keyt 秘钥 + * @return jwt-token + */ + public String createToken(Map map, String keyt) { + // 创建 + JWT jwt = JWT.create().addPayloads(map); + + // 返回 + return generateToken(jwt, keyt); + } + + +} diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java new file mode 100644 index 000000000..a3ae8a708 --- /dev/null +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java @@ -0,0 +1,211 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jwt; + +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; + +import java.util.Map; + +/** + * jwt 操作工具类封装 + * + * @author click33 + * @since 1.27.1 + */ +public class SaJwtUtil { + + /** + * 底层 saJwtTemplate 对象 + */ + public static SaJwtTemplate saJwtTemplate = new SaJwtTemplate(); + + /** + * 获取底层 saJwtTemplate 对象 + * @return / + */ + public static SaJwtTemplate getSaJwtTemplate() { + return saJwtTemplate; + } + + /** + * 设置底层 saJwtTemplate 对象 + * @param saJwtTemplate / + */ + public static void setSaJwtTemplate(SaJwtTemplate saJwtTemplate) { + SaJwtUtil.saJwtTemplate = saJwtTemplate; + } + + // 常量 + + + /** + * key:账号类型 + */ + public static final String LOGIN_TYPE = SaJwtTemplate.LOGIN_TYPE; + + /** + * key:账号id + */ + public static final String LOGIN_ID = SaJwtTemplate.LOGIN_ID; + + /** + * key:登录设备类型 + */ + public static final String DEVICE_TYPE = SaJwtTemplate.DEVICE_TYPE; + + /** + * key:有效截止期 (时间戳) + */ + public static final String EFF = SaJwtTemplate.EFF; + + /** + * key:乱数 ( 混入随机字符串,防止每次生成的 token 都是一样的 ) + */ + public static final String RN_STR = SaJwtTemplate.RN_STR; + + /** + * 当有效期被设为此值时,代表永不过期 + */ + public static final long NEVER_EXPIRE = SaJwtTemplate.NEVER_EXPIRE; + + /** + * 表示一个值不存在 + */ + public static final long NOT_VALUE_EXPIRE = SaJwtTemplate.NOT_VALUE_EXPIRE; + + // ------ 创建 + + /** + * 创建 jwt (简单方式) + * @param loginType 登录类型 + * @param loginId 账号id + * @param extraData 扩展数据 + * @param keyt 秘钥 + * @return jwt-token + */ + public static String createToken(String loginType, Object loginId, Map extraData, String keyt) { + return saJwtTemplate.createToken(loginType, loginId, extraData, keyt); + } + + /** + * 创建 jwt (全参数方式) + * @param loginType 账号类型 + * @param loginId 账号id + * @param deviceType 设备类型 + * @param timeout token有效期 (单位 秒) + * @param extraData 扩展数据 + * @param keyt 秘钥 + * @return jwt-token + */ + public static String createToken(String loginType, Object loginId, String deviceType, + long timeout, Map extraData, String keyt) { + return saJwtTemplate.createToken(loginType, loginId, deviceType, timeout, extraData, keyt); + } + + /** + * 为 JWT 对象和 keyt 秘钥,生成 token 字符串 + * @param jwt JWT构建对象 + * @param keyt 秘钥 + * @return 根据 JWT 对象和 keyt 秘钥,生成的 token 字符串 + */ + public static String generateToken (JWT jwt, String keyt) { + return saJwtTemplate.generateToken(jwt, keyt); + } + + // ------ 解析 + + /** + * jwt 解析 + * @param token Jwt-Token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @param isCheckTimeout 是否校验 timeout 字段 + * @return 解析后的jwt 对象 + */ + public static JWT parseToken(String token, String loginType, String keyt, boolean isCheckTimeout) { + return saJwtTemplate.parseToken(token, loginType, keyt, isCheckTimeout); + } + + /** + * 获取 jwt 数据载荷 (校验 sign、loginType、timeout) + * @param token token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 载荷 + */ + public static JSONObject getPayloads(String token, String loginType, String keyt) { + return saJwtTemplate.getPayloads(token, loginType, keyt); + } + + /** + * 获取 jwt 数据载荷 (校验 sign、loginType,不校验 timeout) + * @param token token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 载荷 + */ + public static JSONObject getPayloadsNotCheck(String token, String loginType, String keyt) { + return saJwtTemplate.getPayloadsNotCheck(token, loginType, keyt); + } + + /** + * 获取 jwt 代表的账号id + * @param token Token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 值 + */ + public static Object getLoginId(String token, String loginType, String keyt) { + return saJwtTemplate.getLoginId(token, loginType, keyt); + } + + /** + * 获取 jwt 代表的账号id (未登录时返回null) + * @param token Token值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 值 + */ + public static Object getLoginIdOrNull(String token, String loginType, String keyt) { + return saJwtTemplate.getLoginIdOrNull(token, loginType, keyt); + } + + /** + * 获取 jwt 剩余有效期 + * @param token JwtToken值 + * @param loginType 登录类型 + * @param keyt 秘钥 + * @return 值 + */ + public static long getTimeout(String token, String loginType, String keyt) { + return saJwtTemplate.getTimeout(token, loginType, keyt); + } + + + // -------------- 其它方法 + + /** + * 创建 jwt (Map 参数方式) + * + * @param map 扩展数据 + * @param keyt 秘钥 + * @return jwt-token + */ + public static String createToken(Map map, String keyt) { + return saJwtTemplate.createToken(map, keyt); + } +} diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForMixin.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForMixin.java new file mode 100644 index 000000000..741b33d47 --- /dev/null +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForMixin.java @@ -0,0 +1,277 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jwt; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.exception.ApiDisabledException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.jwt.error.SaJwtErrorCode; +import cn.dev33.satoken.jwt.exception.SaJwtException; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaTokenConsts; + +import java.util.List; +import java.util.Map; + +/** + * Sa-Token 整合 jwt -- Mixin 混入模式 + * + * @author click33 + * @since 1.30.0 + */ +public class StpLogicJwtForMixin extends StpLogic { + + /** + * Sa-Token 整合 jwt -- Mixin 混入 + */ + public StpLogicJwtForMixin() { + super(StpUtil.TYPE); + } + + /** + * Sa-Token 整合 jwt -- Mixin 混入 + * @param loginType 账号体系标识 + */ + public StpLogicJwtForMixin(String loginType) { + super(loginType); + } + + /** + * 获取jwt秘钥 + * @return / + */ + public String jwtSecretKey() { + String keyt = getConfigOrGlobal().getJwtSecretKey(); + SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); + return keyt; + } + + // + // ------ 重写方法 + // + + // ------------------- 获取token 相关 ------------------- + + /** + * 创建一个TokenValue + */ + @Override + public String createTokenValue(Object loginId, String deviceType, long timeout, Map extraData) { + return SaJwtUtil.createToken(loginType, loginId, deviceType, timeout, extraData, jwtSecretKey()); + } + + /** + * 获取当前会话的Token信息 + * @return token信息 + */ + @Override + public SaTokenInfo getTokenInfo() { + SaTokenInfo info = new SaTokenInfo(); + info.tokenName = getTokenName(); + info.tokenValue = getTokenValue(); + info.isLogin = isLogin(); + info.loginId = getLoginIdDefaultNull(); + info.loginType = getLoginType(); + info.tokenTimeout = getTokenTimeout(); + info.sessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE; + info.tokenSessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE; + info.tokenActiveTimeout = SaTokenDao.NOT_VALUE_EXPIRE; + info.loginDeviceType = getLoginDeviceType(); + return info; + } + + // ------------------- 登录相关操作 ------------------- + + /** + * 获取指定Token对应的账号id (不做任何特殊处理) + */ + @Override + public String getLoginIdNotHandle(String tokenValue) { + try { + Object loginId = SaJwtUtil.getLoginId(tokenValue, loginType, jwtSecretKey()); + return String.valueOf(loginId); + } catch (SaJwtException e) { + // CODE == 30204 时,代表token已过期,此时返回-3,以便外层更精确的显示异常信息 + if(e.getCode() == SaJwtErrorCode.CODE_30204) { + return NotLoginException.TOKEN_TIMEOUT; + } + return null; + } + } + + /** + * 会话注销 + */ + @Override + public void logout() { + // ... + + // 从当前 [storage存储器] 里删除 + SaHolder.getStorage().delete(splicingKeyJustCreatedSave()); + + // 如果打开了Cookie模式,则把cookie清除掉 + if(getConfigOrGlobal().getIsReadCookie()){ + SaHolder.getResponse().deleteCookie(getTokenName()); + } + } + + /** + * [work] 注销下线 + * + * @param tokenValue 指定 token + * @param logoutParameter 注销参数 + */ + public void _logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { + throw new ApiDisabledException(); + } + + /** + * [禁用] 会话注销 + */ + @Override + public void _logout(Object loginId, SaLogoutParameter logoutParameter) { + throw new ApiDisabledException(); + } + + /** + * [禁用] 顶人下线,根据账号id 和 设备类型 + */ + @Override + public void replaced(Object loginId, String deviceType) { + throw new ApiDisabledException(); + } + + /** + * 获取当前 Token 的扩展信息 + */ + @Override + public Object getExtra(String key) { + return getExtra(getTokenValue(), key); + } + + /** + * 获取指定 Token 的扩展信息 + */ + @Override + public Object getExtra(String tokenValue, String key) { + return SaJwtUtil.getPayloads(tokenValue, loginType, jwtSecretKey()).get(key); + } + + /** + * 删除 Token-Id 映射 + */ + @Override + public void deleteTokenToIdMapping(String tokenValue) { + // not action + } + /** + * 更改 Token 指向的 账号Id 值 + */ + @Override + public void updateTokenToIdMapping(String tokenValue, Object loginId) { + // not action + } + /** + * 存储 Token-Id 映射 + */ + @Override + public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) { + // not action + } + + // ------------------- 过期时间相关 ------------------- + + /** + * 获取指定 token 剩余有效时间 (单位: 秒) + */ + @Override + public long getTokenTimeout(String tokenValue) { + return SaJwtUtil.getTimeout(tokenValue, loginType, jwtSecretKey()); + } + + + // ------------------- Token-Session 相关 ------------------- + + /** + * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,isCreate代表是否新建并返回 + * + * @param tokenValue token值 + * @param isCreate 是否新建 + * @return session对象 + */ + public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) { + if(SaFoxUtil.isEmpty(tokenValue)) { + throw new SaTokenException("Token-Session 获取失败:token 不能为空"); + } + long timeout = getTokenTimeout(tokenValue); + return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, timeout, session -> { + // 这里是该 Token-Session 首次创建时才会被执行的方法: + // 设定这个 SaSession 的各种基础信息:类型、账号体系、Token 值 + session.setType(SaTokenConsts.SESSION_TYPE__TOKEN); + session.setLoginType(getLoginType()); + session.setToken(tokenValue); + }); + } + + + // ------------------- 会话管理 ------------------- + + /** + * [禁用] 根据条件查询Token + */ + @Override + public List searchTokenValue(String keyword, int start, int size, boolean sortType) { + throw new ApiDisabledException(); + } + + + // ------------------- Bean对象代理 ------------------- + + /** + * 返回当前 StpLogic 是否支持 isShare + * @return / + */ + @Override + public boolean isSupportShareToken() { + return false; + } + + /** + * 返回全局配置对象的 maxTryTimes 属性 + * @return / + */ + @Override + public int getConfigOfMaxTryTimes(SaLoginParameter loginParameter) { + return -1; + } + + /** + * 重写返回:支持 extra 扩展参数 + */ + @Override + public boolean isSupportExtra() { + return true; + } + +} diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForSimple.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForSimple.java new file mode 100644 index 000000000..404216d75 --- /dev/null +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForSimple.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jwt; + +import java.util.Map; + +import cn.dev33.satoken.jwt.error.SaJwtErrorCode; +import cn.dev33.satoken.jwt.exception.SaJwtException; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; + +/** + * Sa-Token 整合 jwt -- Simple 简单模式 + * + * @author click33 + * @since 1.30.0 + */ +public class StpLogicJwtForSimple extends StpLogic { + + /** + * Sa-Token 整合 jwt -- Simple模式 + */ + public StpLogicJwtForSimple() { + super(StpUtil.TYPE); + } + + /** + * Sa-Token 整合 jwt -- Simple模式 + * @param loginType 账号体系标识 + */ + public StpLogicJwtForSimple(String loginType) { + super(loginType); + } + + /** + * 获取jwt秘钥 + * @return / + */ + public String jwtSecretKey() { + String keyt = getConfigOrGlobal().getJwtSecretKey(); + SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); + return keyt; + } + + // ------ 重写方法 + + /** + * 创建一个TokenValue + */ + @Override + public String createTokenValue(Object loginId, String deviceType, long timeout, Map extraData) { + return SaJwtUtil.createToken(loginType, loginId, extraData, jwtSecretKey()); + } + + /** + * 获取当前 Token 的扩展信息 + */ + @Override + public Object getExtra(String key) { + return getExtra(getTokenValue(), key); + } + + /** + * 获取指定 Token 的扩展信息 + */ + @Override + public Object getExtra(String tokenValue, String key) { + return SaJwtUtil.getPayloadsNotCheck(tokenValue, loginType, jwtSecretKey()).get(key); + } + + + @Override + public boolean isSupportShareToken() { + // 为确保 jwt-simple 模式的 token Extra 数据生成不受旧token影响,这里必须让 is-share 恒为 false + // 即:在使用 jwt-simple 模式后,即使配置了 is-share=true 也不能复用旧 Token,必须每次创建新 Token + return false; + } + + /** + * 重写返回:支持 extra 扩展参数 + */ + @Override + public boolean isSupportExtra() { + return true; + } + +} diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForStateless.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForStateless.java new file mode 100644 index 000000000..8de6de354 --- /dev/null +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForStateless.java @@ -0,0 +1,229 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jwt; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.exception.ApiDisabledException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.jwt.error.SaJwtErrorCode; +import cn.dev33.satoken.jwt.exception.SaJwtException; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.Map; + +/** + * Sa-Token 整合 jwt -- Stateless 无状态模式 + * + * @author click33 + * @since 1.30.0 + */ +public class StpLogicJwtForStateless extends StpLogic { + + /** + * Sa-Token 整合 jwt -- Stateless 无状态 + */ + public StpLogicJwtForStateless() { + super(StpUtil.TYPE); + } + + /** + * Sa-Token 整合 jwt -- Stateless 无状态 + * @param loginType 账号体系标识 + */ + public StpLogicJwtForStateless(String loginType) { + super(loginType); + } + + /** + * 获取jwt秘钥 + * @return / + */ + public String jwtSecretKey() { + String keyt = getConfigOrGlobal().getJwtSecretKey(); + SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); + return keyt; + } + + // + // ------ 重写方法 + // + + // ------------------- 获取token 相关 ------------------- + + /** + * 创建一个TokenValue + */ + @Override + public String createTokenValue(Object loginId, String deviceType, long timeout, Map extraData) { + return SaJwtUtil.createToken(loginType, loginId, deviceType, timeout, extraData, jwtSecretKey()); + } + + /** + * 获取当前会话的Token信息 + * @return token信息 + */ + @Override + public SaTokenInfo getTokenInfo() { + SaTokenInfo info = new SaTokenInfo(); + info.tokenName = getTokenName(); + info.tokenValue = getTokenValue(); + info.isLogin = isLogin(); + info.loginId = getLoginIdDefaultNull(); + info.loginType = getLoginType(); + info.tokenTimeout = getTokenTimeout(); + info.sessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE; + info.tokenSessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE; + info.tokenActiveTimeout = SaTokenDao.NOT_VALUE_EXPIRE; + info.loginDeviceType = getLoginDeviceType(); + return info; + } + + // ------------------- 登录相关操作 ------------------- + + /** + * 创建指定账号id的登录会话 + * @param id 登录id,建议的类型:(long | int | String) + * @param loginParameter 此次登录的参数Model + * @return 返回会话令牌 + */ + @Override + public String createLoginSession(Object id, SaLoginParameter loginParameter) { + + // 1、先检查一下,传入的参数是否有效 + checkLoginArgs(id, loginParameter); + + // 3、生成一个token + String tokenValue = createTokenValue(id, loginParameter.getDeviceType(), loginParameter.getTimeout(), loginParameter.getExtraData()); + + // 4、$$ 发布事件:账号xxx 登录成功 + SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginParameter); + + // 5、返回 + return tokenValue; + } + + /** + * 获取指定Token对应的账号id (不做任何特殊处理) + */ + @Override + public String getLoginIdNotHandle(String tokenValue) { + try { + Object loginId = SaJwtUtil.getLoginId(tokenValue, loginType, jwtSecretKey()); + return String.valueOf(loginId); + } catch (SaJwtException e) { + // CODE == 30204 时,代表token已过期,此时返回-3,以便外层更精确的显示异常信息 + if(e.getCode() == SaJwtErrorCode.CODE_30204) { + return NotLoginException.TOKEN_TIMEOUT; + } + return null; + } + } + + /** + * 会话注销 + */ + @Override + public void logout() { + // 如果连token都没有,那么无需执行任何操作 + String tokenValue = getTokenValue(); + if(SaFoxUtil.isEmpty(tokenValue)) { + return; + } + + // 从当前 [storage存储器] 里删除 + SaHolder.getStorage().delete(splicingKeyJustCreatedSave()); + + // 如果打开了Cookie模式,则把cookie清除掉 + if(getConfigOrGlobal().getIsReadCookie()){ + SaHolder.getResponse().deleteCookie(getTokenName()); + } + } + + /** + * 获取当前 Token 的扩展信息 + */ + @Override + public Object getExtra(String key) { + return getExtra(getTokenValue(), key); + } + + /** + * 获取指定 Token 的扩展信息 + */ + @Override + public Object getExtra(String tokenValue, String key) { + return SaJwtUtil.getPayloads(tokenValue, loginType, jwtSecretKey()).get(key); + } + + + // ------------------- 过期时间相关 ------------------- + + /** + * 获取指定 token 剩余有效时间 (单位: 秒) + */ + @Override + public long getTokenTimeout(String tokenValue) { + return SaJwtUtil.getTimeout(getTokenValue(), loginType, jwtSecretKey()); + } + + + // ------------------- id 反查 token 相关操作 ------------------- + + /** + * 返回当前会话的登录设备类型 + * @return 当前令牌的登录设备类型 + */ + @Override + public String getLoginDeviceType() { + // 如果没有token,直接返回 null + String tokenValue = getTokenValue(); + if(tokenValue == null) { + return null; + } + // 如果还未登录,直接返回 null + if(!isLogin()) { + return null; + } + // 获取 + return SaJwtUtil.getPayloadsNotCheck(tokenValue, loginType, jwtSecretKey()).getStr(SaJwtUtil.DEVICE_TYPE); + } + + + // ------------------- Bean对象代理 ------------------- + + /** + * [禁用] 返回持久化对象 + */ + @Override + public SaTokenDao getSaTokenDao() { + throw new ApiDisabledException(); + } + + /** + * 重写返回:支持 extra 扩展参数 + */ + @Override + public boolean isSupportExtra() { + return true; + } + +} diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/error/SaJwtErrorCode.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/error/SaJwtErrorCode.java new file mode 100644 index 000000000..f8b1288d0 --- /dev/null +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/error/SaJwtErrorCode.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jwt.error; + +/** + * 定义 sa-token-jwt 所有异常细分状态码 + * + * @author click33 + * @since 1.33.0 + */ +public interface SaJwtErrorCode { + + /** 对 jwt 字符串解析失败 */ + int CODE_30201 = 30201; + + /** 此 jwt 的签名无效 */ + int CODE_30202 = 30202; + + /** 此 jwt 的 loginType 字段不符合预期 */ + int CODE_30203 = 30203; + + /** 此 jwt 已超时 */ + int CODE_30204 = 30204; + + /** 没有配置jwt秘钥 */ + int CODE_30205 = 30205; + + /** 登录时提供的账号id为空 */ + int CODE_30206 = 30206; + +} diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/exception/SaJwtException.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/exception/SaJwtException.java new file mode 100644 index 000000000..e4ff16d62 --- /dev/null +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/exception/SaJwtException.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jwt.exception; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.util.SaFoxUtil; + + +/** + * 一个异常:代表 jwt 模块相关错误 + * + * @author click33 + * @since 1.33.0 + */ +public class SaJwtException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129555290130114L; + + /** + * jwt 解析错误 + * @param message 异常描述 + */ + public SaJwtException(String message) { + super(message); + } + + /** + * jwt 解析错误 + * @param message 异常描述 + * @param cause 异常对象 + */ + public SaJwtException(String message, Throwable cause) { + super(message, cause); + } + + /** + * 写入异常细分状态码 + * @param code 异常细分状态码 + * @return 对象自身 + */ + public SaJwtException setCode(int code) { + super.setCode(code); + return this; + } + + /** + * 如果flag==true,则抛出message异常 + * @param flag 标记 + * @param message 异常信息 + */ + public static void throwBy(boolean flag, String message) { + if(flag) { + throw new SaJwtException(message); + } + } + + /** + * 如果value==null或者isEmpty,则抛出message异常 + * @param value 值 + * @param message 异常信息 + * @param code 异常细分状态码 + */ + public static void throwByNull(Object value, String message, int code) { + if(SaFoxUtil.isEmpty(value)) { + throw new SaJwtException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/.gitignore b/sa-token-plugin/sa-token-oauth2/.gitignore deleted file mode 100644 index 8122f47c0..000000000 --- a/sa-token-plugin/sa-token-oauth2/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ -.iml \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/README.md b/sa-token-plugin/sa-token-oauth2/README.md deleted file mode 100644 index 670f21194..000000000 --- a/sa-token-plugin/sa-token-oauth2/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# sa-token-oauth2 内测版 - -sa-token-oauth2 模块是 sa-token 实现 oauth2.0 的部分,目前该模块功能完成度较低,为避免不可预知的风险,建议仅做学习测试使用 - -## 启动步骤 - -1. 启动项目 `sa-token-demo-oauth2-server`, 此为OAuth2.0的服务提供方 -2. 启动项目 `sa-token-demo-oauth2-client`, 此为OAuth2.0的客户端 -3. 根据控制台打印,访问测试地址即可:[http://localhost:8002/login.html](http://localhost:8002/login.html) - -可结合代码注释学习查看 - diff --git a/sa-token-plugin/sa-token-oauth2/pom.xml b/sa-token-plugin/sa-token-oauth2/pom.xml index 4c01a9f97..95844b8a6 100644 --- a/sa-token-plugin/sa-token-oauth2/pom.xml +++ b/sa-token-plugin/sa-token-oauth2/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-plugin - 1.24.0 + ${revision} + ../pom.xml jar @@ -20,7 +21,17 @@ cn.dev33 sa-token-core - ${sa-token-version} + + + + cn.dev33 + sa-token-jwt + true + + + + cn.dev33 + sa-token-sign diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java index 2ad2854bb..7fb416300 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java @@ -1,32 +1,185 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2; -import cn.dev33.satoken.oauth2.config.SaOAuth2Config; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; +import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; +import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverterDefaultImpl; +import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; +import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerateDefaultImpl; +import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; +import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoaderDefaultImpl; +import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver; +import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolverDefaultImpl; +import cn.dev33.satoken.oauth2.template.SaOAuth2Template; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; /** * Sa-Token-OAuth2 模块 总控类 * - * @author kong - * + * @author click33 + * @since 1.19.0 */ public class SaOAuth2Manager { /** * OAuth2 配置 Bean */ - private static SaOAuth2Config config; - public static SaOAuth2Config getConfig() { - if (config == null) { + private static volatile SaOAuth2ServerConfig serverConfig; + public static SaOAuth2ServerConfig getServerConfig() { + if (serverConfig == null) { // 初始化默认值 synchronized (SaOAuth2Manager.class) { - if (config == null) { - setConfig(new SaOAuth2Config()); + if (serverConfig == null) { + setServerConfig(new SaOAuth2ServerConfig()); + } + } + } + return serverConfig; + } + public static void setServerConfig(SaOAuth2ServerConfig serverConfig) { + SaOAuth2Manager.serverConfig = serverConfig; + } + + /** + * OAuth2 数据加载器 Bean + */ + private static volatile SaOAuth2DataLoader dataLoader; + public static SaOAuth2DataLoader getDataLoader() { + if (dataLoader == null) { + synchronized (SaOAuth2Manager.class) { + if (dataLoader == null) { + setDataLoader(new SaOAuth2DataLoaderDefaultImpl()); + } + } + } + return dataLoader; + } + public static void setDataLoader(SaOAuth2DataLoader dataLoader) { + SaOAuth2Manager.dataLoader = dataLoader; + } + + /** + * OAuth2 数据解析器 Bean + */ + private static volatile SaOAuth2DataResolver dataResolver; + public static SaOAuth2DataResolver getDataResolver() { + if (dataResolver == null) { + synchronized (SaOAuth2Manager.class) { + if (dataResolver == null) { + setDataResolver(new SaOAuth2DataResolverDefaultImpl()); + } + } + } + return dataResolver; + } + public static void setDataResolver(SaOAuth2DataResolver dataResolver) { + SaOAuth2Manager.dataResolver = dataResolver; + } + + /** + * OAuth2 数据格式转换器 Bean + */ + private static volatile SaOAuth2DataConverter dataConverter; + public static SaOAuth2DataConverter getDataConverter() { + if (dataConverter == null) { + synchronized (SaOAuth2Manager.class) { + if (dataConverter == null) { + setDataConverter(new SaOAuth2DataConverterDefaultImpl()); + } + } + } + return dataConverter; + } + public static void setDataConverter(SaOAuth2DataConverter dataConverter) { + SaOAuth2Manager.dataConverter = dataConverter; + } + + /** + * OAuth2 数据构建器 Bean + */ + private static volatile SaOAuth2DataGenerate dataGenerate; + public static SaOAuth2DataGenerate getDataGenerate() { + if (dataGenerate == null) { + synchronized (SaOAuth2Manager.class) { + if (dataGenerate == null) { + setDataGenerate(new SaOAuth2DataGenerateDefaultImpl()); + } + } + } + return dataGenerate; + } + public static void setDataGenerate(SaOAuth2DataGenerate dataGenerate) { + SaOAuth2Manager.dataGenerate = dataGenerate; + } + + /** + * OAuth2 数据持久 Bean + */ + private static volatile SaOAuth2Dao dao; + public static SaOAuth2Dao getDao() { + if (dao == null) { + synchronized (SaOAuth2Manager.class) { + if (dao == null) { + setDao(new SaOAuth2Dao()); + } + } + } + return dao; + } + public static void setDao(SaOAuth2Dao dao) { + SaOAuth2Manager.dao = dao; + } + + /** + * OAuth2 模板方法 Bean + */ + private static volatile SaOAuth2Template template; + public static SaOAuth2Template getTemplate() { + if (template == null) { + synchronized (SaOAuth2Manager.class) { + if (template == null) { + setTemplate(new SaOAuth2Template()); + } + } + } + return template; + } + public static void setTemplate(SaOAuth2Template template) { + SaOAuth2Manager.template = template; + } + + /** + * OAuth2 StpLogic + */ + private static volatile StpLogic stpLogic; + public static StpLogic getStpLogic() { + if (stpLogic == null) { + synchronized (SaOAuth2Manager.class) { + if (stpLogic == null) { + setStpLogic(StpUtil.stpLogic); } } } - return config; + return stpLogic; } - public static void setConfig(SaOAuth2Config config) { - SaOAuth2Manager.config = config; + public static void setStpLogic(StpLogic stpLogic) { + SaOAuth2Manager.stpLogic = stpLogic; } } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckAccessToken.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckAccessToken.java new file mode 100644 index 000000000..cbd31e621 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckAccessToken.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Access-Token 校验:指定请求中必须包含有效的 access_token ,并且包含指定的 scope + * + *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.39.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD,ElementType.TYPE}) +public @interface SaCheckAccessToken { + + /** + * 需要校验的 scope [ 数组 ] + * + * @return / + */ + String [] scope() default {}; + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientIdSecret.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientIdSecret.java new file mode 100644 index 000000000..2d719d5d5 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientIdSecret.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * ClientSecret 校验:指定请求中必须包含有效的 client_id 和 client_secret 信息 + * + *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.39.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD,ElementType.TYPE}) +public @interface SaCheckClientIdSecret { + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientToken.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientToken.java new file mode 100644 index 000000000..fb09c9f3a --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientToken.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Client-Token 校验:指定请求中必须包含有效的 client_token ,并且包含指定的 scope + * + *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.39.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD,ElementType.TYPE}) +public @interface SaCheckClientToken { + + /** + * 需要校验的 scope [ 数组 ] + * + * @return / + */ + String [] scope() default {}; + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckAccessTokenHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckAccessTokenHandler.java new file mode 100644 index 000000000..2da82f1d6 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckAccessTokenHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.annotation.SaCheckAccessToken; + +import java.lang.reflect.AnnotatedElement; + +/** + * 注解 SaCheckAccessToken 的处理器 + * + * @author click33 + * @since 1.39.0 + */ +public class SaCheckAccessTokenHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckAccessToken.class; + } + + @Override + public void checkMethod(SaCheckAccessToken at, AnnotatedElement element) { + _checkMethod(at.scope()); + } + + public static void _checkMethod(String[] scope) { + String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); + SaOAuth2Manager.getTemplate().checkAccessTokenScope(accessToken, scope); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientIdSecretHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientIdSecretHandler.java new file mode 100644 index 000000000..dc659d4a9 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientIdSecretHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.oauth2.annotation.SaCheckClientIdSecret; +import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; + +import java.lang.reflect.AnnotatedElement; + +/** + * 注解 SaCheckClientSecret 的处理器 + * + * @author click33 + * @since 1.39.0 + */ +public class SaCheckClientIdSecretHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckClientIdSecret.class; + } + + @Override + public void checkMethod(SaCheckClientIdSecret at, AnnotatedElement element) { + _checkMethod(); + } + + public static void _checkMethod() { + SaOAuth2ServerProcessor.instance.checkCurrClientSecret(); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientTokenHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientTokenHandler.java new file mode 100644 index 000000000..31101ace7 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientTokenHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.annotation.handler; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.annotation.SaCheckClientToken; + +import java.lang.reflect.AnnotatedElement; + +/** + * 注解 SaCheckAccessToken 的处理器 + * + * @author click33 + * @since 1.39.0 + */ +public class SaCheckClientTokenHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckClientToken.class; + } + + @Override + public void checkMethod(SaCheckClientToken at, AnnotatedElement element) { + _checkMethod(at.scope()); + } + + public static void _checkMethod(String[] scope) { + String clientToken = SaOAuth2Manager.getDataResolver().readClientToken(SaHolder.getRequest()); + SaOAuth2Manager.getTemplate().checkClientTokenScope(clientToken, scope); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java deleted file mode 100644 index 06ecd72a9..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2Config.java +++ /dev/null @@ -1,234 +0,0 @@ -package cn.dev33.satoken.oauth2.config; - -import java.io.Serializable; -import java.util.function.BiFunction; -import java.util.function.Supplier; - -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-Token-OAuth2 配置类 Model - * @author kong - * - */ -public class SaOAuth2Config implements Serializable { - - private static final long serialVersionUID = -6541180061782004705L; - - /** 是否打开模式:授权码(Authorization Code) */ - public Boolean isCode = true; - - /** 是否打开模式:隐藏式(Implicit) */ - public Boolean isImplicit = false; - - /** 是否打开模式:密码式(Password) */ - public Boolean isPassword = false; - - /** 是否打开模式:凭证式(Client Credentials) */ - public Boolean isClient = false; - - /** 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token */ - public Boolean isNewRefresh = false; - - /** Code授权码 保存的时间(单位秒) 默认五分钟 */ - public long codeTimeout = 60 * 5; - - /** Access-Token 保存的时间(单位秒) 默认两个小时 */ - public long accessTokenTimeout = 60 * 60 * 2; - - /** Refresh-Token 保存的时间(单位秒) 默认30 天 */ - public long refreshTokenTimeout = 60 * 60 * 24 * 30; - - /** Client-Token 保存的时间(单位秒) 默认两个小时 */ - public long clientTokenTimeout = 60 * 60 * 2; - - - /** - * @return isCode - */ - public Boolean getIsCode() { - return isCode; - } - - /** - * @param isCode 要设置的 isCode - */ - public void setIsCode(Boolean isCode) { - this.isCode = isCode; - } - - /** - * @return isImplicit - */ - public Boolean getIsImplicit() { - return isImplicit; - } - - /** - * @param isImplicit 要设置的 isImplicit - */ - public void setIsImplicit(Boolean isImplicit) { - this.isImplicit = isImplicit; - } - - /** - * @return isPassword - */ - public Boolean getIsPassword() { - return isPassword; - } - - /** - * @param isPassword 要设置的 isPassword - */ - public void setIsPassword(Boolean isPassword) { - this.isPassword = isPassword; - } - - /** - * @return isClient - */ - public Boolean getIsClient() { - return isClient; - } - - /** - * @param isClient 要设置的 isClient - */ - public void setIsClient(Boolean isClient) { - this.isClient = isClient; - } - - /** - * @return isNewRefresh - */ - public Boolean getIsNewRefresh() { - return isNewRefresh; - } - - /** - * @param isNewRefresh 要设置的 isNewRefresh - */ - public void setIsNewRefresh(Boolean isNewRefresh) { - this.isNewRefresh = isNewRefresh; - } - - /** - * @return codeTimeout - */ - public long getCodeTimeout() { - return codeTimeout; - } - - /** - * @param codeTimeout 要设置的 codeTimeout - * @return 对象自身 - */ - public SaOAuth2Config setCodeTimeout(long codeTimeout) { - this.codeTimeout = codeTimeout; - return this; - } - - /** - * @return accessTokenTimeout - */ - public long getAccessTokenTimeout() { - return accessTokenTimeout; - } - - /** - * @param accessTokenTimeout 要设置的 accessTokenTimeout - * @return 对象自身 - */ - public SaOAuth2Config setAccessTokenTimeout(long accessTokenTimeout) { - this.accessTokenTimeout = accessTokenTimeout; - return this; - } - - /** - * @return refreshTokenTimeout - */ - public long getRefreshTokenTimeout() { - return refreshTokenTimeout; - } - - /** - * @param refreshTokenTimeout 要设置的 refreshTokenTimeout - * @return 对象自身 - */ - public SaOAuth2Config setRefreshTokenTimeout(long refreshTokenTimeout) { - this.refreshTokenTimeout = refreshTokenTimeout; - return this; - } - - /** - * @return clientTokenTimeout - */ - public long getClientTokenTimeout() { - return clientTokenTimeout; - } - - /** - * @param clientTokenTimeout 要设置的 clientTokenTimeout - * @return 对象自身 - */ - public SaOAuth2Config setClientTokenTimeout(long clientTokenTimeout) { - this.clientTokenTimeout = clientTokenTimeout; - return this; - } - - - // -------------------- SaOAuth2Handle 所有回调函数 -------------------- - - /** - * OAuth-Server端:未登录时返回的View - */ - public Supplier notLoginView = () -> "当前会话在OAuth-Server认证中心尚未登录"; - - /** - * OAuth-Server端:确认授权时返回的View - */ - public BiFunction confirmView = (clientId, scope) -> "本次操作需要用户授权"; - - /** - * OAuth-Server端:登录函数 - */ - public BiFunction doLoginHandle = (name, pwd) -> SaResult.error(); - - /** - * @param notLoginView OAuth-Server端:未登录时返回的View - * @return 对象自身 - */ - public SaOAuth2Config setNotLoginView(Supplier notLoginView) { - this.notLoginView = notLoginView; - return this; - } - - /** - * @param confirmView OAuth-Server端:确认授权时返回的View - * @return 对象自身 - */ - public SaOAuth2Config setConfirmView(BiFunction confirmView) { - this.confirmView = confirmView; - return this; - } - - /** - * @param doLoginHandle OAuth-Server端:登录函数 - * @return 对象自身 - */ - public SaOAuth2Config setDoLoginHandle(BiFunction doLoginHandle) { - this.doLoginHandle = doLoginHandle; - return this; - } - - - @Override - public String toString() { - return "SaOAuth2Config [isCode=" + isCode + ", isImplicit=" + isImplicit + ", isPassword=" + isPassword - + ", isClient=" + isClient + ", isNewRefresh=" + isNewRefresh + ", codeTimeout=" + codeTimeout - + ", accessTokenTimeout=" + accessTokenTimeout + ", refreshTokenTimeout=" + refreshTokenTimeout - + ", clientTokenTimeout=" + clientTokenTimeout + "]"; - } - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java new file mode 100644 index 000000000..02b8c64fd --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.config; + +import java.io.Serializable; + +/** + * Sa-Token OAuth2 Server 端 Oidc 配置类 Model + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2OidcConfig implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** iss 值,如不配置则自动计算 */ + public String iss; + + /** idToken 有效期(单位秒) 默认十分钟 */ + public long idTokenTimeout = 60 * 10; + + + /** + * 获取 iss 值,如不配置则自动计算 + * + * @return / + */ + public String getIss() { + return this.iss; + } + + /** + * 设置 iss 值,如不配置则自动计算 + * + * @param iss / + * @return 对象自身 + */ + public SaOAuth2OidcConfig setIss(String iss) { + this.iss = iss; + return this; + } + + /** + * 获取 idToken 有效期(单位秒) 默认十分钟 + * + * @return / + */ + public long getIdTokenTimeout() { + return this.idTokenTimeout; + } + + /** + * 设置 idToken 有效期(单位秒) 默认十分钟 + * + * @param idTokenTimeout / + * @return 对象自身 + */ + public SaOAuth2OidcConfig setIdTokenTimeout(long idTokenTimeout) { + this.idTokenTimeout = idTokenTimeout; + return this; + } + + @Override + public String toString() { + return "SaOAuth2OidcConfig{" + + "iss='" + iss + '\'' + + ", idTokenTimeout=" + idTokenTimeout + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java new file mode 100644 index 000000000..e22d4532a --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java @@ -0,0 +1,501 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.config; + +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sa-Token OAuth2 Server 端 配置类 Model + * + * @author click33 + * @since 1.19.0 + */ +public class SaOAuth2ServerConfig implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** 是否打开模式:授权码(Authorization Code) */ + public Boolean enableAuthorizationCode = true; + + /** 是否打开模式:隐藏式(Implicit) */ + public Boolean enableImplicit = true; + + /** 是否打开模式:密码式(Password) */ + public Boolean enablePassword = true; + + /** 是否打开模式:凭证式(Client Credentials) */ + public Boolean enableClientCredentials = true; + + /** Code授权码 保存的时间(单位:秒) 默认五分钟 */ + public long codeTimeout = 60 * 5; + + /** 全局默认配置所有应用:Access-Token 保存的时间(单位:秒) 默认两个小时 */ + public long accessTokenTimeout = 60 * 60 * 2; + + /** 全局默认配置所有应用:Refresh-Token 保存的时间(单位:秒) 默认30 天 */ + public long refreshTokenTimeout = 60 * 60 * 24 * 30; + + /** 全局默认配置所有应用:Client-Token 保存的时间(单位:秒) 默认两个小时 */ + public long clientTokenTimeout = 60 * 60 * 2; + + /** 全局默认配置所有应用:单个应用单个用户最多同时存在的 Access-Token 数量 */ + public int maxAccessTokenCount = 12; + + /** 全局默认配置所有应用:单个应用单个用户最多同时存在的 Refresh-Token 数量 */ + public int maxRefreshTokenCount = 12; + + /** 全局默认配置所有应用:单个应用最多同时存在的 Client-Token 数量 */ + public int maxClientTokenCount = 12; + + /** 全局默认配置所有应用:是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token */ + public Boolean isNewRefresh = false; + + /** 默认 openid 生成算法中使用的摘要前缀 */ + public String openidDigestPrefix = SaOAuth2Consts.OPENID_DEFAULT_DIGEST_PREFIX; + + /** 默认 unionid 生成算法中使用的摘要前缀 */ + public String unionidDigestPrefix = SaOAuth2Consts.UNIONID_DEFAULT_DIGEST_PREFIX; + + /** 指定高级权限,多个用逗号隔开 */ + public String higherScope; + + /** 指定低级权限,多个用逗号隔开 */ + public String lowerScope; + + /** 模式4是否返回 AccessToken 字段,以使其更符合 OAuth2 RFC 规范 */ + public Boolean mode4ReturnAccessToken = false; + + /** 是否在返回值中隐藏默认的状态字段 (code、msg、data) */ + public Boolean hideStatusField = false; + + /** + * oidc 相关配置 + */ + SaOAuth2OidcConfig oidc = new SaOAuth2OidcConfig(); + + /** client 列表 */ + public Map clients = new LinkedHashMap<>(); + + // 额外方法 + + /** + * 注册 client + * @return / + */ + public SaOAuth2ServerConfig addClient(SaClientModel client) { + if(this.clients == null) { + this.clients = new LinkedHashMap<>(); + } + this.clients.put(client.getClientId(), client); + return this; + } + + + // get set + + /** + * 是否打开模式:授权码(Authorization Code) + * @return enableAuthorizationCode + */ + public Boolean getEnableAuthorizationCode() { + return enableAuthorizationCode; + } + + /** + * 设置是否打开模式:授权码(Authorization Code) + * @param enableAuthorizationCode 是否开启 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setEnableAuthorizationCode(Boolean enableAuthorizationCode) { + this.enableAuthorizationCode = enableAuthorizationCode; + return this; + } + + /** + * 是否打开模式:隐藏式(Implicit) + * @return enableImplicit + */ + public Boolean getEnableImplicit() { + return enableImplicit; + } + + /** + * 设置是否打开模式:隐藏式(Implicit) + * @param enableImplicit 是否开启 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setEnableImplicit(Boolean enableImplicit) { + this.enableImplicit = enableImplicit; + return this; + } + + /** + * 是否打开模式:密码式(Password) + * @return enablePassword + */ + public Boolean getEnablePassword() { + return enablePassword; + } + + /** + * 设置是否打开模式:密码式(Password) + * @param enablePassword 是否开启 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setEnablePassword(Boolean enablePassword) { + this.enablePassword = enablePassword; + return this; + } + + /** + * 是否打开模式:凭证式(Client Credentials) + * @return enableClientCredentials + */ + public Boolean getEnableClientCredentials() { + return enableClientCredentials; + } + + /** + * 设置是否打开模式:凭证式(Client Credentials) + * @param enableClientCredentials 是否开启 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setEnableClientCredentials(Boolean enableClientCredentials) { + this.enableClientCredentials = enableClientCredentials; + return this; + } + + /** + * 全局默认配置所有应用:是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token + * @return isNewRefresh + */ + public Boolean getIsNewRefresh() { + return isNewRefresh; + } + + /** + * 全局默认配置所有应用:设置是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token + * @param isNewRefresh 是否开启 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setIsNewRefresh(Boolean isNewRefresh) { + this.isNewRefresh = isNewRefresh; + return this; + } + + /** + * Code授权码 保存的时间(单位:秒) 默认五分钟 + * @return codeTimeout + */ + public long getCodeTimeout() { + return codeTimeout; + } + + /** + * 设置Code授权码保存的时间(单位:秒) + * @param codeTimeout 保存时间(秒) + * @return 对象自身 + */ + public SaOAuth2ServerConfig setCodeTimeout(long codeTimeout) { + this.codeTimeout = codeTimeout; + return this; + } + + /** + * 全局默认配置所有应用:Access-Token 保存的时间(单位:秒) 默认两个小时 + * @return accessTokenTimeout + */ + public long getAccessTokenTimeout() { + return accessTokenTimeout; + } + + /** + * 全局默认配置所有应用:设置Access-Token保存的时间(单位:秒) + * @param accessTokenTimeout 保存时间(秒) + * @return 对象自身 + */ + public SaOAuth2ServerConfig setAccessTokenTimeout(long accessTokenTimeout) { + this.accessTokenTimeout = accessTokenTimeout; + return this; + } + + /** + * 全局默认配置所有应用:Refresh-Token 保存的时间(单位:秒) 默认30天 + * @return refreshTokenTimeout + */ + public long getRefreshTokenTimeout() { + return refreshTokenTimeout; + } + + /** + * 全局默认配置所有应用:设置Refresh-Token保存的时间(单位:秒) + * @param refreshTokenTimeout 保存时间(秒) + * @return 对象自身 + */ + public SaOAuth2ServerConfig setRefreshTokenTimeout(long refreshTokenTimeout) { + this.refreshTokenTimeout = refreshTokenTimeout; + return this; + } + + /** + * 全局默认配置所有应用:Client-Token 保存的时间(单位:秒) 默认两个小时 + * @return clientTokenTimeout + */ + public long getClientTokenTimeout() { + return clientTokenTimeout; + } + + /** + * 全局默认配置所有应用:设置Client-Token保存的时间(单位:秒) + * @param clientTokenTimeout 保存时间(秒) + * @return 对象自身 + */ + public SaOAuth2ServerConfig setClientTokenTimeout(long clientTokenTimeout) { + this.clientTokenTimeout = clientTokenTimeout; + return this; + } + + /** + * 全局默认配置所有应用:单个应用单个用户最多同时存在的 Access-Token 数量 + * @return maxAccessTokenCount + */ + public int getMaxAccessTokenCount() { + return maxAccessTokenCount; + } + + /** + * 设置单个应用单个用户最多同时存在的 Access-Token 数量 + * @param maxAccessTokenCount 最大数量 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setMaxAccessTokenCount(int maxAccessTokenCount) { + this.maxAccessTokenCount = maxAccessTokenCount; + return this; + } + + /** + * 全局默认配置所有应用:单个应用单个用户最多同时存在的 Refresh-Token 数量 + * @return maxRefreshTokenCount + */ + public int getMaxRefreshTokenCount() { + return maxRefreshTokenCount; + } + + /** + * 设置单个应用单个用户最多同时存在的 Refresh-Token 数量 + * @param maxRefreshTokenCount 最大数量 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setMaxRefreshTokenCount(int maxRefreshTokenCount) { + this.maxRefreshTokenCount = maxRefreshTokenCount; + return this; + } + + /** + * 全局默认配置所有应用:单个应用最多同时存在的 Client-Token 数量 + * @return maxClientTokenCount + */ + public int getMaxClientTokenCount() { + return maxClientTokenCount; + } + + /** + * 设置单个应用最多同时存在的 Client-Token 数量 + * @param maxClientTokenCount 最大数量 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setMaxClientTokenCount(int maxClientTokenCount) { + this.maxClientTokenCount = maxClientTokenCount; + return this; + } + + /** + * 默认 openid 生成算法中使用的摘要前缀 + * @return openidDigestPrefix + */ + public String getOpenidDigestPrefix() { + return openidDigestPrefix; + } + + /** + * 设置默认 openid 生成算法中使用的摘要前缀 + * @param openidDigestPrefix 摘要前缀 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setOpenidDigestPrefix(String openidDigestPrefix) { + this.openidDigestPrefix = openidDigestPrefix; + return this; + } + + /** + * 默认 unionid 生成算法中使用的摘要前缀 + * @return unionidDigestPrefix + */ + public String getUnionidDigestPrefix() { + return unionidDigestPrefix; + } + + /** + * 设置默认 unionid 生成算法中使用的摘要前缀 + * @param unionidDigestPrefix 摘要前缀 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setUnionidDigestPrefix(String unionidDigestPrefix) { + this.unionidDigestPrefix = unionidDigestPrefix; + return this; + } + + /** + * 指定高级权限,多个用逗号隔开 + * @return higherScope + */ + public String getHigherScope() { + return higherScope; + } + + /** + * 设置高级权限,多个用逗号隔开 + * @param higherScope 权限字符串 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setHigherScope(String higherScope) { + this.higherScope = higherScope; + return this; + } + + /** + * 指定低级权限,多个用逗号隔开 + * @return lowerScope + */ + public String getLowerScope() { + return lowerScope; + } + + /** + * 设置低级权限,多个用逗号隔开 + * @param lowerScope 权限字符串 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setLowerScope(String lowerScope) { + this.lowerScope = lowerScope; + return this; + } + + /** + * 模式4是否返回 AccessToken 字段,以使其更符合 OAuth2 RFC 规范 + * @return mode4ReturnAccessToken + */ + public Boolean getMode4ReturnAccessToken() { + return mode4ReturnAccessToken; + } + + /** + * 设置模式4是否返回 AccessToken 字段,以使其更符合 OAuth2 RFC 规范 + * @param mode4ReturnAccessToken 是否返回 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setMode4ReturnAccessToken(Boolean mode4ReturnAccessToken) { + this.mode4ReturnAccessToken = mode4ReturnAccessToken; + return this; + } + + /** + * 是否在返回值中隐藏默认的状态字段 (code、msg、data) + * @return hideStatusField + */ + public Boolean getHideStatusField() { + return hideStatusField; + } + + /** + * 设置是否在返回值中隐藏默认的状态字段 (code、msg、data) + * @param hideStatusField 是否隐藏 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setHideStatusField(Boolean hideStatusField) { + this.hideStatusField = hideStatusField; + return this; + } + + /** + * 获取oidc相关配置 + * @return oidc配置对象 + */ + public SaOAuth2OidcConfig getOidc() { + return oidc; + } + + /** + * 设置oidc相关配置 + * @param oidc oidc配置对象 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setOidc(SaOAuth2OidcConfig oidc) { + this.oidc = oidc; + return this; + } + + /** + * 获取client列表 + * @return client列表 + */ + public Map getClients() { + return clients; + } + + /** + * 设置client列表 + * @param clients client列表 + * @return 对象自身 + */ + public SaOAuth2ServerConfig setClients(Map clients) { + this.clients = clients; + return this; + } + + @Override + public String toString() { + return "SaOAuth2ServerConfig {" + + "enableAuthorizationCode=" + enableAuthorizationCode + + ", enableImplicit=" + enableImplicit + + ", enablePassword=" + enablePassword + + ", enableClientCredentials=" + enableClientCredentials + + ", isNewRefresh=" + isNewRefresh + + ", codeTimeout=" + codeTimeout + + ", accessTokenTimeout=" + accessTokenTimeout + + ", refreshTokenTimeout=" + refreshTokenTimeout + + ", clientTokenTimeout=" + clientTokenTimeout + + ", maxAccessTokenCount=" + maxAccessTokenCount + + ", maxRefreshTokenCount=" + maxRefreshTokenCount + + ", maxClientTokenCount=" + maxClientTokenCount + + ", openidDigestPrefix=" + openidDigestPrefix + + ", unionidDigestPrefix=" + unionidDigestPrefix + + ", higherScope=" + higherScope + + ", lowerScope=" + lowerScope + + ", mode4ReturnAccessToken=" + mode4ReturnAccessToken + + ", hideStatusField=" + hideStatusField + + ", oidc=" + oidc + + ", clients=" + clients + + '}'; + } + + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/GrantType.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/GrantType.java new file mode 100644 index 000000000..d1238d436 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/GrantType.java @@ -0,0 +1,12 @@ +package cn.dev33.satoken.oauth2.consts; + +/** + * 所有授权类型 + */ +public final class GrantType { + public static String authorization_code = "authorization_code"; + public static String refresh_token = "refresh_token"; + public static String password = "password"; + public static String client_credentials = "client_credentials"; + public static String implicit = "implicit"; +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java new file mode 100644 index 000000000..1d65c2864 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.consts; + +/** + * Sa-Token OAuth2 所有常量 + * + * @author click33 + * @since 1.23.0 + */ +public class SaOAuth2Consts { + + /** + * 所有API接口 + * @author click33 + */ + public static final class Api { + public static String authorize = "/oauth2/authorize"; + public static String token = "/oauth2/token"; + public static String refresh = "/oauth2/refresh"; + public static String revoke = "/oauth2/revoke"; + public static String client_token = "/oauth2/client_token"; + public static String doLogin = "/oauth2/doLogin"; + public static String doConfirm = "/oauth2/doConfirm"; + } + + /** + * 所有参数名称 + * @author click33 + */ + public static final class Param { + public static String response_type = "response_type"; + public static String client_id = "client_id"; + public static String client_secret = "client_secret"; + public static String redirect_uri = "redirect_uri"; + public static String scope = "scope"; + public static String state = "state"; + public static String code = "code"; + public static String token = "token"; + public static String access_token = "access_token"; + public static String refresh_token = "refresh_token"; + public static String client_token = "client_token"; + public static String grant_type = "grant_type"; + public static String username = "username"; + public static String password = "password"; + public static String name = "name"; + public static String pwd = "pwd"; + public static String build_redirect_uri = "build_redirect_uri"; + public static String Authorization = "Authorization"; + public static String nonce = "nonce"; + } + + /** + * 所有返回类型 + */ + public static final class ResponseType { + public static String code = "code"; + public static String token = "token"; + } + + /** + * 所有 token 类型 + */ + public static final class TokenType { + // 全小写 + public static String basic = "basic"; + public static String digest = "digest"; + public static String bearer = "bearer"; + + // 首字母大写 + public static String Basic = "Basic"; + public static String Digest = "Digest"; + public static String Bearer = "Bearer"; + } + + /** + * 扩展字段 + */ + public static final class ExtraField { + public static String unionid = "unionid"; + public static String openid = "openid"; + public static String userid = "userid"; + public static String id_token = "id_token"; + } + + + /** 默认 openid 生成算法中使用的前缀 */ + public static final String OPENID_DEFAULT_DIGEST_PREFIX = "openid_default_digest_prefix"; + + /** 默认 unionid 生成算法中使用的前缀 */ + public static final String UNIONID_DEFAULT_DIGEST_PREFIX = "unionid_default_digest_prefix"; + + /** 表示OK的返回结果 */ + public static final String OK = "ok"; + + /** 表示请求没有得到任何有效处理 {msg: "not handle"} */ + public static final String NOT_HANDLE = "{\"msg\": \"not handle\"}"; + + /** + * 最终权限处理器标识符:在所有权限处理器执行之后,执行此 scope 标识符代表的权限处理器 + */ + public static final String _FINALLY_WORK_SCOPE = "_FINALLY_WORK_SCOPE"; + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java new file mode 100644 index 000000000..33c2f8c38 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java @@ -0,0 +1,847 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.dao; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.raw.SaRawSessionDelegator; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaTtlMethods; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static cn.dev33.satoken.oauth2.template.SaOAuth2Util.checkClientModel; + +/** + * Sa-Token OAuth2 数据持久层 (在 SaTokenDao 之上再封装一层,方便 OAuth2 模块整体的数据读写操作) + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2Dao implements SaTtlMethods { + + // ------------------- 索引操作公共代码 + + /** + * Raw Session 读写委托 (存储 Access-Token、Refresh-Token、Client-Token 索引) + */ + public SaRawSessionDelegator oauth2RSD = new SaRawSessionDelegator("oauth2"); + + /** + * 在 raw-session 中的保存 Access-Token 索引列表使用的 key + */ + public static final String ACCESS_TOKEN_MAP = "__HD_ACCESS_TOKEN_MAP"; + + /** + * 在 raw-session 中的保存 Refresh-Token 索引列表使用的 key + */ + public static final String REFRESH_TOKEN_MAP = "__HD_REFRESH_TOKEN_MAP"; + + /** + * 在 raw-session 中的保存 Client-Token 索引列表使用的 key + */ + public static final String CLIENT_TOKEN_MAP = "__HD_CLIENT_TOKEN_MAP"; + + /** + * 获取:保存 Access-Token 索引时使用的 RawSession + * + * @param clientId 应用 id + * @param loginId 账号 id + * @param isCreate 如果尚未创建,是否立即创建 + * @return / + */ + protected SaSession getRawSessionByAccessToken(String clientId, Object loginId, boolean isCreate) { + String value = splicingAccessTokenRSDValue(clientId, loginId); + return oauth2RSD.getSessionById(value, isCreate); + } + + /** + * 获取:保存 Refresh-Token 索引时使用的 RawSession + * + * @param clientId 应用 id + * @param loginId 账号 id + * @param isCreate 如果尚未创建,是否立即创建 + * @return / + */ + protected SaSession getRawSessionByRefreshToken(String clientId, Object loginId, boolean isCreate) { + String value = splicingRefreshTokenRSDValue(clientId, loginId); + return oauth2RSD.getSessionById(value, isCreate); + } + + /** + * 获取:保存 Client-Token 索引时使用的 RawSession + * + * @param clientId 应用 id + * @param isCreate 如果尚未创建,是否立即创建 + * @return / + */ + protected SaSession getRawSessionByClientToken(String clientId, boolean isCreate) { + String value = splicingClientTokenRSDValue(clientId); + return oauth2RSD.getSessionById(value, isCreate); + } + + /** + * 在 RawSession 上添加 token 索引,并完整调整索引列表 + * + * @param session 待操作的 RawSession + * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key + * @param token 待添加的 token + * @param timeout 添加的 token 其过期时间 + * @param maxTokenCount 允许的最多 token 数量,超出的将被删除 (-1=不限制) + * @param removeFun 执行删除 token 的函数 + */ + protected void addTokenIndex_AndAdjust(SaSession session, String tokenIndexMapSaveKey, String token, long timeout, int maxTokenCount, SaParamFunction removeFun) { + Map tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap); + if(! tokenIndexMap.containsKey(token)) { + // 添加 + tokenIndexMap.put(token, ttlToExpireTime(timeout)); + // 剔除过期的 + tokenIndexMap = _removeExpiredIndex(tokenIndexMap); + // 删掉溢出的 + tokenIndexMap = _removeOverflowIndex(tokenIndexMap, maxTokenCount, removeFun); + // 保存 + session.set(tokenIndexMapSaveKey, tokenIndexMap); + // 更新 TTL + long maxTtl = getMaxTtlByExpireTime(tokenIndexMap.values()); + if(maxTtl != 0) { + session.updateTimeout(maxTtl); + } + } + } + + /** + * 在 RawSession 上删除 token 索引,并尝试注销 RawSession + * + * @param session 待操作的 RawSession + * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key + * @param token 待删除的 token + */ + protected void deleteTokenIndex_AndTryLogout(SaSession session, String tokenIndexMapSaveKey, String token) { + Map tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap); + tokenIndexMap.remove(token); + // 如果删除后还有记录,就再次保存 + if( ! tokenIndexMap.isEmpty()) { + session.set(tokenIndexMapSaveKey, tokenIndexMap); + } else { + // 没有的话就直接注销此 RawSession + session.logout(); + } + } + + /** + * 剔除已过期的 token 索引 + * + * @param tokenIndexMap token 索引列表 + * @return 调整后的索引列表 + */ + protected Map _removeExpiredIndex(Map tokenIndexMap) { + Map newTokenList = newTokenIndexMap(); + for (Map.Entry entry : tokenIndexMap.entrySet()) { + long ttl = expireTimeToTtl(entry.getValue()); + if(ttl != SaTokenDao.NOT_VALUE_EXPIRE) { + newTokenList.put(entry.getKey(), entry.getValue()); + } + } + return newTokenList; + } + + /** + * 将 token 索引列表中溢出的部分删除(按照插入顺序先进先出,不考虑每个剩余 token 剩余有效期) + * + * @param tokenIndexMap token 索引列表(key=token, value=token过期时间)(传入的 Map 必须是有序的) + * @param maxTokenCount 允许的最多 token 数量,超出的将被删除 (-1=不限制) + * @param removeFun 执行删除 token 的函数 + * @return 调整后的索引列表 + */ + protected Map _removeOverflowIndex(Map tokenIndexMap, int maxTokenCount, SaParamFunction removeFun) { + + // 如果当前数量未超过限制,直接返回 + if (tokenIndexMap.size() <= maxTokenCount || maxTokenCount == SaTokenDao.NEVER_EXPIRE) { + return tokenIndexMap; + } + + // 创建新的索引 Map 副本 + Map newTokenIndexMap = newTokenIndexMap(); + + // 溢出数量 + int overflowCount = tokenIndexMap.size() - maxTokenCount; + + // 已删除 Token 数量 + int removedCount = 0; + + // 遍历原 Map 的所有条目 + for (Map.Entry entry : tokenIndexMap.entrySet()) { + String token = entry.getKey(); + if (removedCount < overflowCount) { + // 溢出部分:执行删除回调,但不添加到新 Map + removeFun.run(token); + removedCount++; + } else { + // 未溢出部分:添加到新 Map 副本 + newTokenIndexMap.put(token, entry.getValue()); + } + } + + // 返回索引 Map 副本 + return newTokenIndexMap; + } + + /** + * 从 RawSession 获取 Token 索引列表(获取之前会完整调整索引列表,保证获取的都是有效 token) + * + * @param session 待操作的 RawSession + * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key + * @return / + */ + protected Map getTokenIndexMap_FromAdjustAfter(SaSession session, String tokenIndexMapSaveKey) { + if(session == null) { + return newTokenIndexMap(); + } + + // 根据 ttl 值过滤一遍 + Map tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap); + Map newTokenIndexMap = _removeExpiredIndex(tokenIndexMap); + + // 如果调整后集合长度归零了,说明 token 已全部过期,直接注销此 RawSession + if(newTokenIndexMap.isEmpty()) { + session.logout(); + return newTokenIndexMap(); + } + + // 没有归零,但是长度变小了,说明有过期的 token,需要重写写入一遍 + if(tokenIndexMap.size() > newTokenIndexMap.size()) { + session.set(tokenIndexMapSaveKey, newTokenIndexMap); + } + + // 转 List 返回 + return newTokenIndexMap; + } + + /** + * 从 RawSession 获取 Token 列表(获取之前会完整调整索引列表,保证获取的都是有效 token) + * + * @param session 待操作的 RawSession + * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key + * @return / + */ + protected List getTokenValueList_FromAdjustAfter(SaSession session, String tokenIndexMapSaveKey) { + return new ArrayList<>(getTokenIndexMap_FromAdjustAfter(session, tokenIndexMapSaveKey).keySet()); + } + + + // ------------------- code 操作 + + /** + * 保存:CodeModel + * @param c / + */ + public void saveCode(CodeModel c) { + if(c == null) { + return; + } + getSaTokenDao().setObject(splicingCodeSaveKey(c.code), c, SaOAuth2Manager.getServerConfig().getCodeTimeout()); + } + + /** + * 删除:CodeModel + * @param code / + */ + public void deleteCode(String code) { + if(code != null) { + getSaTokenDao().deleteObject(splicingCodeSaveKey(code)); + } + } + + /** + * 获取:CodeModel + * @param code / + * @return / + */ + public CodeModel getCode(String code) { + if(code == null) { + return null; + } + return (CodeModel)getSaTokenDao().getObject(splicingCodeSaveKey(code)); + } + + + // ------------------- code 索引 + + /** + * 保存:Code 索引 + * @param c / + */ + public void saveCodeIndex(CodeModel c) { + if(c == null) { + return; + } + getSaTokenDao().set(splicingCodeIndexKey(c.clientId, c.loginId), c.code, SaOAuth2Manager.getServerConfig().getCodeTimeout()); + } + + /** + * 删除:Code 索引 + * @param clientId 应用id + * @param loginId 账号id + */ + public void deleteCodeIndex(String clientId, Object loginId) { + getSaTokenDao().delete(splicingCodeIndexKey(clientId, loginId)); + } + + /** + * 获取:Code Value + * @param clientId 应用id + * @param loginId 账号id + * @return / + */ + public String getCodeValue(String clientId, Object loginId) { + return getSaTokenDao().get(splicingCodeIndexKey(clientId, loginId)); + } + + + // ------------------- Access-Token Model + + /** + * 保存:AccessTokenModel + * @param at / + */ + public void saveAccessToken(AccessTokenModel at) { + if(at == null) { + return; + } + getSaTokenDao().setObject(splicingAccessTokenSaveKey(at.accessToken), at, at.getExpiresIn()); + } + + /** + * 删除:AccessTokenModel + * @param accessToken 值 + */ + public void deleteAccessToken(String accessToken) { + if(accessToken != null) { + getSaTokenDao().deleteObject(splicingAccessTokenSaveKey(accessToken)); + } + } + + /** + * 获取:AccessTokenModel + * @param accessToken / + * @return / + */ + public AccessTokenModel getAccessToken(String accessToken) { + if(accessToken == null) { + return null; + } + return (AccessTokenModel)getSaTokenDao().getObject(splicingAccessTokenSaveKey(accessToken)); + } + + + // ------------------- Access-Token 索引 + + /** + * 保存:Access-Token 索引,并完整调整索引列表 + * + * @param at / + * @param maxAccessTokenCount 允许的最多 Access-Token 数量,超出的将被删除 (-1=不限制) + */ + public void saveAccessTokenIndex_AndAdjust(AccessTokenModel at, int maxAccessTokenCount) { + if(at == null) { + return; + } + SaSession session = getRawSessionByAccessToken(at.clientId, at.loginId, true); + addTokenIndex_AndAdjust(session, ACCESS_TOKEN_MAP, at.accessToken, at.getExpiresIn(), maxAccessTokenCount, this::deleteAccessToken); + } + + /** + * 删除:Access-Token 在 RawSession 上的单个索引数据 + * + * @param clientId 应用 id + * @param loginId 账号id + * @param accessToken 值 + */ + public void deleteAccessTokenIndex_BySingleData(String clientId, Object loginId, String accessToken) { + SaSession session = getRawSessionByAccessToken(clientId, loginId, false); + if(session == null) { + return; + } + deleteTokenIndex_AndTryLogout(session, ACCESS_TOKEN_MAP, accessToken); + } + + /** + * 删除:Access-Token 索引整体 + * @param clientId 应用id + * @param loginId 账号id + */ + public void deleteAccessTokenIndex(String clientId, Object loginId) { + oauth2RSD.deleteSessionById(splicingAccessTokenRSDValue(clientId, loginId)); + } + + /** + * 获取 Access-Token 索引列表(获取之前会完整调整索引列表,保证获取的都是有效 AccessToken 索引) + * + * @param clientId 应用id + * @param loginId 账号id + * @return / + */ + public Map getAccessTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) { + SaSession session = getRawSessionByAccessToken(clientId, loginId, false); + return getTokenIndexMap_FromAdjustAfter(session, ACCESS_TOKEN_MAP); + } + + /** + * 获取 Access-Token 列表(获取之前会完整调整索引列表,保证获取的都是有效 AccessToken) + * + * @param clientId 应用id + * @param loginId 账号id + * @return / + */ + public List getAccessTokenValueList_FromAdjustAfter(String clientId, Object loginId) { + SaSession session = getRawSessionByAccessToken(clientId, loginId, false); + return getTokenValueList_FromAdjustAfter(session, ACCESS_TOKEN_MAP); + } + + + // ------------------- Refresh-Token Model + + /** + * 保存:RefreshTokenModel + * @param rt . + */ + public void saveRefreshToken(RefreshTokenModel rt) { + if(rt == null) { + return; + } + getSaTokenDao().setObject(splicingRefreshTokenSaveKey(rt.refreshToken), rt, rt.getExpiresIn()); + } + + /** + * 删除:RefreshTokenModel + * @param refreshToken 值 + */ + public void deleteRefreshToken(String refreshToken) { + if(refreshToken != null) { + getSaTokenDao().deleteObject(splicingRefreshTokenSaveKey(refreshToken)); + } + } + + /** + * 获取:RefreshTokenModel + * @param refreshToken / + * @return / + */ + public RefreshTokenModel getRefreshToken(String refreshToken) { + if(refreshToken == null) { + return null; + } + return (RefreshTokenModel)getSaTokenDao().getObject(splicingRefreshTokenSaveKey(refreshToken)); + } + + + // ------------------- Refresh-Token 索引 + + /** + * 保存:Refresh-Token 索引 + * + * @param rt / + * @param maxRefreshTokenCount 允许的最多 Refresh-Token 数量,超出的将被删除 (-1=不限制) + */ + public void saveRefreshTokenIndex_AndAdjust(RefreshTokenModel rt, int maxRefreshTokenCount) { + if(rt == null) { + return; + } + SaSession session = getRawSessionByRefreshToken(rt.clientId, rt.loginId, true); + addTokenIndex_AndAdjust(session, REFRESH_TOKEN_MAP, rt.refreshToken, rt.getExpiresIn(), maxRefreshTokenCount, this::deleteRefreshToken); + } + + /** + * 删除:Refresh-Token 在 RawSession 上的单个索引数据 + * + * @param clientId 应用 id + * @param loginId 账号id + * @param refreshToken 值 + */ + public void deleteRefreshTokenIndex_BySingleData(String clientId, Object loginId, String refreshToken) { + SaSession session = getRawSessionByRefreshToken(clientId, loginId, false); + if(session == null) { + return; + } + deleteTokenIndex_AndTryLogout(session, REFRESH_TOKEN_MAP, refreshToken); + } + + /** + * 删除:Refresh-Token 索引整体 + * @param clientId 应用id + * @param loginId 账号id + */ + public void deleteRefreshTokenIndex(String clientId, Object loginId) { + oauth2RSD.deleteSessionById(splicingRefreshTokenRSDValue(clientId, loginId)); + } + + /** + * 获取 Refresh-Token 索引列表(获取之前会完整调整索引列表,保证获取的都是有效 RefreshToken 索引) + * + * @param clientId 应用id + * @param loginId 账号id + * @return / + */ + public Map getRefreshTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) { + SaSession session = getRawSessionByRefreshToken(clientId, loginId, false); + return getTokenIndexMap_FromAdjustAfter(session, REFRESH_TOKEN_MAP); + } + + /** + * 获取 Refresh-Token 列表(获取之前会完整调整索引列表,保证获取的都是有效 RefreshToken) + * + * @param clientId 应用id + * @param loginId 账号id + * @return / + */ + public List getRefreshTokenValueList_FromAdjustAfter(String clientId, Object loginId) { + SaSession session = getRawSessionByRefreshToken(clientId, loginId, false); + return getTokenValueList_FromAdjustAfter(session, REFRESH_TOKEN_MAP); + } + + + // ------------------- Client-Token Model + + /** + * 保存:ClientTokenModel + * @param ct . + */ + public void saveClientToken(ClientTokenModel ct) { + if(ct == null) { + return; + } + getSaTokenDao().setObject(splicingClientTokenSaveKey(ct.clientToken), ct, ct.getExpiresIn()); + } + + /** + * 删除:ClientTokenModel + * @param clientToken 值 + */ + public void deleteClientToken(String clientToken) { + if(clientToken != null) { + getSaTokenDao().deleteObject(splicingClientTokenSaveKey(clientToken)); + } + } + + /** + * 获取:ClientTokenModel + * @param clientToken / + * @return / + */ + public ClientTokenModel getClientToken(String clientToken) { + if(clientToken == null) { + return null; + } + return getSaTokenDao().getObject(splicingClientTokenSaveKey(clientToken), ClientTokenModel.class); + } + + + // ------------------- Client-Token 索引 + + /** + * 保存:Client-Token 索引 + * + * @param ct / + * @param maxClientTokenCount 允许的最多 Client-Token 数量,超出的将被删除 (-1=不限制) + */ + public void saveClientTokenIndex_AndAdjust(ClientTokenModel ct, int maxClientTokenCount) { + if(ct == null) { + return; + } + SaSession session = getRawSessionByClientToken(ct.clientId, true); + addTokenIndex_AndAdjust(session, CLIENT_TOKEN_MAP, ct.clientToken, ct.getExpiresIn(), maxClientTokenCount, this::deleteClientToken); + } + + /** + * 删除:Client-Token 在 RawSession 上的单个索引数据 + * @param clientId 应用 id + * @param clientToken 值 + */ + public void deleteClientTokenIndex_BySingleData(String clientId, String clientToken) { + SaSession session = getRawSessionByClientToken(clientId, false); + if(session == null) { + return; + } + deleteTokenIndex_AndTryLogout(session, CLIENT_TOKEN_MAP, clientToken); + } + + /** + * 删除:Client-Token 索引整体 + * + * @param clientId 应用id + */ + public void deleteClientTokenIndex(String clientId) { + oauth2RSD.deleteSessionById(splicingClientTokenRSDValue(clientId)); + } + + /** + * 获取 Client-Token 索引列表(获取之前会完整调整索引列表,保证获取的都是有效 ClientToken 索引) + * + * @param clientId 应用id + * @param loginId 账号id + * @return / + */ + public Map getClientTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) { + SaSession session = getRawSessionByClientToken(clientId, false); + return getTokenIndexMap_FromAdjustAfter(session, CLIENT_TOKEN_MAP); + } + + /** + * 获取 Client-Token 列表(获取之前会完整调整索引列表,保证获取的都是有效 ClientToken) + * + * @param clientId 应用id + * @return / + */ + public List getClientTokenValueList_FromAdjustAfter(String clientId) { + SaSession session = getRawSessionByClientToken(clientId, false); + return getTokenValueList_FromAdjustAfter(session, CLIENT_TOKEN_MAP); + } + + + // ------------------- GrantScope + + /** + * 保存:用户授权记录 + * @param clientId 应用id + * @param loginId 账号id + * @param scopes 权限列表 + */ + public void saveGrantScope(String clientId, Object loginId, List scopes) { + if( ! SaFoxUtil.isEmpty(scopes)) { + long ttl = checkClientModel(clientId).getAccessTokenTimeout(); + String value = SaOAuth2Manager.getDataConverter().convertScopeListToString(scopes); + getSaTokenDao().set(splicingGrantScopeKey(clientId, loginId), value, ttl); + } + } + + /** + * 删除:用户授权记录 + * @param clientId 应用id + * @param loginId 账号id + */ + public void deleteGrantScope(String clientId, Object loginId) { + getSaTokenDao().delete(splicingGrantScopeKey(clientId, loginId)); + } + + /** + * 获取:用户授权记录 + * @param clientId 应用id + * @param loginId 账号id + * @return 权限 + */ + public List getGrantScope(String clientId, Object loginId) { + String value = getSaTokenDao().get(splicingGrantScopeKey(clientId, loginId)); + return SaOAuth2Manager.getDataConverter().convertScopeStringToList(value); + } + + + // ------------------- State + + /** + * 保存:state + * @param state / + */ + public void saveState(String state) { + if( ! SaFoxUtil.isEmpty(state)) { + long ttl = SaOAuth2Manager.getServerConfig().getCodeTimeout(); + getSaTokenDao().set(splicingStateSaveKey(state), state, ttl); + } + } + + /** + * 删除:state记录 + * @param state / + */ + public void deleteState(String state) { + getSaTokenDao().delete(splicingStateSaveKey(state)); + } + + /** + * 获取:state + * @param state / + * @return / + */ + public String getState(String state) { + if(SaFoxUtil.isEmpty(state)) { + return null; + } + return getSaTokenDao().get(splicingStateSaveKey(state)); + } + + + // ------------------- 其它 + + /** + * 保存:nonce-索引 + * @param c / + */ + public void saveCodeNonceIndex(CodeModel c) { + if(c == null || SaFoxUtil.isEmpty(c.nonce)) { + return; + } + getSaTokenDao().set(splicingCodeNonceIndexSaveKey(c.code), c.nonce, SaOAuth2Manager.getServerConfig().getCodeTimeout()); + } + + /** + * 获取:nonce + * @param code / + * @return / + */ + public String getNonce(String code) { + if(SaFoxUtil.isEmpty(code)) { + return null; + } + return getSaTokenDao().get(splicingCodeNonceIndexSaveKey(code)); + } + + + // ------------------- 拼接key + + /** + * 拼接 key:Code 保存 + * @param code 授权码 + * @return key + */ + public String splicingCodeSaveKey(String code) { + return getSaTokenConfig().getTokenName() + ":oauth2:code:" + code; + } + + /** + * 拼接 key:Code 索引 + * @param clientId 应用id + * @param loginId 账号id + * @return key + */ + public String splicingCodeIndexKey(String clientId, Object loginId) { + return getSaTokenConfig().getTokenName() + ":oauth2:code-index:" + clientId + ":" + loginId; + } + + /** + * 拼接 key:Access-Token 保存 + * @param accessToken accessToken + * @return key + */ + public String splicingAccessTokenSaveKey(String accessToken) { + return getSaTokenConfig().getTokenName() + ":oauth2:access-token:" + accessToken; + } + + /** + * 拼接 key:Access-Token RSD Value + * @param clientId 应用id + * @param loginId 账号id + * @return key + */ + public String splicingAccessTokenRSDValue(String clientId, Object loginId) { + return "access-token:" + clientId + ":" + loginId; + } + + /** + * 拼接 key:Refresh-Token 保存 + * @param refreshToken refreshToken + * @return key + */ + public String splicingRefreshTokenSaveKey(String refreshToken) { + return getSaTokenConfig().getTokenName() + ":oauth2:refresh-token:" + refreshToken; + } + + /** + * 拼接 key:Refresh-Token RSD Value + * @param clientId 应用id + * @param loginId 账号id + * @return key + */ + public String splicingRefreshTokenRSDValue(String clientId, Object loginId) { + return "refresh-token:" + clientId + ":" + loginId; + } + + /** + * 拼接 key:Client-Token 保存 + * @param clientToken clientToken + * @return key + */ + public String splicingClientTokenSaveKey(String clientToken) { + return getSaTokenConfig().getTokenName() + ":oauth2:client-token:" + clientToken; + } + + /** + * 拼接 key:Client-Token RSD Value + * @param clientId 应用id + * @return key + */ + public String splicingClientTokenRSDValue(String clientId) { + return "client-token:" + clientId; + } + + /** + * 拼接 key:用户授权记录 + * @param clientId 应用id + * @param loginId 账号id + * @return key + */ + public String splicingGrantScopeKey(String clientId, Object loginId) { + return getSaTokenConfig().getTokenName() + ":oauth2:grant-scope:" + clientId + ":" + loginId; + } + + /** + * 拼接 key:state 参数保存 + * @param state / + * @return key + */ + public String splicingStateSaveKey(String state) { + return getSaTokenConfig().getTokenName() + ":oauth2:state:" + state; + } + + /** + * 拼接 key:code-nonce 索引 参数保存 + * @param code 授权码 + * @return key + */ + public String splicingCodeNonceIndexSaveKey(String code) { + return getSaTokenConfig().getTokenName() + ":oauth2:code-nonce-index:" + code; + } + + + // -------- bean 对象代理 + + /** + * 获取使用的 getSaTokenDao 实例 + * + * @return / + */ + public SaTokenDao getSaTokenDao() { + return SaManager.getSaTokenDao(); + } + + /** + * 获取使用的 SaTokenConfig 实例 + * + * @return / + */ + public SaTokenConfig getSaTokenConfig() { + return SaManager.getConfig(); + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverter.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverter.java new file mode 100644 index 000000000..ca43d8c0d --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverter.java @@ -0,0 +1,111 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.convert; + +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; + +import java.util.List; + +/** + * Sa-Token OAuth2 数据格式转换器 + * + * @author click33 + * @since 1.39.0 + */ +public interface SaOAuth2DataConverter { + + /** + * 转换 scope 数据格式:String -> List + * @param scopeString / + * @return / + */ + List convertScopeStringToList(String scopeString); + + /** + * 转换 scope 数据格式:List -> String + * @param scopeList / + * @return / + */ + String convertScopeListToString(List scopeList); + + /** + * 转换 redirect_uri 数据格式:String -> List + * @param redirectUris / + * @return / + */ + List convertRedirectUriStringToList(String redirectUris); + + /** + * 根据 RequestAuthModel 构建一个 CodeModel + * @param ra RequestAuthModel + * @return CodeModel 对象 + */ + CodeModel convertRequestAuthToCode(RequestAuthModel ra); + + /** + * 根据 RequestAuthModel 构建一个 AccessTokenModel + * @param ra RequestAuthModel + * @param accessTokenTimeout Access-Token 有效期 (单位:秒) + * @return AccessTokenModel 对象 + */ + AccessTokenModel convertRequestAuthToAccessToken(RequestAuthModel ra, long accessTokenTimeout); + + /** + * 根据 Code 构建一个 Access-Token + * @param cm CodeModel对象 + * @param accessTokenTimeout Access-Token 有效期 (单位:秒) + * @return AccessToken对象 + */ + AccessTokenModel convertCodeToAccessToken(CodeModel cm, long accessTokenTimeout); + + /** + * 根据 Access-Token 构建一个 Refresh-Token + * @param at / + * @param refreshTokenTimeout Refresh-Token 有效期 (单位:秒) + * @return / + */ + RefreshTokenModel convertAccessTokenToRefreshToken(AccessTokenModel at, long refreshTokenTimeout); + + /** + * 根据 Refresh-Token 构建一个 Access-Token + * @param rt / + * @param accessTokenTimeout Access-Token 有效期 (单位:秒) + * @return / + */ + AccessTokenModel convertRefreshTokenToAccessToken(RefreshTokenModel rt, long accessTokenTimeout); + + /** + * 根据 Refresh-Token 构建一个新的 Refresh-Token + * @param rt / + * @param refreshTokenTimeout Refresh-Token 有效期 (单位:秒) + * @return / + */ + RefreshTokenModel convertRefreshTokenToRefreshToken(RefreshTokenModel rt, long refreshTokenTimeout); + + /** + * 根据 SaClientModel 构建一个 ClientTokenModel + * @param clientModel / + * @param scopes 权限列表 + * @return / + */ + ClientTokenModel convertSaClientToClientToken(SaClientModel clientModel, List scopes); + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverterDefaultImpl.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverterDefaultImpl.java new file mode 100644 index 000000000..507dd68a0 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverterDefaultImpl.java @@ -0,0 +1,200 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.convert; + +import cn.dev33.satoken.oauth2.consts.GrantType; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaTtlMethods; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +/** + * Sa-Token OAuth2 数据格式转换器,默认实现类 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2DataConverterDefaultImpl implements SaOAuth2DataConverter, SaTtlMethods { + + /** + * 转换 scope 数据格式:String -> List + */ + @Override + public List convertScopeStringToList(String scopeString) { + if(SaFoxUtil.isEmpty(scopeString)) { + return new ArrayList<>(); + } + // 兼容以下三种分隔符:空格、逗号、%20、加号 + scopeString = scopeString.replace(" ", ","); + scopeString = scopeString.replace("%20", ","); + scopeString = scopeString.replace("+", ","); + return SaFoxUtil.convertStringToList(scopeString); + } + + /** + * 转换 scope 数据格式:List -> String + */ + @Override + public String convertScopeListToString(List scopeList) { + return SaFoxUtil.convertListToString(scopeList); + } + + /** + * 转换 redirect_uri 数据格式:String -> List + */ + @Override + public List convertRedirectUriStringToList(String redirectUris) { + if(SaFoxUtil.isEmpty(redirectUris)) { + return new ArrayList<>(); + } + return SaFoxUtil.convertStringToList(redirectUris); + } + + /** + * 根据 RequestAuthModel 构建一个 CodeModel + * @param ra RequestAuthModel + * @return CodeModel 对象 + */ + @Override + public CodeModel convertRequestAuthToCode(RequestAuthModel ra){ + String codeValue = SaOAuth2Strategy.instance.createCodeValue.execute(ra.clientId, ra.loginId, ra.scopes); + CodeModel cm = new CodeModel(); + cm.code = codeValue; + cm.clientId = ra.clientId; + cm.scopes = ra.scopes; + cm.loginId = ra.loginId; + cm.redirectUri = ra.redirectUri; + cm.nonce = ra.getNonce(); + return cm; + } + + /** + * 根据 RequestAuthModel 构建一个 AccessTokenModel + * @param ra RequestAuthModel + * @return AccessTokenModel 对象 + */ + @Override + public AccessTokenModel convertRequestAuthToAccessToken(RequestAuthModel ra, long accessTokenTimeout) { + String newAtValue = SaOAuth2Strategy.instance.createAccessToken.execute(ra.clientId, ra.loginId, ra.scopes); + AccessTokenModel at = new AccessTokenModel(); + at.accessToken = newAtValue; + at.clientId = ra.clientId; + at.loginId = ra.loginId; + at.scopes = ra.scopes; + at.tokenType = SaOAuth2Consts.TokenType.Bearer; + at.expiresTime = ttlToExpireTime(accessTokenTimeout); + at.extraData = new LinkedHashMap<>(); + return at; + } + + /** + * 根据 Code 构建一个 Access-Token + */ + @Override + public AccessTokenModel convertCodeToAccessToken(CodeModel cm, long accessTokenTimeout) { + AccessTokenModel at = new AccessTokenModel(); + at.accessToken = SaOAuth2Strategy.instance.createAccessToken.execute(cm.clientId, cm.loginId, cm.scopes); + at.clientId = cm.clientId; + at.loginId = cm.loginId; + at.scopes = cm.scopes; + at.tokenType = SaOAuth2Consts.TokenType.Bearer; + at.grantType = GrantType.authorization_code; + at.expiresTime = ttlToExpireTime(accessTokenTimeout); + at.extraData = new LinkedHashMap<>(); + return at; + } + + /** + * 根据 Access-Token 构建一个 Refresh-Token + */ + @Override + public RefreshTokenModel convertAccessTokenToRefreshToken(AccessTokenModel at, long refreshTokenTimeout) { + RefreshTokenModel rt = new RefreshTokenModel(); + rt.refreshToken = SaOAuth2Strategy.instance.createRefreshToken.execute(at.clientId, at.loginId, at.scopes); + rt.clientId = at.clientId; + rt.loginId = at.loginId; + rt.scopes = at.scopes; + rt.expiresTime = ttlToExpireTime(refreshTokenTimeout); + rt.extraData = new LinkedHashMap<>(at.extraData); + return rt; + } + + /** + * 根据 Refresh-Token 构建一个 Access-Token + */ + @Override + public AccessTokenModel convertRefreshTokenToAccessToken(RefreshTokenModel rt, long accessTokenTimeout) { + AccessTokenModel at = new AccessTokenModel(); + at.accessToken = SaOAuth2Strategy.instance.createAccessToken.execute(rt.clientId, rt.loginId, rt.scopes); + at.refreshToken = rt.refreshToken; + at.clientId = rt.clientId; + at.loginId = rt.loginId; + at.scopes = rt.scopes; + at.tokenType = SaOAuth2Consts.TokenType.Bearer; + at.grantType = GrantType.refresh_token; + at.extraData = new LinkedHashMap<>(rt.extraData); + at.expiresTime = ttlToExpireTime(accessTokenTimeout); + at.refreshExpiresTime = rt.expiresTime; + return at; + } + + /** + * 根据 Refresh-Token 构建一个新的 Refresh-Token + */ + @Override + public RefreshTokenModel convertRefreshTokenToRefreshToken(RefreshTokenModel rt, long refreshTokenTimeout) { + RefreshTokenModel newRt = new RefreshTokenModel(); + newRt.refreshToken = SaOAuth2Strategy.instance.createRefreshToken.execute(rt.clientId, rt.loginId, rt.scopes); + newRt.expiresTime = ttlToExpireTime(refreshTokenTimeout); + newRt.clientId = rt.clientId; + newRt.scopes = rt.scopes; + newRt.loginId = rt.loginId; + newRt.extraData = new LinkedHashMap<>(rt.extraData); + return newRt; + } + + /** + * 根据 SaClientModel 构建一个 ClientTokenModel + * @param clientModel / + * @param scopes 权限列表 + * @return / + */ + @Override + public ClientTokenModel convertSaClientToClientToken(SaClientModel clientModel, List scopes) { + String clientTokenValue = SaOAuth2Strategy.instance.createClientToken.execute(clientModel.getClientId(), scopes); + ClientTokenModel ct = new ClientTokenModel(clientTokenValue, clientModel.getClientId(), scopes); + ct.clientToken = clientTokenValue; + ct.clientId = clientModel.getClientId(); + ct.scopes = scopes; + ct.tokenType = SaOAuth2Consts.TokenType.Bearer; + ct.expiresTime = ttlToExpireTime(clientModel.getClientTokenTimeout()); + ct.grantType = GrantType.client_credentials; + ct.extraData = new LinkedHashMap<>(); + return ct; + } + +} + diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java new file mode 100644 index 000000000..b258d2edd --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.generate; + +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Sa-Token OAuth2 数据构建器,负责相关 Model 数据构建 + * + * @author click33 + * @since 1.39.0 + */ +public interface SaOAuth2DataGenerate { + + /** + * 构建 Model:Code授权码 + * @param ra 请求参数Model + * @return 授权码Model + */ + CodeModel generateCode(RequestAuthModel ra); + + /** + * 构建 Model:Access-Token (根据 code 授权码) + * @param code 授权码Model + * @return AccessToken Model + */ + AccessTokenModel generateAccessToken(String code); + + /** + * 刷新 Model:根据 Refresh-Token 生成一个新的 Access-Token + * @param refreshToken Refresh-Token值 + * @return 新的 Access-Token + */ + AccessTokenModel refreshAccessToken(String refreshToken); + + /** + * 构建 Model:Access-Token (根据 RequestAuthModel 构建,用于隐藏式 and 密码式) + * @param ra 请求参数Model + * @param isCreateRt 是否生成对应的Refresh-Token + * @param appendWork 对生成的 AccessTokenModel 进行追加操作 + * @return Access-Token Model + */ + AccessTokenModel generateAccessToken(RequestAuthModel ra, boolean isCreateRt, Consumer appendWork); + + /** + * 构建 Model:Client-Token + * @param clientId 应用id + * @param scopes 授权范围 + * @return Client-Token Model + */ + ClientTokenModel generateClientToken(String clientId, List scopes); + + /** + * 构建 URL:下放Code URL (Authorization Code 授权码) + * @param redirectUri 下放地址 + * @param code code参数 + * @param state state参数 + * @return 构建完毕的URL + */ + String buildRedirectUri(String redirectUri, String code, String state); + + /** + * 构建 URL:下放Access-Token URL (implicit 隐藏式) + * @param redirectUri 下放地址 + * @param token token + * @param state state参数 + * @return 构建完毕的URL + */ + String buildImplicitRedirectUri(String redirectUri, String token, String state); + + /** + * 检查 state 是否被重复使用 + * @param state / + */ + void checkState(String state); + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java new file mode 100644 index 000000000..dfd6328f9 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java @@ -0,0 +1,270 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.generate; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; +import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2AuthorizationCodeException; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.oauth2.exception.SaOAuth2RefreshTokenException; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaTtlMethods; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Sa-Token OAuth2 数据构建器,默认实现类 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate, SaTtlMethods { + + /** + * 构建 Model:Code授权码 + * @param ra 请求参数Model + * @return 授权码Model + */ + @Override + public CodeModel generateCode(RequestAuthModel ra) { + + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + // 删除旧 Code + dao.deleteCode(dao.getCodeValue(ra.clientId, ra.loginId)); + + // 生成新 Code + CodeModel cm = SaOAuth2Manager.getDataConverter().convertRequestAuthToCode(ra); + + // 保存新 Code + dao.saveCode(cm); + dao.saveCodeIndex(cm); + + // 保存 code -> nonce + dao.saveCodeNonceIndex(cm); + + // 返回 + return cm; + } + + /** + * 构建 Model:Access-Token (根据 code 授权码) + * @param code 授权码 + * @return AccessToken Model + */ + @Override + public AccessTokenModel generateAccessToken(String code) { + + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + SaOAuth2DataConverter dataConverter = SaOAuth2Manager.getDataConverter(); + + // 1、先校验 + CodeModel cm = dao.getCode(code); + SaOAuth2AuthorizationCodeException.throwBy(cm == null, "无效 code: " + code, code, SaOAuth2ErrorCode.CODE_30110); + + // 2、开发者自定义的授权前置检查 + SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(cm.loginId, cm.clientId); + + // 3、生成新 Access-Token + SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(cm.clientId); + AccessTokenModel at = dataConverter.convertCodeToAccessToken(cm, clientModel.getAccessTokenTimeout()); + SaOAuth2Strategy.instance.workAccessTokenByScope.accept(at); + + // 4、生成新 Refresh-Token + RefreshTokenModel rt = dataConverter.convertAccessTokenToRefreshToken(at, clientModel.getRefreshTokenTimeout()); + at.refreshToken = rt.refreshToken; + at.refreshExpiresTime = rt.expiresTime; + + // 5、保存 Access-Token、Refresh-Token + dao.saveAccessToken(at); + dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount()); + dao.saveRefreshToken(rt); + dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount()); + + // 6、删除 Code (一个 code 只可以使用一次) + dao.deleteCode(code); + dao.deleteCodeIndex(cm.clientId, cm.loginId); + + // 7、返回 Access-Token + return at; + } + + /** + * 刷新 Model:根据 Refresh-Token 生成一个新的 Access-Token + * @param refreshToken Refresh-Token值 + * @return 新的 Access-Token + */ + @Override + public AccessTokenModel refreshAccessToken(String refreshToken) { + + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + // 1、获取 Refresh-Token 信息 + RefreshTokenModel rt = dao.getRefreshToken(refreshToken); + SaOAuth2RefreshTokenException.throwBy(rt == null, "无效 refresh_token: " + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111); + + // 2、开发者自定义的授权前置检查 + SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(rt.loginId, rt.clientId); + + // 3、如果配置了 isNewRefresh=true,则生成一个新的 Refresh-Token + SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(rt.clientId); + if(clientModel.getIsNewRefresh()) { + rt = SaOAuth2Manager.getDataConverter().convertRefreshTokenToRefreshToken(rt, clientModel.getRefreshTokenTimeout()); + dao.saveRefreshToken(rt); + dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount()); + } + + // 4、生成新 Access-Token + AccessTokenModel at = SaOAuth2Manager.getDataConverter().convertRefreshTokenToAccessToken(rt, clientModel.getAccessTokenTimeout()); + SaOAuth2Strategy.instance.refreshAccessTokenWorkByScope.accept(at); + + // 5、保存新 Access-Token + dao.saveAccessToken(at); + dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount()); + + // 6、返回新 Access-Token + return at; + } + + /** + * 构建 Model:Access-Token (根据 RequestAuthModel 构建,用于隐藏式 and 密码式) + * @param ra 请求参数Model + * @param isCreateRt 是否生成对应的Refresh-Token + * @param appendWork 对生成的 AccessTokenModel 进行追加操作 + * + * @return Access-Token Model + */ + @Override + public AccessTokenModel generateAccessToken(RequestAuthModel ra, boolean isCreateRt, Consumer appendWork) { + + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + SaOAuth2DataConverter dataConverter = SaOAuth2Manager.getDataConverter(); + + // 1、开发者自定义的授权前置检查 + SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId); + + // 2、生成 Access-Token + SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(ra.clientId); + AccessTokenModel at = dataConverter.convertRequestAuthToAccessToken(ra, clientModel.getAccessTokenTimeout()); + if(appendWork != null) { + appendWork.accept(at); + } + SaOAuth2Strategy.instance.workAccessTokenByScope.accept(at); + + // 3、生成 & 保存 Refresh-Token + if(isCreateRt) { + RefreshTokenModel rt = dataConverter.convertAccessTokenToRefreshToken(at, clientModel.getRefreshTokenTimeout()); + at.refreshToken = rt.refreshToken; + at.refreshExpiresTime = rt.expiresTime; + + dao.saveRefreshToken(rt); + dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount()); + } + + // 4、保存 Access-Token + dao.saveAccessToken(at); + dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount()); + + // 5、返回 Access-Token + return at; + } + + /** + * 构建 Model:Client-Token + * @param clientId 应用id + * @param scopes 授权范围 + * @return Client-Token Model + */ + @Override + public ClientTokenModel generateClientToken(String clientId, List scopes) { + + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + // 1、如果配置了 Lower-Client-Token 的 ttl ,则需要更新一下 + SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(clientId); + + // 2、生成 Client-Token + ClientTokenModel ct = SaOAuth2Manager.getDataConverter().convertSaClientToClientToken(clientModel, scopes); + SaOAuth2Strategy.instance.workClientTokenByScope.accept(ct); + + // 3、保存 Client-Token + dao.saveClientToken(ct); + dao.saveClientTokenIndex_AndAdjust(ct, clientModel.getMaxClientTokenCount()); + + // 4、返回 + return ct; + } + + /** + * 构建 URL:下放Code URL (Authorization Code 授权码) + * @param redirectUri 下放地址 + * @param code code参数 + * @param state state参数 + * @return 构建完毕的URL + */ + @Override + public String buildRedirectUri(String redirectUri, String code, String state) { + String url = SaFoxUtil.joinParam(redirectUri, SaOAuth2Consts.Param.code, code); + if( ! SaFoxUtil.isEmpty(state)) { + checkState(state); + url = SaFoxUtil.joinParam(url, SaOAuth2Consts.Param.state, state); + } + return url; + } + + /** + * 构建 URL:下放Access-Token URL (implicit 隐藏式) + * @param redirectUri 下放地址 + * @param token token + * @param state state参数 + * @return 构建完毕的URL + */ + @Override + public String buildImplicitRedirectUri(String redirectUri, String token, String state) { + String url = SaFoxUtil.joinSharpParam(redirectUri, SaOAuth2Consts.Param.token, token); + if( ! SaFoxUtil.isEmpty(state)) { + checkState(state); + url = SaFoxUtil.joinSharpParam(url, SaOAuth2Consts.Param.state, state); + } + return url; + } + + /** + * 检查 state 是否被重复使用 + * @param state / + */ + @Override + public void checkState(String state) { + String value = SaOAuth2Manager.getDao().getState(state); + if(SaFoxUtil.isNotEmpty(value)) { + throw new SaOAuth2Exception("多次请求的 state 不可重复: " + state).setCode(SaOAuth2ErrorCode.CODE_30127); + } + SaOAuth2Manager.getDao().saveState(state); + } + +} + diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java new file mode 100644 index 000000000..7da926bb3 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.loader; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2ClientModelException; +import cn.dev33.satoken.secure.SaSecureUtil; + +import java.util.List; + +/** + * Sa-Token OAuth2 数据加载器 + * + * @author click33 + * @since 1.39.0 + */ +public interface SaOAuth2DataLoader { + + /** + * 根据 id 获取 Client 信息 + * + * @param clientId 应用id + * @return ClientModel + */ + default SaClientModel getClientModel(String clientId) { + // 默认从内存配置中读取数据 + return SaOAuth2Manager.getServerConfig().getClients().get(clientId); + } + + /** + * 根据 id 获取 Client 信息,不允许为 null + * + * @param clientId 应用id + * @return ClientModel + */ + default SaClientModel getClientModelNotNull(String clientId) { + SaClientModel clientModel = getClientModel(clientId); + if(clientModel == null) { + throw new SaOAuth2ClientModelException("无效 client_id: " + clientId) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30105); + } + return clientModel; + } + + /** + * 根据 ClientId 和 LoginId 获取 openid + * + * @param clientId 应用id + * @param loginId 账号id + * @return 此账号在此Client下的openid + */ + default String getOpenid(String clientId, Object loginId) { + return SaSecureUtil.md5(SaOAuth2Manager.getServerConfig().getOpenidDigestPrefix() + "_" + clientId + "_" + loginId); + } + + /** + * 根据 subjectId 和 loginId 获取 unionid + * + * @param subjectId 应用主体id + * @param loginId 账号id + * @return 此账号在此主体 Client 下的 unionid + */ + default String getUnionid(String subjectId, Object loginId) { + return SaSecureUtil.md5(SaOAuth2Manager.getServerConfig().getUnionidDigestPrefix() + "_" + subjectId + "_" + loginId); + } + + /** + * 获取高级权限列表 + * @return / + */ + default List getHigherScopeList() { + String higherScope = SaOAuth2Manager.getServerConfig().getHigherScope(); + return SaOAuth2Manager.getDataConverter().convertScopeStringToList(higherScope); + } + + /** + * 获取低级权限列表 + * @return / + */ + default List getLowerScopeList() { + String lowerScope = SaOAuth2Manager.getServerConfig().getLowerScope(); + return SaOAuth2Manager.getDataConverter().convertScopeStringToList(lowerScope); + } +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoaderDefaultImpl.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoaderDefaultImpl.java new file mode 100644 index 000000000..7156cda16 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoaderDefaultImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.loader; + +/** + * Sa-Token OAuth2 数据加载器 默认实现类 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2DataLoaderDefaultImpl implements SaOAuth2DataLoader{ + + // be empty of + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/AccessTokenModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/AccessTokenModel.java new file mode 100644 index 000000000..0dd033db3 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/AccessTokenModel.java @@ -0,0 +1,247 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.model; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Model: Access-Token + * + * @author click33 + * @since 1.23.0 + */ +public class AccessTokenModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * Access-Token 值 + */ + public String accessToken; + + /** + * Refresh-Token 值 + */ + public String refreshToken; + + /** + * Access-Token 到期时间 + */ + public long expiresTime; + + /** + * Refresh-Token 到期时间 + */ + public long refreshExpiresTime; + + /** + * 应用id + */ + public String clientId; + + /** + * 账号id + */ + public Object loginId; + + /** + * 授权范围 + */ + public List scopes; + + /** + * Token 类型 + */ + public String tokenType; + + /** + * 授权类型 + */ + public String grantType; + + /** + * 扩展数据 + */ + public Map extraData; + + /** + * 创建时间,13位时间戳 + */ + public long createTime; + + + public AccessTokenModel() { + this.createTime = System.currentTimeMillis(); + } + + /** + * 构建一个 + * @param accessToken accessToken + * @param clientId 应用id + * @param scopes 请求授权范围 + * @param loginId 对应的账号id + */ + public AccessTokenModel(String accessToken, String clientId, Object loginId, List scopes) { + this(); + this.accessToken = accessToken; + this.clientId = clientId; + this.loginId = loginId; + this.scopes = scopes; + } + + // 额外追加方法 + + /** + * 获取:此 Access-Token 的剩余有效期(秒) + * @return / + */ + public long getExpiresIn() { + long s = (expiresTime - System.currentTimeMillis()) / 1000; + return s < 1 ? -2 : s; + } + + /** + * 获取:此 Refresh-Token 的剩余有效期(秒) + * @return / + */ + public long getRefreshExpiresIn() { + long s = (refreshExpiresTime - System.currentTimeMillis()) / 1000; + return s < 1 ? -2 : s; + } + + + // get set + + public String getAccessToken() { + return accessToken; + } + + public AccessTokenModel setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public String getRefreshToken() { + return refreshToken; + } + + public AccessTokenModel setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + public long getExpiresTime() { + return expiresTime; + } + + public AccessTokenModel setExpiresTime(long expiresTime) { + this.expiresTime = expiresTime; + return this; + } + + public long getRefreshExpiresTime() { + return refreshExpiresTime; + } + + public AccessTokenModel setRefreshExpiresTime(long refreshExpiresTime) { + this.refreshExpiresTime = refreshExpiresTime; + return this; + } + + public String getClientId() { + return clientId; + } + + public AccessTokenModel setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Object getLoginId() { + return loginId; + } + + public AccessTokenModel setLoginId(Object loginId) { + this.loginId = loginId; + return this; + } + + public List getScopes() { + return scopes; + } + + public AccessTokenModel setScopes(List scopes) { + this.scopes = scopes; + return this; + } + + public String getTokenType() { + return tokenType; + } + + public AccessTokenModel setTokenType(String tokenType) { + this.tokenType = tokenType; + return this; + } + + public String getGrantType() { + return grantType; + } + + public AccessTokenModel setGrantType(String grantType) { + this.grantType = grantType; + return this; + } + + public Map getExtraData() { + return extraData; + } + + public AccessTokenModel setExtraData(Map extraData) { + this.extraData = extraData; + return this; + } + + public long getCreateTime() { + return createTime; + } + + public AccessTokenModel setCreateTime(long createTime) { + this.createTime = createTime; + return this; + } + + @Override + public String toString() { + return "AccessTokenModel{" + + "accessToken='" + accessToken + + ", refreshToken='" + refreshToken + + ", expiresTime=" + expiresTime + + ", refreshExpiresTime=" + refreshExpiresTime + + ", clientId='" + clientId + + ", loginId=" + loginId + + ", scopes=" + scopes + + ", tokenType='" + tokenType + + ", grantType='" + grantType + + ", extraData=" + extraData + + ", createTime=" + createTime + + '}'; + } + + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/ClientTokenModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/ClientTokenModel.java new file mode 100644 index 000000000..23aa36b93 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/ClientTokenModel.java @@ -0,0 +1,189 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.model; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Model: Client-Token + * + * @author click33 + * @since 1.23.0 + */ +public class ClientTokenModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * Client-Token 值 + */ + public String clientToken; + + /** + * Client-Token 到期时间 + */ + public long expiresTime; + + /** + * 应用id + */ + public String clientId; + + /** + * 授权范围 + */ + public List scopes; + + /** + * Token 类型 + */ + public String tokenType; + + /** + * 授权类型 + */ + public String grantType; + + /** + * 扩展数据 + */ + public Map extraData; + + /** + * 创建时间,13位时间戳 + */ + public long createTime; + + public ClientTokenModel(){ + this.createTime = System.currentTimeMillis(); + } + + /** + * 构建一个 ClientTokenModel + * @param clientToken clientToken + * @param clientId 应用id + * @param scopes 请求授权范围 + */ + public ClientTokenModel(String clientToken, String clientId, List scopes) { + this(); + this.clientToken = clientToken; + this.clientId = clientId; + this.scopes = scopes; + } + + // 额外追加方法 + + /** + * 获取:此 Client-Token 的剩余有效期(秒) + * @return / + */ + public long getExpiresIn() { + long s = (expiresTime - System.currentTimeMillis()) / 1000; + return s < 1 ? -2 : s; + } + + + // get set + + public String getClientToken() { + return clientToken; + } + + public ClientTokenModel setClientToken(String clientToken) { + this.clientToken = clientToken; + return this; + } + + public long getExpiresTime() { + return expiresTime; + } + + public ClientTokenModel setExpiresTime(long expiresTime) { + this.expiresTime = expiresTime; + return this; + } + + public String getClientId() { + return clientId; + } + + public ClientTokenModel setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + public List getScopes() { + return scopes; + } + + public ClientTokenModel setScopes(List scopes) { + this.scopes = scopes; + return this; + } + + public String getTokenType() { + return tokenType; + } + + public ClientTokenModel setTokenType(String tokenType) { + this.tokenType = tokenType; + return this; + } + + public String getGrantType() { + return grantType; + } + + public ClientTokenModel setGrantType(String grantType) { + this.grantType = grantType; + return this; + } + + public Map getExtraData() { + return extraData; + } + + public ClientTokenModel setExtraData(Map extraData) { + this.extraData = extraData; + return this; + } + + public long getCreateTime() { + return createTime; + } + + public ClientTokenModel setCreateTime(long createTime) { + this.createTime = createTime; + return this; + } + + @Override + public String toString() { + return "ClientTokenModel{" + + "clientToken='" + clientToken + + ", expiresTime=" + expiresTime + + ", clientId='" + clientId + + ", scopes=" + scopes + + ", tokenType=" + tokenType + + ", grantType=" + grantType + + ", extraData=" + extraData + + ", createTime=" + createTime + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/CodeModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/CodeModel.java new file mode 100644 index 000000000..da2ccb0ff --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/CodeModel.java @@ -0,0 +1,168 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.model; + +import java.io.Serializable; +import java.util.List; + +/** + * Model: 授权码 + * + * @author click33 + * @since 1.23.0 + */ +public class CodeModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 授权码 + */ + public String code; + + /** + * 应用id + */ + public String clientId; + + /** + * 授权范围 + */ + public List scopes; + + /** + * 对应账号id + */ + public Object loginId; + + /** + * 重定向的地址 + */ + public String redirectUri; + + /** + * 随机数 + */ + public String nonce; + + /** + * 创建时间,13位时间戳 + */ + public long createTime; + + /** + * 构建一个 + */ + public CodeModel() { + this.createTime = System.currentTimeMillis(); + } + + /** + * 构建一个 + * @param code 授权码 + * @param clientId 应用id + * @param scopes 请求授权范围 + * @param loginId 对应的账号id + * @param redirectUri 重定向地址 + * @param nonce 随机数 + */ + public CodeModel(String code, String clientId, List scopes, Object loginId, String redirectUri, String nonce) { + this(); + this.code = code; + this.clientId = clientId; + this.scopes = scopes; + this.loginId = loginId; + this.redirectUri = redirectUri; + this.nonce = nonce; + } + + public String getCode() { + return code; + } + + public CodeModel setCode(String code) { + this.code = code; + return this; + } + + public String getClientId() { + return clientId; + } + + public CodeModel setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + public List getScopes() { + return scopes; + } + + public CodeModel setScopes(List scopes) { + this.scopes = scopes; + return this; + } + + public Object getLoginId() { + return loginId; + } + + public CodeModel setLoginId(Object loginId) { + this.loginId = loginId; + return this; + } + + public String getRedirectUri() { + return redirectUri; + } + + public CodeModel setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + return this; + } + + public String getNonce() { + return nonce; + } + + public CodeModel setNonce(String nonce) { + this.nonce = nonce; + return this; + } + + public long getCreateTime() { + return createTime; + } + + public CodeModel setCreateTime(long createTime) { + this.createTime = createTime; + return this; + } + + @Override + public String toString() { + return "CodeModel{" + + "code='" + code + '\'' + + ", clientId='" + clientId + '\'' + + ", scopes=" + scopes + + ", loginId=" + loginId + + ", redirectUri='" + redirectUri + '\'' + + ", nonce='" + nonce + '\'' + + ", createTime=" + createTime + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/RefreshTokenModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/RefreshTokenModel.java new file mode 100644 index 000000000..3919f6216 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/RefreshTokenModel.java @@ -0,0 +1,162 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.model; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Model: Refresh-Token + * + * @author click33 + * @since 1.23.0 + */ +public class RefreshTokenModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * Refresh-Token 值 + */ + public String refreshToken; + + /** + * Refresh-Token 到期时间 + */ + public long expiresTime; + + /** + * 应用id + */ + public String clientId; + + /** + * 对应账号id + */ + public Object loginId; + + /** + * 授权范围 + */ + public List scopes; + + /** + * 扩展数据 + */ + public Map extraData; + + /** + * 创建时间,13位时间戳 + */ + public long createTime; + + public RefreshTokenModel() { + this.createTime = System.currentTimeMillis(); + } + + + // 额外追加方法 + + /** + * 获取:此 Refresh-Token 的剩余有效期(秒) + * @return / + */ + public long getExpiresIn() { + long s = (expiresTime - System.currentTimeMillis()) / 1000; + return s < 1 ? -2 : s; + } + + + // get set + + public String getRefreshToken() { + return refreshToken; + } + + public RefreshTokenModel setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + public long getExpiresTime() { + return expiresTime; + } + + public RefreshTokenModel setExpiresTime(long expiresTime) { + this.expiresTime = expiresTime; + return this; + } + + public String getClientId() { + return clientId; + } + + public RefreshTokenModel setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + public List getScopes() { + return scopes; + } + + public RefreshTokenModel setScopes(List scopes) { + this.scopes = scopes; + return this; + } + + public Object getLoginId() { + return loginId; + } + + public RefreshTokenModel setLoginId(Object loginId) { + this.loginId = loginId; + return this; + } + + public Map getExtraData() { + return extraData; + } + + public RefreshTokenModel setExtraData(Map extraData) { + this.extraData = extraData; + return this; + } + + public long getCreateTime() { + return createTime; + } + + public RefreshTokenModel setCreateTime(long createTime) { + this.createTime = createTime; + return this; + } + + @Override + public String toString() { + return "RefreshTokenModel [" + + "refreshToken=" + refreshToken + + ", expiresTime=" + expiresTime + + ", clientId=" + clientId + + ", loginId=" + loginId + + ", scopes=" + scopes + + ", extraData=" + extraData + + ", createTime=" + createTime + + "]"; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java new file mode 100644 index 000000000..d2d1d9edc --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java @@ -0,0 +1,401 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.model.loader; + +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Client 应用信息 Model + * + * @author click33 + * @since 1.23.0 + */ +public class SaClientModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 应用id + */ + public String clientId; + + /** + * 应用秘钥 + */ + public String clientSecret; + + /** + * 应用签约的所有权限 + */ + public List contractScopes = new ArrayList<>(); + + /** + * 应用允许授权的所有 redirect_uri + */ + public List allowRedirectUris = new ArrayList<>(); + + /** + * 应用允许的所有 grant_type + */ + public List allowGrantTypes = new ArrayList<>(); + + /** + * 主体id + */ + public String subjectId; + + /** 此应用 Access-Token 保存的时间(单位秒) [默认取全局配置] */ + public long accessTokenTimeout; + + /** 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置] */ + public long refreshTokenTimeout; + + /** 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置] */ + public long clientTokenTimeout; + + /** 此应用单个用户最多同时存在的 Access-Token 数量 */ + public int maxAccessTokenCount; + + /** 此应用单个用户最多同时存在的 Refresh-Token 数量 */ + public int maxRefreshTokenCount; + + /** 此应用最多同时存在的 Client-Token 数量 */ + public int maxClientTokenCount; + + /** 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token [默认取全局配置] */ + public Boolean isNewRefresh; + + /** 是否允许此应用自动确认授权(高危配置,禁止向不被信任的第三方开启此选项) */ + public Boolean isAutoConfirm = false; + + + public SaClientModel() { + SaOAuth2Strategy.instance.setSaClientModelDefaultFields.run(this); + } + public SaClientModel(String clientId, String clientSecret, List contractScopes, List allowRedirectUris) { + this(); + this.clientId = clientId; + this.clientSecret = clientSecret; + this.contractScopes = contractScopes; + this.allowRedirectUris = allowRedirectUris; + } + + + // 追加方法 + + /** + * @param scopes 添加应用签约的所有权限 + * @return 对象自身 + */ + public SaClientModel addContractScopes(String... scopes) { + if(this.contractScopes == null) { + this.contractScopes = new ArrayList<>(); + } + this.contractScopes.addAll(Arrays.asList(scopes)); + return this; + } + + /** + * @param redirectUris 添加应用允许授权的所有 redirect_uri + * @return 对象自身 + */ + public SaClientModel addAllowRedirectUris(String... redirectUris) { + if(this.allowRedirectUris == null) { + this.allowRedirectUris = new ArrayList<>(); + } + this.allowRedirectUris.addAll(Arrays.asList(redirectUris)); + return this; + } + + /** + * @param grantTypes 应用允许的所有 grant_type + * @return 对象自身 + */ + public SaClientModel addAllowGrantTypes(String... grantTypes) { + if(this.allowGrantTypes == null) { + this.allowGrantTypes = new ArrayList<>(); + } + this.allowGrantTypes.addAll(Arrays.asList(grantTypes)); + return this; + } + + + // get set + + /** + * @return 应用id + */ + public String getClientId() { + return clientId; + } + + /** + * @param clientId 应用id + * @return 对象自身 + */ + public SaClientModel setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + /** + * @return 应用秘钥 + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * @param clientSecret 应用秘钥 + * @return 对象自身 + */ + public SaClientModel setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + /** + * @return 应用签约的所有权限 + */ + public List getContractScopes() { + return contractScopes; + } + + /** + * @param contractScopes 应用签约的所有权限 + * @return 对象自身 + */ + public SaClientModel setContractScopes(List contractScopes) { + this.contractScopes = contractScopes; + return this; + } + + /** + * @return 应用允许授权的所有 redirect_uri + */ + public List getAllowRedirectUris() { + return allowRedirectUris; + } + + /** + * @param allowRedirectUris 应用允许授权的所有 redirect_uri + * @return 对象自身 + */ + public SaClientModel setAllowRedirectUris(List allowRedirectUris) { + this.allowRedirectUris = allowRedirectUris; + return this; + } + + /** + * @return 应用允许的所有 grant_type + */ + public List getAllowGrantTypes() { + return allowGrantTypes; + } + + /** + * 应用允许的所有 grant_type + * @param allowGrantTypes / + * @return / + */ + public SaClientModel setAllowGrantTypes(List allowGrantTypes) { + this.allowGrantTypes = allowGrantTypes; + return this; + } + + /** + * 获取 主体id + * + * @return subjectId 主体id + */ + public String getSubjectId() { + return this.subjectId; + } + + /** + * 设置 主体id + * + * @param subjectId 主体id + */ + public SaClientModel setSubjectId(String subjectId) { + this.subjectId = subjectId; + return this; + } + + /** + * @return 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token [默认取全局配置] + */ + public Boolean getIsNewRefresh() { + return isNewRefresh; + } + + /** + * @param isNewRefresh 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token [默认取全局配置] + * @return 对象自身 + */ + public SaClientModel setIsNewRefresh(Boolean isNewRefresh) { + this.isNewRefresh = isNewRefresh; + return this; + } + + /** + * @return 此应用 Access-Token 保存的时间(单位秒) [默认取全局配置] + */ + public long getAccessTokenTimeout() { + return accessTokenTimeout; + } + + /** + * @param accessTokenTimeout 此应用 Access-Token 保存的时间(单位秒) [默认取全局配置] + * @return 对象自身 + */ + public SaClientModel setAccessTokenTimeout(long accessTokenTimeout) { + this.accessTokenTimeout = accessTokenTimeout; + return this; + } + + /** + * @return 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置] + */ + public long getRefreshTokenTimeout() { + return refreshTokenTimeout; + } + + /** + * @param refreshTokenTimeout 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置] + * @return 对象自身 + */ + public SaClientModel setRefreshTokenTimeout(long refreshTokenTimeout) { + this.refreshTokenTimeout = refreshTokenTimeout; + return this; + } + + /** + * @return 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置] + */ + public long getClientTokenTimeout() { + return clientTokenTimeout; + } + + /** + * @param clientTokenTimeout 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置] + * @return 对象自身 + */ + public SaClientModel setClientTokenTimeout(long clientTokenTimeout) { + this.clientTokenTimeout = clientTokenTimeout; + return this; + } + + /** + * 获取 是否允许此应用自动确认授权(高危配置,禁止向不被信任的第三方开启此选项) + * + * @return / + */ + public Boolean getIsAutoConfirm() { + return this.isAutoConfirm; + } + + /** + * 设置 是否允许此应用自动确认授权(高危配置,禁止向不被信任的第三方开启此选项) + * + * @param isAutoConfirm / + * @return 对象自身 + */ + public SaClientModel setIsAutoConfirm(Boolean isAutoConfirm) { + this.isAutoConfirm = isAutoConfirm; + return this; + } + + /** + * 此应用单个用户最多同时存在的 Access-Token 数量 + * @return / + */ + public int getMaxAccessTokenCount() { + return maxAccessTokenCount; + } + + /** + * 设置 此应用单个用户最多同时存在的 Access-Token 数量 + * @param maxAccessTokenCount / + * @return 对象自身 + */ + public SaClientModel setMaxAccessTokenCount(int maxAccessTokenCount) { + this.maxAccessTokenCount = maxAccessTokenCount; + return this; + } + + /** + * 此应用单个用户最多同时存在的 Refresh-Token 数量 + * @return / + */ + public int getMaxRefreshTokenCount() { + return maxRefreshTokenCount; + } + + /** + * 此应用单个用户最多同时存在的 Refresh-Token 数量 + * @param maxRefreshTokenCount / + * @return 对象自身 + */ + public SaClientModel setMaxRefreshTokenCount(int maxRefreshTokenCount) { + this.maxRefreshTokenCount = maxRefreshTokenCount; + return this; + } + + /** + * 此应用单个用户最多同时存在的 Client-Token 数量 + * @return / + */ + public int getMaxClientTokenCount() { + return maxClientTokenCount; + } + + /** + * 此应用单个用户最多同时存在的 Client-Token 数量 + * @param maxClientTokenCount / + * @return 对象自身 + */ + public SaClientModel setMaxClientTokenCount(int maxClientTokenCount) { + this.maxClientTokenCount = maxClientTokenCount; + return this; + } + + @Override + public String toString() { + return "SaClientModel{" + + "clientId='" + clientId + '\'' + + ", clientSecret='" + clientSecret + '\'' + + ", contractScopes=" + contractScopes + + ", allowRedirectUris=" + allowRedirectUris + + ", allowGrantTypes=" + allowGrantTypes + + ", subjectId=" + subjectId + + ", isNewRefresh=" + isNewRefresh + + ", accessTokenTimeout=" + accessTokenTimeout + + ", refreshTokenTimeout=" + refreshTokenTimeout + + ", clientTokenTimeout=" + clientTokenTimeout + + ", isAutoConfirm=" + isAutoConfirm + + ", maxAccessTokenCount=" + maxAccessTokenCount + + ", refreshTokenTimeout=" + refreshTokenTimeout + + ", maxClientTokenCount=" + maxClientTokenCount + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java new file mode 100644 index 000000000..496568764 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.model.oidc; + +import java.io.Serializable; +import java.util.Map; + +/** + * OIDC IdToken Model + * + *
    参考: + *
    IDToken + *
    StandardClaims + * + * @author click33 + * @since 1.23.0 + */ +public class IdTokenModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 必填:发行者标识符,例如:https://server.example.com + */ + public String iss; + + /** + * 必填:用户标识符,用户id,例如:10001 + */ + public Object sub; + + /** + * 必填:客户端标识符,clientId,例如:s6BhdRkqt3 + */ + public String aud; + + /** + * 必填:令牌到期时间,10位时间戳,例如:1723341795 + */ + public long exp; + + /** + * 必填:签发此令牌的时间,10位时间戳,例如:1723339995 + */ + public long iat; + + /** + * 用户认证时间,10位时间戳,例如:1723339988 + */ + public long authTime; + + /** + * 随机数,客户端提供,防止重放攻击,例如:e9a3f4d9 + */ + public String nonce; + + /** + * 身份验证上下文类引用 + */ + public String acr; + + /** + * 身份验证方法参考 + */ + public String amr; + + /** + * 授权方 - 签发 ID 令牌的一方,如果存在,它必须包含此方的 OAuth 2.0 客户端 ID。 + */ + public String azp; + + /** + * 扩展数据 + */ + public Map extraData; + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/ClientIdAndSecretModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/ClientIdAndSecretModel.java new file mode 100644 index 000000000..4d9c6eb59 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/ClientIdAndSecretModel.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.model.request; + +import java.io.Serializable; + +/** + * Client 的 id 和 secret + * + * @author click33 + * @since 1.39.0 + */ +public class ClientIdAndSecretModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 应用id + */ + public String clientId; + + /** + * 应用秘钥 + */ + public String clientSecret; + + public ClientIdAndSecretModel() { + } + public ClientIdAndSecretModel(String clientId, String clientSecret) { + super(); + this.clientId = clientId; + this.clientSecret = clientSecret; + } + + /** + * @return 应用id + */ + public String getClientId() { + return clientId; + } + + /** + * @param clientId 应用id + * @return 对象自身 + */ + public ClientIdAndSecretModel setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + /** + * @return 应用秘钥 + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * @param clientSecret 应用秘钥 + * @return 对象自身 + */ + public ClientIdAndSecretModel setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + @Override + public String toString() { + return "ClientIdAndSecretModel{" + + "clientId='" + clientId + '\'' + + ", clientSecret='" + clientSecret + '\'' + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/RequestAuthModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/RequestAuthModel.java new file mode 100644 index 000000000..029cc461f --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/RequestAuthModel.java @@ -0,0 +1,216 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.model.request; + +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.io.Serializable; +import java.util.List; + +/** + * 请求授权参数的 Model + * + * @author click33 + * @since 1.23.0 + */ +public class RequestAuthModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 应用id + */ + public String clientId; + + /** + * 授权范围 + */ + public List scopes; + + /** + * 对应的账号id + */ + public Object loginId; + + /** + * 待重定向URL + */ + public String redirectUri; + + /** + * 授权类型, 非必填 + */ + public String responseType; + + /** + * 状态标识, 可为null + */ + public String state; + + /** + * 随机数 + */ + public String nonce; + + + /** + * @return clientId + */ + public String getClientId() { + return clientId; + } + + /** + * @param clientId 要设置的 clientId + * @return 对象自身 + */ + public RequestAuthModel setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + /** + * @return scopes + */ + public List getScopes() { + return scopes; + } + + /** + * @param scopes 要设置的 scopes + * @return 对象自身 + */ + public RequestAuthModel setScopes(List scopes) { + this.scopes = scopes; + return this; + } + + /** + * @return loginId + */ + public Object getLoginId() { + return loginId; + } + + /** + * @param loginId 要设置的 loginId + * @return 对象自身 + */ + public RequestAuthModel setLoginId(Object loginId) { + this.loginId = loginId; + return this; + } + + /** + * @return redirectUri + */ + public String getRedirectUri() { + return redirectUri; + } + + /** + * @param redirectUri 要设置的 redirectUri + * @return 对象自身 + */ + public RequestAuthModel setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + return this; + } + + /** + * @return responseType + */ + public String getResponseType() { + return responseType; + } + + /** + * @param responseType 要设置的 responseType + * @return 对象自身 + */ + public RequestAuthModel setResponseType(String responseType) { + this.responseType = responseType; + return this; + } + + /** + * @return state + */ + public String getState() { + return state; + } + + /** + * @param state 要设置的 state + * @return 对象自身 + */ + public RequestAuthModel setState(String state) { + this.state = state; + return this; + } + + /** + * @return nonce + */ + public String getNonce() { + return nonce; + } + + /** + * @param nonce 要设置的随机数 + * @return 对象自身 + */ + public RequestAuthModel setNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * 数据自检 + * @return 对象自身 + */ + public RequestAuthModel checkModel() { + if(SaFoxUtil.isEmpty(clientId)) { + throw new SaOAuth2Exception("client_id 不可为空").setCode(SaOAuth2ErrorCode.CODE_30101); + } + if(SaFoxUtil.isEmpty(scopes)) { + throw new SaOAuth2Exception("scope 不可为空").setCode(SaOAuth2ErrorCode.CODE_30102); + } + if(SaFoxUtil.isEmpty(redirectUri)) { + throw new SaOAuth2Exception("redirect_uri 不可为空").setCode(SaOAuth2ErrorCode.CODE_30103); + } + if(SaFoxUtil.isEmpty(String.valueOf(loginId))) { + throw new SaOAuth2Exception("LoginId 不可为空").setCode(SaOAuth2ErrorCode.CODE_30104); + } + return this; + } + + @Override + public String toString() { + return "RequestAuthModel{" + + "clientId='" + clientId + '\'' + + ", scopes=" + scopes + + ", loginId=" + loginId + + ", redirectUri='" + redirectUri + '\'' + + ", responseType='" + responseType + '\'' + + ", state='" + state + '\'' + + ", nonce='" + nonce + '\'' + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolver.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolver.java new file mode 100644 index 000000000..49e1c68b6 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolver.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.resolver; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +import cn.dev33.satoken.util.SaResult; + +import java.util.Map; + +/** + * Sa-Token OAuth2 数据解析器,负责 Web 交互层面的数据进出: + *

    1、从请求中按照指定格式读取数据

    + *

    2、构建数据输出格式

    + * + * @author click33 + * @since 1.39.0 + */ +public interface SaOAuth2DataResolver { + + /** + * 数据读取:从请求对象中读取 ClientId、Secret + * + * @param request / + * @return / + */ + ClientIdAndSecretModel readClientIdAndSecret(SaRequest request); + + /** + * 数据读取:从请求对象中读取 AccessToken,获取不到返回 null + *
    1、请求参数 access_token,2、请求头 Authorization Bearer access_token + * + * @param request / + * @return / + */ + String readAccessToken(SaRequest request); + + /** + * 数据读取:从请求对象中读取 ClientToken,获取不到返回 null + *
    1、请求参数 client_token,2、请求头 Authorization Bearer client_token + * + * @param request / + * @return / + */ + String readClientToken(SaRequest request); + + /** + * 数据读取:从请求对象中构建 RequestAuthModel + * @param req SaRequest对象 + * @param loginId 账号id + * @return RequestAuthModel对象 + */ + RequestAuthModel readRequestAuthModel(SaRequest req, Object loginId); + + /** + * 构建返回值: 获取 token + * @param at token信息 + * @return / + */ + Map buildAccessTokenReturnValue(AccessTokenModel at); + + /** + * 构建返回值: RefreshToken 刷新 Access-Token + * @param at token信息 + * @return / + */ + default Map buildRefreshTokenReturnValue(AccessTokenModel at) { + return buildAccessTokenReturnValue(at); + } + + /** + * 构建返回值: 回收 Access-Token + * @return / + */ + default Map buildRevokeTokenReturnValue() { + return SaResult.ok(); + } + + /** + * 构建返回值: 凭证式 模式认证 获取 token + * @param ct token信息 + */ + Map buildClientTokenReturnValue(ClientTokenModel ct); + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolverDefaultImpl.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolverDefaultImpl.java new file mode 100644 index 000000000..767ea01cd --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolverDefaultImpl.java @@ -0,0 +1,201 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.data.resolver; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Param; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.TokenType; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sa-Token OAuth2 数据解析器,负责 Web 交互层面的数据进出: + *

    1、从请求中按照指定格式读取数据

    + *

    2、构建数据输出格式

    + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2DataResolverDefaultImpl implements SaOAuth2DataResolver { + + /** + * 数据读取:从请求对象中读取 ClientId、Secret,如果获取不到则抛出异常 + * + * @param request / + * @return / + */ + @Override + public ClientIdAndSecretModel readClientIdAndSecret(SaRequest request) { + // 优先从请求参数中获取 + String clientId = request.getParam(SaOAuth2Consts.Param.client_id); + String clientSecret = request.getParam(SaOAuth2Consts.Param.client_secret); + + // 此处必须 clientId 和 clientSecret 都有值才可以采用,fix pr: https://gitee.com/dromara/sa-token/pulls/346 + if(SaFoxUtil.isNotEmpty(clientId) && SaFoxUtil.isNotEmpty(clientSecret)) { + return new ClientIdAndSecretModel(clientId, clientSecret); + } + + // 如果请求参数中没有提供 client_id 参数,则尝试从 Authorization 中获取 + String authorizationValue = SaHttpBasicUtil.getAuthorizationValue(); + if(SaFoxUtil.isNotEmpty(authorizationValue)) { + String[] arr = authorizationValue.split(":"); + clientId = arr[0]; + if(arr.length > 1) { + clientSecret = arr[1]; + } + return new ClientIdAndSecretModel(clientId, clientSecret); + } + + // 如果只提供了 clientId 参数,也为其构建一个 ClientIdAndSecretModel 对象,clientSecret 置空 + if(SaFoxUtil.isNotEmpty(clientId)) { + return new ClientIdAndSecretModel(clientId, null); + } + + // 如果都没有提供,则抛出异常 + throw new SaOAuth2Exception("请提供 client 信息").setCode(SaOAuth2ErrorCode.CODE_30191); + } + + /** + * 数据读取:从请求对象中读取 AccessToken,获取不到返回 null,获取不到返回 null + *
    1、请求参数 access_token,2、请求头 Authorization Bearer access_token + */ + @Override + public String readAccessToken(SaRequest request) { + // 优先从请求参数中获取,可以读取到的话直接返回 + String accessToken = request.getParam(Param.access_token); + if(SaFoxUtil.isNotEmpty(accessToken)) { + return accessToken; + } + + // 如果请求参数中没有提供 access_token 参数,则尝试从 Authorization 中获取 + String authorizationValue = request.getHeader(Param.Authorization); + if(SaFoxUtil.isEmpty(authorizationValue)) { + return null; + } + + // 判断前缀,裁剪 + String prefix = TokenType.Bearer + " "; + if(authorizationValue.startsWith(prefix)) { + return authorizationValue.substring(prefix.length()); + } + + // 前缀不符合,返回 null + return null; + } + + /** + * 数据读取:从请求对象中读取 ClientToken,获取不到返回 null + *
    1、请求参数 client_token,2、请求头 Authorization Bearer client_token + */ + @Override + public String readClientToken(SaRequest request) { + // 优先从请求参数中获取,可以读取到的话直接返回 + String clientToken = request.getParam(Param.client_token); + if(SaFoxUtil.isNotEmpty(clientToken)) { + return clientToken; + } + + // 如果请求参数中没有提供 client_token 参数,则尝试从 Authorization 中获取 + String authorizationValue = request.getHeader(Param.Authorization); + if(SaFoxUtil.isEmpty(authorizationValue)) { + return null; + } + + // 判断前缀,裁剪 + String prefix = TokenType.Bearer + " "; + if(authorizationValue.startsWith(prefix)) { + return authorizationValue.substring(prefix.length()); + } + + // 前缀不符合,返回 null + return null; + } + + /** + * 数据读取:从请求对象中构建 RequestAuthModel + */ + @Override + public RequestAuthModel readRequestAuthModel(SaRequest req, Object loginId) { + RequestAuthModel ra = new RequestAuthModel(); + ra.clientId = req.getParamNotNull(Param.client_id); + ra.responseType = req.getParamNotNull(Param.response_type); + ra.redirectUri = req.getParamNotNull(Param.redirect_uri); + ra.state = req.getParam(Param.state); + ra.nonce = req.getParam(Param.nonce); + ra.scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(Param.scope)); + ra.loginId = loginId; + return ra; + } + + + /** + * 构建返回值: 获取 token + */ + @Override + public Map buildAccessTokenReturnValue(AccessTokenModel at) { + Map map = new LinkedHashMap<>(); + map.put("token_type", at.tokenType); + map.put("access_token", at.accessToken); + map.put("refresh_token", at.refreshToken); + map.put("expires_in", at.getExpiresIn()); + map.put("refresh_expires_in", at.getRefreshExpiresIn()); + map.put("client_id", at.clientId); + map.put("scope", SaOAuth2Manager.getDataConverter().convertScopeListToString(at.scopes)); + map.putAll(at.extraData); + SaResult result = SaResult.ok().setMap(map); + if(SaOAuth2Manager.getServerConfig().hideStatusField) { + result.removeDefaultFields(); + } + return result; + } + + /** + * 构建返回值: 凭证式 模式认证 获取 token + */ + @Override + public Map buildClientTokenReturnValue(ClientTokenModel ct) { + Map map = new LinkedHashMap<>(); + map.put("token_type", ct.tokenType); + map.put("client_token", ct.clientToken); + if(SaOAuth2Manager.getServerConfig().getMode4ReturnAccessToken()) { + map.put("access_token", ct.clientToken); + } + map.put("expires_in", ct.getExpiresIn()); + map.put("client_id", ct.clientId); + map.put("scope", SaOAuth2Manager.getDataConverter().convertScopeListToString(ct.scopes)); + map.putAll(ct.extraData); + + SaResult result = SaResult.ok().setMap(map); + if(SaOAuth2Manager.getServerConfig().hideStatusField) { + result.removeDefaultFields(); + } + return result; + } + +} + diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java new file mode 100644 index 000000000..8a19c5f57 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java @@ -0,0 +1,113 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.error; + +/** + * 定义 sa-token-oauth2 所有异常细分状态码 + * + * @author click33 + * @since 1.33.0 + */ +public interface SaOAuth2ErrorCode { + + /** client_id 不可为空 */ + int CODE_30101 = 30101; + + /** scope 不可为空 */ + int CODE_30102 = 30102; + + /** redirect_uri 不可为空 */ + int CODE_30103 = 30103; + + /** LoginId 不可为空 */ + int CODE_30104 = 30104; + + /** 无效 client_id */ + int CODE_30105 = 30105; + + /** 无效 access_token */ + int CODE_30106 = 30106; + + /** 无效 client_token */ + int CODE_30107 = 30107; + + /** Access-Token 不具备指定的 Scope */ + int CODE_30108 = 30108; + + /** Client-Token 不具备指定的 Scope */ + int CODE_30109 = 30109; + + /** 无效 Code 码 */ + int CODE_30110 = 30110; + + /** 无效 Refresh-Token */ + int CODE_30111 = 30111; + + /** 请求的 Scope 暂未签约 */ + int CODE_30112 = 30112; + + /** 无效 redirect_url */ + int CODE_30113 = 30113; + + /** 非法 redirect_url */ + int CODE_30114 = 30114; + + /** 无效client_secret */ + int CODE_30115 = 30115; + + /** redirect_uri 不一致 */ + int CODE_30120 = 30120; + + /** client_id 不一致 */ + int CODE_30122 = 30122; + + /** 无效 response_type */ + int CODE_30125 = 30125; + + /** 无效 grant_type */ + int CODE_30126 = 30126; + + /** 无效 state */ + int CODE_30127 = 30127; + + /** 暂未开放授权码模式 */ + int CODE_30131 = 30131; + + /** 暂未开放隐藏式模式 */ + int CODE_30132 = 30132; + + /** 暂未开放密码式模式 */ + int CODE_30133 = 30133; + + /** 暂未开放凭证式模式 */ + int CODE_30134 = 30134; + + /** 系统暂未开放的授权模式 */ + int CODE_30141 = 30141; + + /** 应用暂未开放的授权模式 */ + int CODE_30142 = 30142; + + /** 无效的请求 Method */ + int CODE_30151 = 30151; + + /** Password 模式认证失败 */ + int CODE_30161 = 30161; + + /** 其它异常 */ + int CODE_30191 = 30191; + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenException.java new file mode 100644 index 000000000..76a8e75ab --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenException.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; + +/** + * 一个异常:代表 Access-Token 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2AccessTokenException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Access-Token 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2AccessTokenException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Access-Token 相关错误 + * @param message 异常描述 + */ + public SaOAuth2AccessTokenException(String message) { + super(message); + } + + /** + * 具体引起异常的 Access-Token 值 + */ + public String accessToken; + + public String getAccessToken() { + return accessToken; + } + + public SaOAuth2AccessTokenException setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2AccessTokenException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenScopeException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenScopeException.java new file mode 100644 index 000000000..41821f1c1 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenScopeException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; + +/** + * 一个异常:代表 Access-Token Scope 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2AccessTokenScopeException extends SaOAuth2AccessTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Access-Token Scope 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2AccessTokenScopeException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Access-Token Scope 相关错误 + * @param message 异常描述 + */ + public SaOAuth2AccessTokenScopeException(String message) { + super(message); + } + + /** + * 具体引起异常的 Access-Token 值 + */ + public String accessToken; + + /** + * 具体引起异常的 scope 值 + */ + public String scope; + + public String getAccessToken() { + return accessToken; + } + + public SaOAuth2AccessTokenScopeException setAccessToken(String accessToken) { + this.accessToken = accessToken; + return this; + } + + public String getScope() { + return scope; + } + + public SaOAuth2AccessTokenScopeException setScope(String scope) { + this.scope = scope; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2AccessTokenScopeException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AuthorizationCodeException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AuthorizationCodeException.java new file mode 100644 index 000000000..dd4f2b27d --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AuthorizationCodeException.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; + +/** + * 一个异常:代表 Code 授权码相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2AuthorizationCodeException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Access-Token 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2AuthorizationCodeException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Access-Token 相关错误 + * @param message 异常描述 + */ + public SaOAuth2AuthorizationCodeException(String message) { + super(message); + } + + /** + * 具体引起异常的 code 值 + */ + public String authorizationCode; + + public String getAuthorizationCode() { + return authorizationCode; + } + + public SaOAuth2AuthorizationCodeException setAuthorizationCode(String authorizationCode) { + this.authorizationCode = authorizationCode; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param authorizationCode 引入异常的 code 值 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, String authorizationCode, int code) { + if(flag) { + throw new SaOAuth2AuthorizationCodeException(message).setAuthorizationCode(authorizationCode).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelException.java new file mode 100644 index 000000000..c89de422c --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelException.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; + +/** + * 一个异常:代表 ClientModel 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2ClientModelException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 ClientModel 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2ClientModelException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 ClientModel 相关错误 + * @param message 异常描述 + */ + public SaOAuth2ClientModelException(String message) { + super(message); + } + + /** + * 具体引起异常的 ClientId 值 + */ + public String clientId; + + public String getClientId() { + return clientId; + } + + public SaOAuth2ClientModelException setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2ClientModelException(message).setCode(code); + } + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param clientId 应用id + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, String clientId, int code) { + if(flag) { + throw new SaOAuth2ClientModelException(message).setClientId(clientId).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelScopeException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelScopeException.java new file mode 100644 index 000000000..fd9265d3a --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelScopeException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; + +/** + * 一个异常:代表 ClientModel Scope 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2ClientModelScopeException extends SaOAuth2ClientModelException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 ClientModel Scope 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2ClientModelScopeException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 ClientModel Scope 相关错误 + * @param message 异常描述 + */ + public SaOAuth2ClientModelScopeException(String message) { + super(message); + } + + /** + * 具体引起异常的 ClientId 值 + */ + public String clientId; + + /** + * 具体引起异常的 scope 值 + */ + public String scope; + + public String getClientId() { + return clientId; + } + + public SaOAuth2ClientModelScopeException setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + public String getScope() { + return scope; + } + + public SaOAuth2ClientModelScopeException setScope(String scope) { + this.scope = scope; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2ClientModelScopeException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenException.java new file mode 100644 index 000000000..ca5bd1b0a --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenException.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; + +/** + * 一个异常:代表 Client-Token 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2ClientTokenException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Client-Token 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2ClientTokenException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Client-Token 相关错误 + * @param message 异常描述 + */ + public SaOAuth2ClientTokenException(String message) { + super(message); + } + + /** + * 具体引起异常的 Client-Token 值 + */ + public String clientToken; + + public String getClientToken() { + return clientToken; + } + + public SaOAuth2ClientTokenException setClientToken(String clientToken) { + this.clientToken = clientToken; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2ClientTokenException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenScopeException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenScopeException.java new file mode 100644 index 000000000..75929ab26 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenScopeException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; + +/** + * 一个异常:代表 Client-Token Scope 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2ClientTokenScopeException extends SaOAuth2ClientTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Client-Token Scope 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2ClientTokenScopeException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Client-Token Scope 相关错误 + * @param message 异常描述 + */ + public SaOAuth2ClientTokenScopeException(String message) { + super(message); + } + + /** + * 具体引起异常的 Client-Token 值 + */ + public String clientToken; + + /** + * 具体引起异常的 scope 值 + */ + public String scope; + + public String getClientToken() { + return clientToken; + } + + public SaOAuth2ClientTokenScopeException setClientToken(String clientToken) { + this.clientToken = clientToken; + return this; + } + + public String getScope() { + return scope; + } + + public SaOAuth2ClientTokenScopeException setScope(String scope) { + this.scope = scope; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2ClientTokenScopeException(message).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java index a6eb2d1f8..e079af6a8 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java @@ -1,11 +1,27 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; import cn.dev33.satoken.exception.SaTokenException; /** - * 一个异常:代表OAuth2认证流程错误 + * 一个异常:代表 OAuth2 认证流程错误 * - * @author kong + * @author click33 + * @since 1.33.0 */ public class SaOAuth2Exception extends SaTokenException { @@ -13,9 +29,17 @@ public class SaOAuth2Exception extends SaTokenException { * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; - + + /** + * 一个异常:代表 OAuth2 认证流程错误 + * @param cause 根异常原因 + */ + public SaOAuth2Exception(Throwable cause) { + super(cause); + } + /** - * 一个异常:代表OAuth2认证流程错误 + * 一个异常:代表 OAuth2 认证流程错误 * @param message 异常描述 */ public SaOAuth2Exception(String message) { @@ -23,13 +47,14 @@ public SaOAuth2Exception(String message) { } /** - * 如果flag==true,则抛出message异常 + * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 + * @param code 异常细分码 */ - public static void throwBy(boolean flag, String message) { + public static void throwBy(boolean flag, String message, int code) { if(flag) { - throw new SaOAuth2Exception(message); + throw new SaOAuth2Exception(message).setCode(code); } } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2RefreshTokenException.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2RefreshTokenException.java new file mode 100644 index 000000000..f1e3c691a --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2RefreshTokenException.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.exception; + +/** + * 一个异常:代表 Refresh-Token 相关错误 + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2RefreshTokenException extends SaOAuth2Exception { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 Refresh-Token 相关错误 + * @param cause 根异常原因 + */ + public SaOAuth2RefreshTokenException(Throwable cause) { + super(cause); + } + + /** + * 一个异常:代表 Refresh-Token 相关错误 + * @param message 异常描述 + */ + public SaOAuth2RefreshTokenException(String message) { + super(message); + } + + /** + * 具体引起异常的 Refresh-Token 值 + */ + public String refreshToken; + + public String getRefreshToken() { + return refreshToken; + } + + public SaOAuth2RefreshTokenException setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, int code) { + if(flag) { + throw new SaOAuth2RefreshTokenException(message).setCode(code); + } + } + + /** + * 如果 flag==true,则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param refreshToken refreshToken + * @param code 异常细分码 + */ + public static void throwBy(boolean flag, String message, String refreshToken, int code) { + if(flag) { + throw new SaOAuth2RefreshTokenException(message).setRefreshToken(refreshToken).setCode(code); + } + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2ConfirmViewFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2ConfirmViewFunction.java new file mode 100644 index 000000000..1d7b42b8d --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2ConfirmViewFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function; + + +import java.util.List; +import java.util.function.BiFunction; + +/** + * 函数式接口:OAuth-Server端 确认授权时返回的View + * + *

    参数:无

    + *

    返回:view 视图

    + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2ConfirmViewFunction extends BiFunction, Object> { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2DoLoginHandleFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2DoLoginHandleFunction.java new file mode 100644 index 000000000..ba247de47 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2DoLoginHandleFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function; + + +import java.util.function.BiFunction; + +/** + * 函数式接口:登录函数 + * + *

    参数:name, pwd

    + *

    返回:认证返回结果

    + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2DoLoginHandleFunction extends BiFunction { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2NotLoginViewFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2NotLoginViewFunction.java new file mode 100644 index 000000000..2f30b9f65 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2NotLoginViewFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function; + + +import java.util.function.Supplier; + +/** + * 函数式接口:OAuth-Server端 未登录时返回的View + * + *

    参数:clientId, scope

    + *

    返回:view 视图

    + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2NotLoginViewFunction extends Supplier { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateAccessTokenValueFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateAccessTokenValueFunction.java new file mode 100644 index 000000000..8a2c8f825 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateAccessTokenValueFunction.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function.strategy; + +import java.util.List; + +/** + * 函数式接口:创建一个 AccessToken value + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2CreateAccessTokenValueFunction { + + /** + * 创建一个 AccessToken value + * @param clientId 应用id + * @param loginId 账号id + * @param scopes 权限 + * @return AccessToken value + */ + String execute(String clientId, Object loginId, List scopes); + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateClientTokenValueFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateClientTokenValueFunction.java new file mode 100644 index 000000000..14a97270f --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateClientTokenValueFunction.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function.strategy; + +import java.util.List; + +/** + * 函数式接口:创建一个 ClientToken value + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2CreateClientTokenValueFunction { + + /** + * 创建一个 ClientToken value + * @param clientId 应用id + * @param scopes 权限 + * @return ClientToken value + */ + String execute(String clientId, List scopes); + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateCodeValueFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateCodeValueFunction.java new file mode 100644 index 000000000..befca0213 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateCodeValueFunction.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function.strategy; + +import java.util.List; + +/** + * 函数式接口:创建一个 code value + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2CreateCodeValueFunction { + + /** + * 创建一个 code value + * @param clientId 应用id + * @param loginId 账号id + * @param scopes 权限 + * @return code value + */ + String execute(String clientId, Object loginId, List scopes); + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateRefreshTokenValueFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateRefreshTokenValueFunction.java new file mode 100644 index 000000000..e7a86387d --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateRefreshTokenValueFunction.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function.strategy; + +import java.util.List; + +/** + * 函数式接口:创建一个 RefreshToken value + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2CreateRefreshTokenValueFunction { + + /** + * 创建一个 RefreshToken value + * @param clientId 应用id + * @param loginId 账号id + * @param scopes 权限 + * @return RefreshToken value + */ + String execute(String clientId, Object loginId, List scopes); + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2GrantTypeAuthFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2GrantTypeAuthFunction.java new file mode 100644 index 000000000..78fcea16e --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2GrantTypeAuthFunction.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function.strategy; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; + +import java.util.function.Function; + +/** + * 函数式接口:GrantType 认证 + * + *

    参数:SaRequest、grant_type

    + *

    返回:处理结果

    + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2GrantTypeAuthFunction extends Function { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkAccessTokenFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkAccessTokenFunction.java new file mode 100644 index 000000000..d612c3295 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkAccessTokenFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function.strategy; + +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; + +import java.util.function.Consumer; + +/** + * 函数式接口:AccessTokenModel 加工 + * + *

    参数:AccessTokenModel

    + *

    返回:无

    + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2ScopeWorkAccessTokenFunction extends Consumer { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkClientTokenFunction.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkClientTokenFunction.java new file mode 100644 index 000000000..f849915f7 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkClientTokenFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.function.strategy; + +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; + +import java.util.function.Consumer; + +/** + * 函数式接口:ClientTokenModel 加工 + * + *

    参数:ClientTokenModel

    + *

    返回:无

    + * + * @author click33 + * @since 1.39.0 + */ +@FunctionalInterface +public interface SaOAuth2ScopeWorkClientTokenFunction extends Consumer { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/AuthorizationCodeGrantTypeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/AuthorizationCodeGrantTypeHandler.java new file mode 100644 index 000000000..422100ce8 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/AuthorizationCodeGrantTypeHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.granttype.handler; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.consts.GrantType; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; + +import java.util.List; + +/** + * authorization_code grant_type 处理器 + * + * @author click33 + * @since 1.39.0 + */ +public class AuthorizationCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { + + @Override + public String getHandlerGrantType() { + return GrantType.authorization_code; + } + + @Override + public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { + ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); + String clientSecret = clientIdAndSecret.clientSecret; + String code = req.getParamNotNull(SaOAuth2Consts.Param.code); + String redirectUri = req.getParam(SaOAuth2Consts.Param.redirect_uri); + + // 校验参数 + SaOAuth2Manager.getTemplate().checkGainTokenParam(code, clientId, clientSecret, redirectUri); + + // 构建 Access-Token、返回 + return SaOAuth2Manager.getDataGenerate().generateAccessToken(code); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java new file mode 100644 index 000000000..1e84a1c4a --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.granttype.handler; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.consts.GrantType; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.oauth2.granttype.handler.model.PasswordAuthResult; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +import cn.dev33.satoken.stp.StpUtil; + +import java.util.List; + +/** + * password grant_type 处理器 + * + * @author click33 + * @since 1.39.0 + */ +public class PasswordGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { + + @Override + public String getHandlerGrantType() { + return GrantType.password; + } + + @Override + public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { + + // 1、获取请求参数 + String username = req.getParamNotNull(SaOAuth2Consts.Param.username); + String password = req.getParamNotNull(SaOAuth2Consts.Param.password); + + // 2、调用API 开始登录,如果没能成功登录,则直接退出 + PasswordAuthResult passwordAuthResult = loginByUsernamePassword(username, password); + Object loginId = passwordAuthResult.getLoginId(); + if(loginId == null) { + throw new SaOAuth2Exception("登录失败").setCode(SaOAuth2ErrorCode.CODE_30161); + } + + // 3、构建 ra 对象 + RequestAuthModel ra = new RequestAuthModel(); + ra.clientId = clientId; + ra.loginId = loginId; + ra.scopes = scopes; + + // 4、生成 Access-Token + AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = GrantType.password); + return at; + } + + /** + * 根据 username、password 进行登录,如果登录失败请直接抛出异常或返回 loginId = null + * @param username / + * @param password / + */ + public PasswordAuthResult loginByUsernamePassword(String username, String password) { + System.err.println("警告信息:当前 password 认证模式,使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle),仅供开发测试"); + System.err.println("正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法"); + SaOAuth2Strategy.instance.doLoginHandle.apply(username, password); + Object loginId = StpUtil.getLoginIdDefaultNull(); + return new PasswordAuthResult(loginId); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/RefreshTokenGrantTypeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/RefreshTokenGrantTypeHandler.java new file mode 100644 index 000000000..c43b00d18 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/RefreshTokenGrantTypeHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.granttype.handler; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.consts.GrantType; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2ClientModelException; +import cn.dev33.satoken.oauth2.exception.SaOAuth2RefreshTokenException; + +import java.util.List; + +/** + * refresh_token grant_type 处理器 + * + * @author click33 + * @since 1.39.0 + */ +public class RefreshTokenGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { + + @Override + public String getHandlerGrantType() { + return GrantType.refresh_token; + } + + @Override + public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { + // 获取参数 + String refreshToken = req.getParamNotNull(SaOAuth2Consts.Param.refresh_token); + + // 校验:Refresh-Token 是否存在 + RefreshTokenModel rt = SaOAuth2Manager.getDao().getRefreshToken(refreshToken); + SaOAuth2RefreshTokenException.throwBy(rt == null, "无效refresh_token: " + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111); + + // 校验:Refresh-Token 代表的 ClientId 与提供的 ClientId 是否一致 + SaOAuth2ClientModelException.throwBy( ! rt.clientId.equals(clientId), "无效client_id: " + clientId, clientId, SaOAuth2ErrorCode.CODE_30122); + + // 获取新 Access-Token + AccessTokenModel accessTokenModel = SaOAuth2Manager.getDataGenerate().refreshAccessToken(refreshToken); + return accessTokenModel; + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/SaOAuth2GrantTypeHandlerInterface.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/SaOAuth2GrantTypeHandlerInterface.java new file mode 100644 index 000000000..d0da6ec3b --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/SaOAuth2GrantTypeHandlerInterface.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.granttype.handler; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; + +import java.util.List; + +/** + * 所有 OAuth2 GrantType 处理器的父接口,如果要自定义 GrantType 处理器,必须实现此接口 + * + * @author click33 + * @since 1.39.0 + */ +public interface SaOAuth2GrantTypeHandlerInterface { + + /** + * 获取所要处理的 GrantType + * + * @return / + */ + String getHandlerGrantType(); + + /** + * 获取 AccessTokenModel 对象 + * + * @param req / + * @return / + */ + AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes); + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/model/PasswordAuthResult.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/model/PasswordAuthResult.java new file mode 100644 index 000000000..c49fe2595 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/model/PasswordAuthResult.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.granttype.handler.model; + +import java.io.Serializable; + +/** + * Model: Password Grant_Type 认证结果 + * + * @author click33 + * @since 1.43.0 + */ +public class PasswordAuthResult implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 对应账号id + */ + public Object loginId; + + /** + * 构建一个 + */ + public PasswordAuthResult() { + + } + /** + * 构建一个 + * @param loginId 对应的账号id + */ + public PasswordAuthResult(Object loginId) { + this(); + this.loginId = loginId; + } + + /** + * 获取 对应账号id + * @return / + */ + public Object getLoginId() { + return loginId; + } + + /** + * 设置 对应账号id + * @param loginId 对应账号id + * @return 对象自身 + */ + public PasswordAuthResult setLoginId(Object loginId) { + this.loginId = loginId; + return this; + } + + @Override + public String toString() { + return "PasswordAuthResult{" + + ", loginId=" + loginId + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Consts.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Consts.java deleted file mode 100644 index afd8108e7..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Consts.java +++ /dev/null @@ -1,70 +0,0 @@ -package cn.dev33.satoken.oauth2.logic; - -/** - * Sa-Token-OAuth2 所有常量 - * @author kong - * - */ -public class SaOAuth2Consts { - - /** - * 所有API接口 - * @author kong - */ - public static final class Api { - public static String authorize = "/oauth2/authorize"; - public static String token = "/oauth2/token"; - public static String refresh = "/oauth2/refresh"; - public static String revoke = "/oauth2/revoke"; - public static String client_token = "/oauth2/client_token"; - public static String doLogin = "/oauth2/doLogin"; - public static String doConfirm = "/oauth2/doConfirm"; - } - - /** - * 所有参数名称 - * @author kong - */ - public static final class Param { - public static String response_type = "response_type"; - public static String client_id = "client_id"; - public static String client_secret = "client_secret"; - public static String redirect_uri = "redirect_uri"; - public static String scope = "scope"; - public static String state = "state"; - public static String code = "code"; - public static String token = "token"; - public static String access_token = "access_token"; - public static String refresh_token = "refresh_token"; - public static String grant_type = "grant_type"; - public static String username = "username"; - public static String password = "password"; - public static String name = "name"; - public static String pwd = "pwd"; - } - - /** - * 所有返回类型 - */ - public static final class ResponseType { - public static String code = "code"; - public static String token = "token"; - } - - /** - * 所有授权类型 - */ - public static final class GrantType { - public static String authorization_code = "authorization_code"; - public static String refresh_token = "refresh_token"; - public static String password = "password"; - public static String client_credentials = "client_credentials"; - } - - /** 表示OK的返回结果 */ - public static final String OK = "ok"; - - /** 表示请求没有得到任何有效处理 {msg: "not handle"} */ - public static final String NOT_HANDLE = "{\"msg\": \"not handle\"}"; - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Handle.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Handle.java deleted file mode 100644 index fda31bc05..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Handle.java +++ /dev/null @@ -1,290 +0,0 @@ -package cn.dev33.satoken.oauth2.logic; - -import cn.dev33.satoken.context.SaHolder; -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.context.model.SaResponse; -import cn.dev33.satoken.oauth2.SaOAuth2Manager; -import cn.dev33.satoken.oauth2.config.SaOAuth2Config; -import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.Api; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.GrantType; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.Param; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.ResponseType; -import cn.dev33.satoken.oauth2.model.AccessTokenModel; -import cn.dev33.satoken.oauth2.model.ClientTokenModel; -import cn.dev33.satoken.oauth2.model.CodeModel; -import cn.dev33.satoken.oauth2.model.RequestAuthModel; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaResult; - -/** - * Sa-Token-OAuth2 请求处理类封装 - * @author kong - * - */ -public class SaOAuth2Handle { - - /** - * 处理Server端请求, 路由分发 - * @return 处理结果 - */ - public static Object serverRequest() { - - // 获取变量 - SaRequest req = SaHolder.getRequest(); - SaResponse res = SaHolder.getResponse(); - SaOAuth2Config cfg = SaOAuth2Manager.getConfig(); - - // ------------------ 路由分发 ------------------ - - // 模式一:Code授权码 - if(req.isPath(Api.authorize) && req.isParam(Param.response_type, ResponseType.code) && cfg.isCode) { - return authorize(req, res, cfg); - } - - // Code授权码 获取 Access-Token - if(req.isPath(Api.token) && req.isParam(Param.grant_type, GrantType.authorization_code)) { - return token(req, res, cfg); - } - - // Refresh-Token 刷新 Access-Token - if(req.isPath(Api.refresh) && req.isParam(Param.grant_type, GrantType.refresh_token)) { - return refreshToken(req); - } - - // 回收 Access-Token - if(req.isPath(Api.revoke)) { - return revokeToken(req); - } - - // doLogin 登录接口 - if(req.isPath(Api.doLogin)) { - return doLogin(req, res, cfg); - } - - // doConfirm 确认授权接口 - if(req.isPath(Api.doConfirm)) { - return doConfirm(req); - } - - // 模式二:隐藏式 - if(req.isPath(Api.authorize) && req.isParam(Param.response_type, ResponseType.token) && cfg.isImplicit) { - return authorize(req, res, cfg); - } - - // 模式三:密码式 - if(req.isPath(Api.token) && req.isParam(Param.grant_type, GrantType.password) && cfg.isPassword) { - return password(req, res, cfg); - } - - // 模式四:凭证式 - if(req.isPath(Api.client_token) && req.isParam(Param.grant_type, GrantType.client_credentials) && cfg.isClient) { - return clientToken(req, res, cfg); - } - - // 默认返回 - return SaOAuth2Consts.NOT_HANDLE; - } - - /** - * 模式一:Code授权码 / 模式二:隐藏式 - * @param req 请求对象 - * @param res 响应对象 - * @param cfg 配置对象 - * @return 处理结果 - */ - public static Object authorize(SaRequest req, SaResponse res, SaOAuth2Config cfg) { - - // 1、如果尚未登录, 则先去登录 - if(StpUtil.isLogin() == false) { - return cfg.notLoginView.get(); - } - - // 2、构建请求Model - RequestAuthModel ra = SaOAuth2Util.generateRequestAuth(req, StpUtil.getLoginId()); - - // 3、校验:重定向域名是否合法 - SaOAuth2Util.checkRightUrl(ra.clientId, ra.redirectUri); - - // 4、校验:此次申请的Scope,该Client是否已经签约 - SaOAuth2Util.checkContract(ra.clientId, ra.scope); - - // 5、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面 - boolean isGrant = SaOAuth2Util.isGrant(ra.loginId, ra.clientId, ra.scope); - if(isGrant == false) { - return cfg.confirmView.apply(ra.clientId, ra.scope); - } - - // 6、判断授权类型 - // 如果是 授权码式,则:开始重定向授权,下放code - if(ResponseType.code.equals(ra.responseType)) { - CodeModel codeModel = SaOAuth2Util.generateCode(ra); - String redirectUri = SaOAuth2Util.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); - return res.redirect(redirectUri); - } - // 如果是 隐藏式,则:开始重定向授权,下放 token - if(ResponseType.token.equals(ra.responseType)) { - AccessTokenModel at = SaOAuth2Util.generateAccessToken(ra, false); - String redirectUri = SaOAuth2Util.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); - return res.redirect(redirectUri); - } - - // 默认返回 - throw new SaOAuth2Exception("无效response_type: " + ra.responseType); - } - - /** - * Code授权码 获取 Access-Token - * @param req 请求对象 - * @param res 响应对象 - * @param cfg 配置对象 - * @return 处理结果 - */ - public static Object token(SaRequest req, SaResponse res, SaOAuth2Config cfg) { - // 获取参数 - String code = req.getParamNotNull(Param.code); - String clientId = req.getParamNotNull(Param.client_id); - String clientSecret = req.getParamNotNull(Param.client_secret); - String redirectUri = req.getParam(Param.redirect_uri); - - // 校验参数 - SaOAuth2Util.checkGainTokenParam(code, clientId, clientSecret, redirectUri); - - // 构建 Access-Token - AccessTokenModel token = SaOAuth2Util.generateAccessToken(code); - - // 返回 - return SaResult.data(token.toLineMap()); - } - - /** - * Refresh-Token 刷新 Access-Token - * @param req 请求对象 - * @return 处理结果 - */ - public static Object refreshToken(SaRequest req) { - // 获取参数 - String clientId = req.getParamNotNull(Param.client_id); - String clientSecret = req.getParamNotNull(Param.client_secret); - String refreshToken = req.getParamNotNull(Param.refresh_token); - - // 校验参数 - SaOAuth2Util.checkRefreshTokenParam(clientId, clientSecret, refreshToken); - - // 获取新Token返回 - Object data = SaOAuth2Util.refreshAccessToken(refreshToken).toLineMap(); - return SaResult.data(data); - } - - /** - * 回收 Access-Token - * @param req 请求对象 - * @return 处理结果 - */ - public static Object revokeToken(SaRequest req) { - // 获取参数 - String clientId = req.getParamNotNull(Param.client_id); - String clientSecret = req.getParamNotNull(Param.client_secret); - String accessToken = req.getParamNotNull(Param.access_token); - - // 如果 Access-Token 不存在,直接返回 - if(SaOAuth2Util.getAccessToken(accessToken) == null) { - return SaResult.ok("access_token不存在:" + accessToken); - } - - // 校验参数 - SaOAuth2Util.checkAccessTokenParam(clientId, clientSecret, accessToken); - - // 获取新Token返回 - SaOAuth2Util.revokeAccessToken(accessToken); - return SaResult.ok(); - } - - /** - * doLogin 登录接口 - * @param req 请求对象 - * @param res 响应对象 - * @param cfg 配置对象 - * @return 处理结果 - */ - public static Object doLogin(SaRequest req, SaResponse res, SaOAuth2Config cfg) { - return cfg.doLoginHandle.apply(req.getParamNotNull(Param.name), req.getParamNotNull("pwd")); - } - - /** - * doConfirm 确认授权接口 - * @param req 请求对象 - * @return 处理结果 - */ - public static Object doConfirm(SaRequest req) { - String clientId = req.getParamNotNull(Param.client_id); - String scope = req.getParamNotNull(Param.scope); - Object loginId = StpUtil.getLoginId(); - SaOAuth2Util.saveGrantScope(clientId, loginId, scope); - return SaResult.ok(); - } - - /** - * 模式三:密码式 - * @param req 请求对象 - * @param res 响应对象 - * @param cfg 配置对象 - * @return 处理结果 - */ - public static Object password(SaRequest req, SaResponse res, SaOAuth2Config cfg) { - - // 1、获取请求参数 - String username = req.getParamNotNull(Param.username); - String password = req.getParamNotNull(Param.password); - String clientId = req.getParamNotNull(Param.client_id); - - // 2、校验client_id - SaOAuth2Util.checkClientModel(clientId); - - // 3、防止因前端误传token造成逻辑干扰 - SaHolder.getStorage().set(StpUtil.stpLogic.splicingKeyJustCreatedSave(), "no-token"); - - // 4、调用API 开始登录,如果没能成功登录,则直接退出 - Object retObj = cfg.doLoginHandle.apply(username, password); - if(StpUtil.isLogin() == false) { - return retObj; - } - - // 5、构建 ra对象 - RequestAuthModel ra = new RequestAuthModel(); - ra.clientId = clientId; - ra.loginId = StpUtil.getLoginId(); - ra.scope = req.getParam(Param.scope, ""); - - // 6、生成 Access-Token - AccessTokenModel at = SaOAuth2Util.generateAccessToken(ra, true); - - // 7、返回 Access-Token - return SaResult.data(at.toLineMap()); - } - - /** - * 模式四:凭证式 - * @param req 请求对象 - * @param res 响应对象 - * @param cfg 配置对象 - * @return 处理结果 - */ - public static Object clientToken(SaRequest req, SaResponse res, SaOAuth2Config cfg) { - - // 获取参数 - String clientId = req.getParamNotNull(Param.client_id); - String clientSecret = req.getParamNotNull(Param.client_secret); - String scope = req.getParam(Param.scope); - - // 校验 ClientSecret - SaOAuth2Util.checkClientSecret(clientId, clientSecret); - - // 返回 Client-Token - ClientTokenModel ct = SaOAuth2Util.generateClientToken(clientId, scope); - - // 返回 Client-Token - return SaResult.data(ct.toLineMap()); - } - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Template.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Template.java deleted file mode 100644 index ea93e6343..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Template.java +++ /dev/null @@ -1,924 +0,0 @@ -package cn.dev33.satoken.oauth2.logic; - -import java.util.List; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.oauth2.SaOAuth2Manager; -import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Consts.Param; -import cn.dev33.satoken.oauth2.model.AccessTokenModel; -import cn.dev33.satoken.oauth2.model.ClientTokenModel; -import cn.dev33.satoken.oauth2.model.CodeModel; -import cn.dev33.satoken.oauth2.model.RefreshTokenModel; -import cn.dev33.satoken.oauth2.model.RequestAuthModel; -import cn.dev33.satoken.oauth2.model.SaClientModel; -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * Sa-Token-OAuth2 模块 代码实现 - * @author kong - * - */ -public class SaOAuth2Template { - - // ------------------- 获取数据 (开发者必须重写的函数) - /** - * 根据id获取Client信息 - * @param clientId 应用id - * @return ClientModel - */ - public SaClientModel getClientModel(String clientId) { - return null; - } - /** - * 根据ClientId 和 LoginId 获取openid - * @param clientId 应用id - * @param loginId 账号id - * @return 此账号在此Client下的openid - */ - public String getOpenid(String clientId, Object loginId) { - return null; - } - - // ------------------- 资源校验API - /** - * 根据id获取Client信息, 如果Client为空,则抛出异常 - * @param clientId 应用id - * @return ClientModel - */ - public SaClientModel checkClientModel(String clientId) { - SaClientModel clientModel = getClientModel(clientId); - if(clientModel == null) { - throw new SaOAuth2Exception("无效client_id: " + clientId); - } - return clientModel; - } - /** - * 获取 Access-Token,如果AccessToken为空则抛出异常 - * @param accessToken . - * @return . - */ - public AccessTokenModel checkAccessToken(String accessToken) { - AccessTokenModel at = getAccessToken(accessToken); - SaOAuth2Exception.throwBy(at == null, "无效access_token:" + accessToken); - return at; - } - /** - * 获取 Client-Token,如果ClientToken为空则抛出异常 - * @param clientToken . - * @return . - */ - public ClientTokenModel checkClientToken(String clientToken) { - ClientTokenModel ct = getClientToken(clientToken); - SaOAuth2Exception.throwBy(ct == null, "无效:client_token" + ct); - return ct; - } - /** - * 获取 Access-Token 所代表的LoginId - * @param accessToken Access-Token - * @return LoginId - */ - public Object getLoginIdByAccessToken(String accessToken) { - return checkAccessToken(accessToken).loginId; - } - /** - * 校验:指定 Access-Token 是否具有指定 Scope - * @param accessToken Access-Token - * @param scopes 需要校验的权限列表 - */ - public void checkScope(String accessToken, String... scopes) { - if(scopes == null || scopes.length == 0) { - return; - } - AccessTokenModel at = checkAccessToken(accessToken); - List scopeList = SaFoxUtil.convertStringToList(at.scope); - for (String scope : scopes) { - SaOAuth2Exception.throwBy(scopeList.contains(scope) == false, "该 Access-Token 不具备 Scope:" + scope); - } - } - - // ------------------- generate 构建数据 - /** - * 构建Model:请求Model - * @param req SaRequest对象 - * @param loginId 账号id - * @return RequestAuthModel对象 - */ - public RequestAuthModel generateRequestAuth(SaRequest req, Object loginId) { - RequestAuthModel ra = new RequestAuthModel(); - ra.clientId = req.getParamNotNull(Param.client_id); - ra.responseType = req.getParamNotNull(Param.response_type); - ra.redirectUri = req.getParamNotNull(Param.redirect_uri); - ra.state = req.getParam(Param.state); - ra.scope = req.getParam(Param.scope, ""); - ra.loginId = loginId; - return ra; - } - /** - * 构建Model:Code授权码 - * @param ra 请求参数Model - * @return 授权码Model - */ - public CodeModel generateCode(RequestAuthModel ra) { - - // 删除旧Code - deleteCode(getCodeValue(ra.clientId, ra.loginId)); - - // 生成新Code - String code = randomCode(ra.clientId, ra.loginId, ra.scope); - CodeModel cm = new CodeModel(code, ra.clientId, ra.scope, ra.loginId, ra.redirectUri); - - // 保存新Code - saveCode(cm); - saveCodeIndex(cm); - - // 返回 - return cm; - } - /** - * 构建Model:Access-Token - * @param code 授权码Model - * @return AccessToken Model - */ - public AccessTokenModel generateAccessToken(String code) { - - // 1、先校验 - CodeModel cm = getCode(code); - SaOAuth2Exception.throwBy(cm == null, "无效code"); - - // 2、删除旧Token - deleteAccessToken(getAccessTokenValue(cm.clientId, cm.loginId)); - deleteRefreshToken(getRefreshTokenValue(cm.clientId, cm.loginId)); - - // 3、生成token - AccessTokenModel at = converCodeToAccessToken(cm); - RefreshTokenModel rt = converAccessTokenToRefreshToken(at); - at.refreshToken = rt.refreshToken; - at.refreshExpiresTime = rt.expiresTime; - - // 4、保存token - saveAccessToken(at); - saveAccessTokenIndex(at); - saveRefreshToken(rt); - saveRefreshTokenIndex(rt); - - // 5、删除此Code - deleteCode(code); - deleteCodeIndex(cm.clientId, cm.loginId); - - // 6、返回 Access-Token - return at; - } - /** - * 刷新Model:根据 Refresh-Token 生成一个新的 Access-Token - * @param refreshToken Refresh-Token值 - * @return 新的 Access-Token - */ - public AccessTokenModel refreshAccessToken(String refreshToken) { - - // 获取 Refresh-Token 信息 - RefreshTokenModel rt = getRefreshToken(refreshToken); - SaOAuth2Exception.throwBy(rt == null, "无效refresh_token: " + refreshToken); - - // 如果配置了[每次刷新产生新的Refresh-Token] - if(SaOAuth2Manager.getConfig().getIsNewRefresh()) { - // 删除旧 Refresh-Token - deleteRefreshToken(rt.refreshToken); - - // 创建并保持新的 Refresh-Token - rt = converRefreshTokenToRefreshToken(rt); - saveRefreshToken(rt); - saveRefreshTokenIndex(rt); - } - - // 删除旧 Access-Token - deleteAccessToken(getAccessTokenValue(rt.clientId, rt.loginId)); - - // 生成新 Access-Token - AccessTokenModel at = converRefreshTokenToAccessToken(rt); - - // 保存新 Access-Token - saveAccessToken(at); - saveAccessTokenIndex(at); - - // 返回新 Access-Token - return at; - } - /** - * 构建Model:Access-Token (根据RequestAuthModel构建,用于隐藏式 and 密码式) - * @param ra 请求参数Model - * @param isCreateRt 是否生成对应的Refresh-Token - * @return Access-Token Model - */ - public AccessTokenModel generateAccessToken(RequestAuthModel ra, boolean isCreateRt) { - - // 1、删除 旧Token - deleteAccessToken(getAccessTokenValue(ra.clientId, ra.loginId)); - if(isCreateRt) { - deleteRefreshToken(getRefreshTokenValue(ra.clientId, ra.loginId)); - } - - // 2、生成 新Access-Token - String newAtValue = randomAccessToken(ra.clientId, ra.loginId, ra.scope); - AccessTokenModel at = new AccessTokenModel(newAtValue, ra.clientId, ra.loginId, ra.scope); - at.openid = getOpenid(ra.clientId, ra.loginId); - at.expiresTime = System.currentTimeMillis() + (SaOAuth2Manager.getConfig().getAccessTokenTimeout() * 1000); - - // 3、生成&保存 Refresh-Token - if(isCreateRt) { - RefreshTokenModel rt = converAccessTokenToRefreshToken(at); - saveRefreshToken(rt); - saveRefreshTokenIndex(rt); - } - - // 5、保存 新Access-Token - saveAccessToken(at); - saveAccessTokenIndex(at); - - // 6、返回 新Access-Token - return at; - } - /** - * 构建Model:Client-Token - * @param clientId 应用id - * @param scope 授权范围 - * @return Client-Token Model - */ - public ClientTokenModel generateClientToken(String clientId, String scope) { - // 1、删掉 Past-Token - deleteClientToken(getPastTokenValue(clientId)); - - // 2、将Client-Token 标记 Past-Token - String ctValue = getClientTokenValue(clientId); - savePastTokenIndex(getClientToken(ctValue)); - - // 3、生成新Token - ClientTokenModel ct = new ClientTokenModel(randomClientToken(clientId, scope), clientId, scope); - ct.expiresTime = System.currentTimeMillis() + (SaOAuth2Manager.getConfig().getClientTokenTimeout() * 1000); - - // 3、保存新Token - saveClientToken(ct); - saveClientTokenIndex(ct); - - // 4、返回 - return ct; - } - /** - * 构建URL:下放Code URL (Authorization Code 授权码) - * @param redirectUri 下放地址 - * @param code code参数 - * @param state state参数 - * @return 构建完毕的URL - */ - public String buildRedirectUri(String redirectUri, String code, String state) { - String url = SaFoxUtil.joinParam(redirectUri, Param.code, code); - if(SaFoxUtil.isEmpty(state) == false) { - url = SaFoxUtil.joinParam(url, Param.state, state); - } - return url; - } - /** - * 构建URL:下放Access-Token URL (implicit 隐藏式) - * @param redirectUri 下放地址 - * @param token token - * @param state state参数 - * @return 构建完毕的URL - */ - public String buildImplicitRedirectUri(String redirectUri, String token, String state) { - String url = SaFoxUtil.joinSharpParam(redirectUri, Param.token, token); - if(SaFoxUtil.isEmpty(state) == false) { - url = SaFoxUtil.joinSharpParam(url, Param.state, state); - } - return url; - } - /** - * 回收 Access-Token - * @param accessToken Access-Token值 - */ - public void revokeAccessToken(String accessToken) { - - // 如果查不到任何东西, 直接返回 - AccessTokenModel at = getAccessToken(accessToken); - if(at == null) { - return; - } - - // 删除 Access-Token - deleteAccessToken(accessToken); - deleteAccessTokenIndex(at.clientId, at.accessToken); - - // 删除对应的 Refresh-Token - String refreshToken = getRefreshTokenValue(at.clientId, at.loginId); - deleteRefreshToken(refreshToken); - deleteRefreshTokenIndex(at.clientId, at.loginId); - } - - // ------------------- check 数据校验 - /** - * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope - * @param loginId 账号id - * @param clientId 应用id - * @param scope 权限 - * @return 是否已经授权 - */ - public boolean isGrant(Object loginId, String clientId, String scope) { - List grantScopeList = SaFoxUtil.convertStringToList(getGrantScope(clientId, loginId)); - List scopeList = SaFoxUtil.convertStringToList(scope); - return scopeList.size() == 0 || grantScopeList.containsAll(scopeList); - } - /** - * 校验:该Client是否签约了指定的Scope - * @param clientId 应用id - * @param scope 权限(多个用逗号隔开) - */ - public void checkContract(String clientId, String scope) { - List clientScopeList = SaFoxUtil.convertStringToList(checkClientModel(clientId).contractScope); - List scopelist = SaFoxUtil.convertStringToList(scope); - if(clientScopeList.containsAll(scopelist) == false) { - throw new SaOAuth2Exception("请求的Scope暂未签约"); - } - } - /** - * 校验:该Client使用指定url作为回调地址,是否合法 - * @param clientId 应用id - * @param url 指定url - */ - public void checkRightUrl(String clientId, String url) { - // 1、是否是一个有效的url - if(SaFoxUtil.isUrl(url) == false) { - throw new SaOAuth2Exception("无效redirect_url:" + url); - } - - // 2、截取掉?后面的部分 - int qIndex = url.indexOf("?"); - if(qIndex != -1) { - url = url.substring(0, qIndex); - } - - // 3、是否在[允许地址列表]之中 - List allowList = SaFoxUtil.convertStringToList(checkClientModel(clientId).allowUrl); - if(SaManager.getSaTokenAction().hasElement(allowList, url) == false) { - throw new SaOAuth2Exception("非法redirect_url:" + url); - } - } - /** - * 校验:clientId 与 clientSecret 是否正确 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @return SaClientModel对象 - */ - public SaClientModel checkClientSecret(String clientId, String clientSecret) { - SaClientModel cm = checkClientModel(clientId); - SaOAuth2Exception.throwBy(cm.clientSecret == null || cm.clientSecret.equals(clientSecret) == false, "无效client_secret: " + clientSecret); - return cm; - } - /** - * 校验:使用 code 获取 token 时提供的参数校验 - * @param code 授权码 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param redirectUri 重定向地址 - * @return CodeModel对象 - */ - public CodeModel checkGainTokenParam(String code, String clientId, String clientSecret, String redirectUri) { - - // 校验:Code是否存在 - CodeModel cm = getCode(code); - SaOAuth2Exception.throwBy(cm == null, "无效code: " + code); - - // 校验:ClientId是否一致 - SaOAuth2Exception.throwBy(cm.clientId.equals(clientId) == false, "无效client_id: " + clientId); - - // 校验:Secret是否正确 - String dbSecret = checkClientModel(clientId).clientSecret; - SaOAuth2Exception.throwBy(dbSecret == null || dbSecret.equals(clientSecret) == false, "无效client_secret: " + clientSecret); - - // 如果提供了redirectUri,则校验其是否与请求Code时提供的一致 - if(SaFoxUtil.isEmpty(redirectUri) == false) { - SaOAuth2Exception.throwBy(redirectUri.equals(cm.redirectUri) == false, "无效redirect_uri: " + redirectUri); - } - - // 返回CodeMdoel - return cm; - } - /** - * 校验:使用 Refresh-Token 刷新 Access-Token 时提供的参数校验 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param refreshToken Refresh-Token - * @return CodeModel对象 - */ - public RefreshTokenModel checkRefreshTokenParam(String clientId, String clientSecret, String refreshToken) { - - // 校验:Refresh-Token是否存在 - RefreshTokenModel rt = getRefreshToken(refreshToken); - SaOAuth2Exception.throwBy(rt == null, "无效refresh_token: " + refreshToken); - - // 校验:ClientId是否一致 - SaOAuth2Exception.throwBy(rt.clientId.equals(clientId) == false, "无效client_id: " + clientId); - - // 校验:Secret是否正确 - String dbSecret = checkClientModel(clientId).clientSecret; - SaOAuth2Exception.throwBy(dbSecret == null || dbSecret.equals(clientSecret) == false, "无效client_secret: " + clientSecret); - - // 返回Refresh-Token - return rt; - } - /** - * 校验:Access-Token、clientId、clientSecret 三者是否匹配成功 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param accessToken Access-Token - * @return SaClientModel对象 - */ - public AccessTokenModel checkAccessTokenParam(String clientId, String clientSecret, String accessToken) { - AccessTokenModel at = checkAccessToken(accessToken); - SaOAuth2Exception.throwBy(at.clientId.equals(clientId) == false, "无效client_id:" + clientId); - checkClientSecret(clientId, clientSecret); - return at; - } - - // ------------------- conver 数据转换 - /** - * 将 Code 转换为 Access-Token - * @param cm CodeModel对象 - * @return AccessToken对象 - */ - public AccessTokenModel converCodeToAccessToken(CodeModel cm) { - AccessTokenModel at = new AccessTokenModel(); - at.accessToken = randomAccessToken(cm.clientId, cm.loginId, cm.scope); - // at.refreshToken = randomRefreshToken(cm.clientId, cm.loginId, cm.scope); - at.clientId = cm.clientId; - at.loginId = cm.loginId; - at.scope = cm.scope; - at.openid = getOpenid(cm.clientId, cm.loginId); - at.expiresTime = System.currentTimeMillis() + (SaOAuth2Manager.getConfig().getAccessTokenTimeout() * 1000); - // at.refreshExpiresTime = System.currentTimeMillis() + (SaOAuth2Manager.getConfig().getRefreshTokenTimeout() * 1000); - return at; - } - /** - * 将 Access-Token 转换为 Refresh-Token - * @param at . - * @return . - */ - public RefreshTokenModel converAccessTokenToRefreshToken(AccessTokenModel at) { - RefreshTokenModel rt = new RefreshTokenModel(); - rt.refreshToken = randomRefreshToken(at.clientId, at.loginId, at.scope); - rt.clientId = at.clientId; - rt.loginId = at.loginId; - rt.scope = at.scope; - rt.openid = at.openid; - rt.expiresTime = System.currentTimeMillis() + (SaOAuth2Manager.getConfig().getRefreshTokenTimeout() * 1000); - // 改变at属性 - at.refreshToken = rt.refreshToken; - at.refreshExpiresTime = rt.expiresTime; - return rt; - } - /** - * 将 Refresh-Token 转换为 Access-Token - * @param rt . - * @return . - */ - public AccessTokenModel converRefreshTokenToAccessToken(RefreshTokenModel rt) { - AccessTokenModel at = new AccessTokenModel(); - at.accessToken = randomAccessToken(rt.clientId, rt.loginId, rt.scope); - at.refreshToken = rt.refreshToken; - at.clientId = rt.clientId; - at.loginId = rt.loginId; - at.scope = rt.scope; - at.openid = rt.openid; - at.expiresTime = System.currentTimeMillis() + (SaOAuth2Manager.getConfig().getAccessTokenTimeout() * 1000); - at.refreshExpiresTime = rt.expiresTime; - return at; - } - /** - * 根据 Refresh-Token 创建一个新的 Refresh-Token - * @param rt . - * @return . - */ - public RefreshTokenModel converRefreshTokenToRefreshToken(RefreshTokenModel rt) { - RefreshTokenModel newRt = new RefreshTokenModel(); - newRt.refreshToken = randomRefreshToken(rt.clientId, rt.loginId, rt.scope); - newRt.expiresTime = System.currentTimeMillis() + (SaOAuth2Manager.getConfig().getRefreshTokenTimeout() * 1000); - newRt.clientId = rt.clientId; - newRt.scope = rt.scope; - newRt.loginId = rt.loginId; - newRt.openid = rt.openid; - return newRt; - } - - // ------------------- save 数据 - /** - * 持久化:Code-Model - * @param c . - */ - public void saveCode(CodeModel c) { - if(c == null) { - return; - } - SaManager.getSaTokenDao().setObject(splicingCodeSaveKey(c.code), c, SaOAuth2Manager.getConfig().getCodeTimeout()); - } - /** - * 持久化:Code-索引 - * @param c . - */ - public void saveCodeIndex(CodeModel c) { - if(c == null) { - return; - } - SaManager.getSaTokenDao().set(splicingCodeIndexKey(c.clientId, c.loginId), c.code, SaOAuth2Manager.getConfig().getCodeTimeout()); - } - /** - * 持久化:AccessToken-Model - * @param at . - */ - public void saveAccessToken(AccessTokenModel at) { - if(at == null) { - return; - } - SaManager.getSaTokenDao().setObject(splicingAccessTokenSaveKey(at.accessToken), at, at.getExpiresIn()); - } - /** - * 持久化:AccessToken-索引 - * @param at . - */ - public void saveAccessTokenIndex(AccessTokenModel at) { - if(at == null) { - return; - } - SaManager.getSaTokenDao().set(splicingAccessTokenIndexKey(at.clientId, at.loginId), at.accessToken, at.getExpiresIn()); - } - /** - * 持久化:RefreshToken-Model - * @param rt . - */ - public void saveRefreshToken(RefreshTokenModel rt) { - if(rt == null) { - return; - } - SaManager.getSaTokenDao().setObject(splicingRefreshTokenSaveKey(rt.refreshToken), rt, rt.getExpiresIn()); - } - /** - * 持久化:RefreshToken-索引 - * @param rt . - */ - public void saveRefreshTokenIndex(RefreshTokenModel rt) { - if(rt == null) { - return; - } - SaManager.getSaTokenDao().set(splicingRefreshTokenIndexKey(rt.clientId, rt.loginId), rt.refreshToken, rt.getExpiresIn()); - } - /** - * 持久化:ClientToken-Model - * @param ct . - */ - public void saveClientToken(ClientTokenModel ct) { - if(ct == null) { - return; - } - SaManager.getSaTokenDao().setObject(splicingClientTokenSaveKey(ct.clientToken), ct, ct.getExpiresIn()); - } - /** - * 持久化:ClientToken-索引 - * @param ct . - */ - public void saveClientTokenIndex(ClientTokenModel ct) { - if(ct == null) { - return; - } - SaManager.getSaTokenDao().set(splicingClientTokenIndexKey(ct.clientId), ct.clientToken, ct.getExpiresIn()); - } - /** - * 持久化:Past-Token-索引 - * @param ct . - */ - public void savePastTokenIndex(ClientTokenModel ct) { - if(ct == null) { - return; - } - SaManager.getSaTokenDao().set(splicingPastTokenIndexKey(ct.clientId), ct.clientToken, ct.getExpiresIn()); - } - /** - * 持久化:用户授权记录 - * @param clientId 应用id - * @param loginId 账号id - * @param scope 权限列表(多个逗号隔开) - */ - public void saveGrantScope(String clientId, Object loginId, String scope) { - if(SaFoxUtil.isEmpty(scope) == false) { - long ttl = SaOAuth2Manager.getConfig().getAccessTokenTimeout(); - SaManager.getSaTokenDao().set(splicingGrantScopeKey(clientId, loginId), scope, ttl); - } - } - - // ------------------- get 数据 - /** - * 获取:Code Model - * @param code . - * @return . - */ - public CodeModel getCode(String code) { - if(code == null) { - return null; - } - return (CodeModel)SaManager.getSaTokenDao().getObject(splicingCodeSaveKey(code)); - } - /** - * 获取:Code Value - * @param clientId 应用id - * @param loginId 账号id - * @return . - */ - public String getCodeValue(String clientId, Object loginId) { - return SaManager.getSaTokenDao().get(splicingCodeIndexKey(clientId, loginId)); - } - /** - * 获取:Access-Token Model - * @param accessToken . - * @return . - */ - public AccessTokenModel getAccessToken(String accessToken) { - if(accessToken == null) { - return null; - } - return (AccessTokenModel)SaManager.getSaTokenDao().getObject(splicingAccessTokenSaveKey(accessToken)); - } - /** - * 获取:Access-Token Value - * @param clientId 应用id - * @param loginId 账号id - * @return . - */ - public String getAccessTokenValue(String clientId, Object loginId) { - return SaManager.getSaTokenDao().get(splicingAccessTokenIndexKey(clientId, loginId)); - } - /** - * 获取:Refresh-Token Model - * @param refreshToken . - * @return . - */ - public RefreshTokenModel getRefreshToken(String refreshToken) { - if(refreshToken == null) { - return null; - } - return (RefreshTokenModel)SaManager.getSaTokenDao().getObject(splicingRefreshTokenSaveKey(refreshToken)); - } - /** - * 获取:Refresh-Token Value - * @param clientId 应用id - * @param loginId 账号id - * @return . - */ - public String getRefreshTokenValue(String clientId, Object loginId) { - return SaManager.getSaTokenDao().get(splicingRefreshTokenIndexKey(clientId, loginId)); - } - /** - * 获取:Client-Token Model - * @param clientToken . - * @return . - */ - public ClientTokenModel getClientToken(String clientToken) { - if(clientToken == null) { - return null; - } - return (ClientTokenModel)SaManager.getSaTokenDao().getObject(splicingClientTokenSaveKey(clientToken)); - } - /** - * 获取:Client-Token Value - * @param clientId 应用id - * @return . - */ - public String getClientTokenValue(String clientId) { - return SaManager.getSaTokenDao().get(splicingClientTokenIndexKey(clientId)); - } - /** - * 获取:Past-Token Value - * @param clientId 应用id - * @return . - */ - public String getPastTokenValue(String clientId) { - return SaManager.getSaTokenDao().get(splicingPastTokenIndexKey(clientId)); - } - /** - * 获取:用户授权记录 - * @param clientId 应用id - * @param loginId 账号id - * @return 权限 - */ - public String getGrantScope(String clientId, Object loginId) { - return SaManager.getSaTokenDao().get(splicingGrantScopeKey(clientId, loginId)); - } - - // ------------------- delete数据 - /** - * 删除:Code - * @param code 值 - */ - public void deleteCode(String code) { - if(code != null) { - SaManager.getSaTokenDao().deleteObject(splicingCodeSaveKey(code)); - } - } - /** - * 删除:Code索引 - * @param clientId 应用id - * @param loginId 账号id - */ - public void deleteCodeIndex(String clientId, Object loginId) { - SaManager.getSaTokenDao().delete(splicingCodeIndexKey(clientId, loginId)); - } - /** - * 删除:Access-Token - * @param accessToken 值 - */ - public void deleteAccessToken(String accessToken) { - if(accessToken != null) { - SaManager.getSaTokenDao().deleteObject(splicingAccessTokenSaveKey(accessToken)); - } - } - /** - * 删除:Access-Token索引 - * @param clientId 应用id - * @param loginId 账号id - */ - public void deleteAccessTokenIndex(String clientId, Object loginId) { - SaManager.getSaTokenDao().delete(splicingAccessTokenIndexKey(clientId, loginId)); - } - /** - * 删除:Refresh-Token - * @param refreshToken 值 - */ - public void deleteRefreshToken(String refreshToken) { - if(refreshToken != null) { - SaManager.getSaTokenDao().deleteObject(splicingRefreshTokenSaveKey(refreshToken)); - } - } - /** - * 删除:Refresh-Token索引 - * @param clientId 应用id - * @param loginId 账号id - */ - public void deleteRefreshTokenIndex(String clientId, Object loginId) { - SaManager.getSaTokenDao().delete(splicingRefreshTokenIndexKey(clientId, loginId)); - } - /** - * 删除:Client-Token - * @param clientToken 值 - */ - public void deleteClientToken(String clientToken) { - if(clientToken != null) { - SaManager.getSaTokenDao().deleteObject(splicingClientTokenSaveKey(clientToken)); - } - } - /** - * 删除:Client-Token索引 - * @param clientId 应用id - */ - public void deleteClientTokenIndex(String clientId) { - SaManager.getSaTokenDao().delete(splicingClientTokenIndexKey(clientId)); - } - /** - * 删除:Past-Token索引 - * @param clientId 应用id - */ - public void deletePastTokenIndex(String clientId) { - SaManager.getSaTokenDao().delete(splicingPastTokenIndexKey(clientId)); - } - /** - * 删除:用户授权记录 - * @param clientId 应用id - * @param loginId 账号id - */ - public void deleteGrantScope(String clientId, Object loginId) { - SaManager.getSaTokenDao().delete(splicingGrantScopeKey(clientId, loginId)); - } - - // ------------------- Random数据 - /** - * 随机一个 Code - * @param clientId 应用id - * @param loginId 账号id - * @param scope 权限 - * @return Code - */ - public String randomCode(String clientId, Object loginId, String scope) { - return SaFoxUtil.getRandomString(60); - } - /** - * 随机一个 Access-Token - * @param clientId 应用id - * @param loginId 账号id - * @param scope 权限 - * @return Access-Token - */ - public String randomAccessToken(String clientId, Object loginId, String scope) { - return SaFoxUtil.getRandomString(60); - } - /** - * 随机一个 Refresh-Token - * @param clientId 应用id - * @param loginId 账号id - * @param scope 权限 - * @return Refresh-Token - */ - public String randomRefreshToken(String clientId, Object loginId, String scope) { - return SaFoxUtil.getRandomString(60); - } - /** - * 随机一个 Client-Token - * @param clientId 应用id - * @param scope 权限 - * @return Client-Token - */ - public String randomClientToken(String clientId, String scope) { - return SaFoxUtil.getRandomString(60); - } - - // ------------------- 拼接key - /** - * 拼接key:Code持久化 - * @param code 授权码 - * @return key - */ - public String splicingCodeSaveKey(String code) { - return SaManager.getConfig().getTokenName() + ":oauth2:code:" + code; - } - /** - * 拼接key:Code索引 - * @param clientId 应用id - * @param loginId 账号id - * @return key - */ - public String splicingCodeIndexKey(String clientId, Object loginId) { - return SaManager.getConfig().getTokenName() + ":oauth2:code-index:" + clientId + ":" + loginId; - } - /** - * 拼接key:Access-Token持久化 - * @param accessToken accessToken - * @return key - */ - public String splicingAccessTokenSaveKey(String accessToken) { - return SaManager.getConfig().getTokenName() + ":oauth2:access-token:" + accessToken; - } - /** - * 拼接key:Access-Token索引 - * @param clientId 应用id - * @param loginId 账号id - * @return key - */ - public String splicingAccessTokenIndexKey(String clientId, Object loginId) { - return SaManager.getConfig().getTokenName() + ":oauth2:access-token-index:" + clientId + ":" + loginId; - } - /** - * 拼接key:Refresh-Token持久化 - * @param refreshToken refreshToken - * @return key - */ - public String splicingRefreshTokenSaveKey(String refreshToken) { - return SaManager.getConfig().getTokenName() + ":oauth2:refresh-token:" + refreshToken; - } - /** - * 拼接key:Refresh-Token索引 - * @param clientId 应用id - * @param loginId 账号id - * @return key - */ - public String splicingRefreshTokenIndexKey(String clientId, Object loginId) { - return SaManager.getConfig().getTokenName() + ":oauth2:refresh-token-index:" + clientId + ":" + loginId; - } - /** - * 拼接key:Client-Token持久化 - * @param clientToken clientToken - * @return key - */ - public String splicingClientTokenSaveKey(String clientToken) { - return SaManager.getConfig().getTokenName() + ":oauth2:client-token:" + clientToken; - } - /** - * 拼接key:Past-Token 索引 - * @param clientId clientId - * @return key - */ - public String splicingClientTokenIndexKey(String clientId) { - return SaManager.getConfig().getTokenName() + ":oauth2:client-token-indedx:" + clientId; - } - /** - * 拼接key:Past-Token 索引 - * @param clientId clientId - * @return key - */ - public String splicingPastTokenIndexKey(String clientId) { - return SaManager.getConfig().getTokenName() + ":oauth2:past-token-indedx:" + clientId; - } - /** - * 拼接key:用户授权记录 - * @param clientId 应用id - * @param loginId 账号id - * @return key - */ - public String splicingGrantScopeKey(String clientId, Object loginId) { - return SaManager.getConfig().getTokenName() + ":oauth2:grant-scope:" + clientId + ":" + loginId; - } - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Util.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Util.java deleted file mode 100644 index 068f9001e..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/logic/SaOAuth2Util.java +++ /dev/null @@ -1,298 +0,0 @@ -package cn.dev33.satoken.oauth2.logic; - -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.oauth2.model.AccessTokenModel; -import cn.dev33.satoken.oauth2.model.ClientTokenModel; -import cn.dev33.satoken.oauth2.model.CodeModel; -import cn.dev33.satoken.oauth2.model.RefreshTokenModel; -import cn.dev33.satoken.oauth2.model.RequestAuthModel; -import cn.dev33.satoken.oauth2.model.SaClientModel; - -/** - * Sa-Token-OAuth2 模块 工具类 - * @author kong - * - */ -public class SaOAuth2Util { - - /** - * 模板代码对象 - */ - public static SaOAuth2Template saOAuth2Template = new SaOAuth2Template(); - - - // ------------------- 资源校验API - - /** - * 根据id获取Client信息, 如果Client为空,则抛出异常 - * @param clientId 应用id - * @return ClientModel - */ - public static SaClientModel checkClientModel(String clientId) { - return saOAuth2Template.checkClientModel(clientId); - } - - /** - * 获取 Access-Token,如果AccessToken为空则抛出异常 - * @param accessToken . - * @return . - */ - public static AccessTokenModel checkAccessToken(String accessToken) { - return saOAuth2Template.checkAccessToken(accessToken); - } - - /** - * 获取 Client-Token,如果ClientToken为空则抛出异常 - * @param clientToken . - * @return . - */ - public static ClientTokenModel checkClientToken(String clientToken) { - return saOAuth2Template.checkClientToken(clientToken); - } - - /** - * 获取 Access-Token 所代表的LoginId - * @param accessToken Access-Token - * @return LoginId - */ - public static Object getLoginIdByAccessToken(String accessToken) { - return saOAuth2Template.getLoginIdByAccessToken(accessToken); - } - - /** - * 校验:指定 Access-Token 是否具有指定 Scope - * @param accessToken Access-Token - * @param scopes 需要校验的权限列表 - */ - public static void checkScope(String accessToken, String... scopes) { - saOAuth2Template.checkScope(accessToken, scopes); - } - - // ------------------- generate 构建数据 - - /** - * 构建Model:请求Model - * @param req SaRequest对象 - * @param loginId 账号id - * @return RequestAuthModel对象 - */ - public static RequestAuthModel generateRequestAuth(SaRequest req, Object loginId) { - return saOAuth2Template.generateRequestAuth(req, loginId); - } - - /** - * 构建Model:Code授权码 - * @param ra 请求参数Model - * @return 授权码Model - */ - public static CodeModel generateCode(RequestAuthModel ra) { - return saOAuth2Template.generateCode(ra); - } - - /** - * 构建Model:Access-Token - * @param code 授权码Model - * @return AccessToken Model - */ - public static AccessTokenModel generateAccessToken(String code) { - return saOAuth2Template.generateAccessToken(code); - } - - /** - * 刷新Model:根据 Refresh-Token 生成一个新的 Access-Token - * @param refreshToken Refresh-Token值 - * @return 新的 Access-Token - */ - public static AccessTokenModel refreshAccessToken(String refreshToken) { - return saOAuth2Template.refreshAccessToken(refreshToken); - } - - /** - * 构建Model:Access-Token (根据RequestAuthModel构建,用于隐藏式 and 密码式) - * @param ra 请求参数Model - * @param isCreateRt 是否生成对应的Refresh-Token - * @return Access-Token Model - */ - public static AccessTokenModel generateAccessToken(RequestAuthModel ra, boolean isCreateRt) { - return saOAuth2Template.generateAccessToken(ra, isCreateRt); - } - - /** - * 构建Model:Client-Token - * @param clientId 应用id - * @param scope 授权范围 - * @return Client-Token Model - */ - public static ClientTokenModel generateClientToken(String clientId, String scope) { - return saOAuth2Template.generateClientToken(clientId, scope); - } - - /** - * 构建URL:下放Code URL (Authorization Code 授权码) - * @param redirectUri 下放地址 - * @param code code参数 - * @param state state参数 - * @return 构建完毕的URL - */ - public static String buildRedirectUri(String redirectUri, String code, String state) { - return saOAuth2Template.buildRedirectUri(redirectUri, code, state); - } - - /** - * 构建URL:下放Access-Token URL (implicit 隐藏式) - * @param redirectUri 下放地址 - * @param token token - * @param state state参数 - * @return 构建完毕的URL - */ - public static String buildImplicitRedirectUri(String redirectUri, String token, String state) { - return saOAuth2Template.buildImplicitRedirectUri(redirectUri, token, state); - } - - /** - * 回收 Access-Token - * @param accessToken Access-Token值 - */ - public static void revokeAccessToken(String accessToken) { - saOAuth2Template.revokeAccessToken(accessToken); - } - - // ------------------- 数据校验 - - /** - * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope - * @param loginId 账号id - * @param clientId 应用id - * @param scope 权限 - * @return 是否已经授权 - */ - public static boolean isGrant(Object loginId, String clientId, String scope) { - return saOAuth2Template.isGrant(loginId, clientId, scope); - } - - /** - * 校验:该Client是否签约了指定的Scope - * @param clientId 应用id - * @param scope 权限(多个用逗号隔开) - */ - public static void checkContract(String clientId, String scope) { - saOAuth2Template.checkContract(clientId, scope); - } - - /** - * 校验:该Client使用指定url作为回调地址,是否合法 - * @param clientId 应用id - * @param url 指定url - */ - public static void checkRightUrl(String clientId, String url) { - saOAuth2Template.checkRightUrl(clientId, url); - } - - /** - * 校验:clientId 与 clientSecret 是否正确 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @return SaClientModel对象 - */ - public static SaClientModel checkClientSecret(String clientId, String clientSecret) { - return saOAuth2Template.checkClientSecret(clientId, clientSecret); - } - - /** - * 校验:使用 code 获取 token 时提供的参数校验 - * @param code 授权码 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param redirectUri 重定向地址 - * @return CodeModel对象 - */ - public static CodeModel checkGainTokenParam(String code, String clientId, String clientSecret, String redirectUri) { - return saOAuth2Template.checkGainTokenParam(code, clientId, clientSecret, redirectUri); - } - - /** - * 校验:使用 Refresh-Token 刷新 Access-Token 时提供的参数校验 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param refreshToken Refresh-Token - * @return CodeModel对象 - */ - public static RefreshTokenModel checkRefreshTokenParam(String clientId, String clientSecret, String refreshToken) { - return saOAuth2Template.checkRefreshTokenParam(clientId, clientSecret, refreshToken); - } - - /** - * 校验:Access-Token、clientId、clientSecret 三者是否匹配成功 - * @param clientId 应用id - * @param clientSecret 秘钥 - * @param accessToken Access-Token - * @return SaClientModel对象 - */ - public static AccessTokenModel checkAccessTokenParam(String clientId, String clientSecret, String accessToken) { - return saOAuth2Template.checkAccessTokenParam(clientId, clientSecret, accessToken); - } - - // ------------------- save 数据 - - /** - * 持久化:用户授权记录 - * @param clientId 应用id - * @param loginId 账号id - * @param scope 权限列表(多个逗号隔开) - */ - public static void saveGrantScope(String clientId, Object loginId, String scope) { - saOAuth2Template.saveGrantScope(clientId, loginId, scope); - } - - - // ------------------- get 数据 - - /** - * 获取:Code Model - * @param code . - * @return . - */ - public static CodeModel getCode(String code) { - return saOAuth2Template.getCode(code); - } - - /** - * 获取:Access-Token Model - * @param accessToken . - * @return . - */ - public static AccessTokenModel getAccessToken(String accessToken) { - return saOAuth2Template.getAccessToken(accessToken); - } - - /** - * 获取:Refresh-Token Model - * @param refreshToken . - * @return . - */ - public static RefreshTokenModel getRefreshToken(String refreshToken) { - return saOAuth2Template.getRefreshToken(refreshToken); - } - - /** - * 获取:Client-Token Model - * @param clientToken . - * @return . - */ - public static ClientTokenModel getClientToken(String clientToken) { - return saOAuth2Template.getClientToken(clientToken); - } - - /** - * 获取:用户授权记录 - * @param clientId 应用id - * @param loginId 账号id - * @return 权限 - */ - public static String getGrantScope(String clientId, Object loginId) { - return saOAuth2Template.getGrantScope(clientId, loginId); - } - - - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/AccessTokenModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/AccessTokenModel.java deleted file mode 100644 index f45284502..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/AccessTokenModel.java +++ /dev/null @@ -1,114 +0,0 @@ -package cn.dev33.satoken.oauth2.model; - -import java.io.Serializable; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Model: Access-Token - * @author kong - * - */ -public class AccessTokenModel implements Serializable { - - private static final long serialVersionUID = -6541180061782004705L; - - /** - * Access-Token 值 - */ - public String accessToken; - - /** - * Refresh-Token 值 - */ - public String refreshToken; - - /** - * Access-Token 到期时间 - */ - public long expiresTime; - - /** - * Refresh-Token 到期时间 - */ - public long refreshExpiresTime; - - /** - * 应用id - */ - public String clientId; - - /** - * 账号id - */ - public Object loginId; - - /** - * 开放账号id - */ - public String openid; - - /** - * 授权范围 - */ - public String scope; - - public AccessTokenModel() {} - /** - * 构建一个 - * @param accessToken accessToken - * @param clientId 应用id - * @param scope 请求授权范围 - * @param loginId 对应的账号id - */ - public AccessTokenModel(String accessToken, String clientId, Object loginId, String scope) { - super(); - this.accessToken = accessToken; - this.clientId = clientId; - this.loginId = loginId; - this.scope = scope; - } - - @Override - public String toString() { - return "AccessTokenModel [accessToken=" + accessToken + ", refreshToken=" + refreshToken - + ", accessTokenTimeout=" + expiresTime + ", refreshTokenTimeout=" + refreshExpiresTime - + ", clientId=" + clientId + ", scope=" + scope + ", openid=" + openid + "]"; - } - - - /** - * 获取:此 Access-Token 的剩余有效期(秒) - * @return see note - */ - public long getExpiresIn() { - long s = (expiresTime - System.currentTimeMillis()) / 1000; - return s < 1 ? -2 : s; - } - - /** - * 获取:此 Refresh-Token 的剩余有效期(秒) - * @return see note - */ - public long getRefreshExpiresIn() { - long s = (refreshExpiresTime - System.currentTimeMillis()) / 1000; - return s < 1 ? -2 : s; - } - - /** - * 将所有属性转换为下划线形式的Map - * @return 属性转Map - */ - public Map toLineMap() { - Map map = new LinkedHashMap(); - map.put("access_token", accessToken); - map.put("refresh_token", refreshToken); - map.put("expires_in", getExpiresIn()); - map.put("refresh_expires_in", getRefreshExpiresIn()); - map.put("client_id", clientId); - map.put("scope", scope); - map.put("openid", openid); - return map; - } - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/ClientTokenModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/ClientTokenModel.java deleted file mode 100644 index 681acb021..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/ClientTokenModel.java +++ /dev/null @@ -1,80 +0,0 @@ -package cn.dev33.satoken.oauth2.model; - -import java.io.Serializable; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Model: Client-Token - * @author kong - * - */ -public class ClientTokenModel implements Serializable { - - private static final long serialVersionUID = -6541180061782004705L; - - /** - * Client-Token 值 - */ - public String clientToken; - - /** - * Client-Token 到期时间 - */ - public long expiresTime; - - /** - * 应用id - */ - public String clientId; - - /** - * 授权范围 - */ - public String scope; - - public ClientTokenModel() {} - - /** - * 构建一个 - * @param accessToken accessToken - * @param clientId 应用id - * @param scope 请求授权范围 - */ - public ClientTokenModel(String accessToken, String clientId, String scope) { - super(); - this.clientToken = accessToken; - this.clientId = clientId; - this.scope = scope; - } - - @Override - public String toString() { - return "ClientTokenModel [clientToken=" + clientToken + ", expiresTime=" + expiresTime + ", clientId=" - + clientId + ", scope=" + scope + "]"; - } - - /** - * 获取:此 Client-Token 的剩余有效期(秒) - * @return see note - */ - public long getExpiresIn() { - long s = (expiresTime - System.currentTimeMillis()) / 1000; - return s < 1 ? -2 : s; - } - - /** - * 将所有属性转换为下划线形式的Map - * @return 属性转Map - */ - public Map toLineMap() { - Map map = new LinkedHashMap(); - map.put("client_token", clientToken); - map.put("expires_in", getExpiresIn()); - map.put("client_id", clientId); - map.put("scope", scope); - return map; - } - - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/CodeModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/CodeModel.java deleted file mode 100644 index 774083fc2..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/CodeModel.java +++ /dev/null @@ -1,139 +0,0 @@ -package cn.dev33.satoken.oauth2.model; - -import java.io.Serializable; - -/** - * Model: 授权码 - * @author kong - * - */ -public class CodeModel implements Serializable { - - private static final long serialVersionUID = -6541180061782004705L; - - /** - * 授权码 - */ - public String code; - - /** - * 应用id - */ - public String clientId; - - /** - * 授权范围 - */ - public String scope; - - /** - * 对应账号id - */ - public Object loginId; - - /** - * 重定向的地址 - */ - public String redirectUri; - - /** - * 构建一个 - */ - public CodeModel() { - - } - /** - * 构建一个 - * @param code 授权码 - * @param clientId 应用id - * @param scope 请求授权范围 - * @param loginId 对应的账号id - * @param redirectUri 重定向地址 - */ - public CodeModel(String code, String clientId, String scope, Object loginId, String redirectUri) { - super(); - this.code = code; - this.clientId = clientId; - this.scope = scope; - this.loginId = loginId; - this.redirectUri = redirectUri; - } - - - /** - * @return code - */ - public String getCode() { - return code; - } - - /** - * @param code 要设置的 code - */ - public void setCode(String code) { - this.code = code; - } - - /** - * @return clientId - */ - public String getClientId() { - return clientId; - } - - /** - * @param clientId 要设置的 clientId - */ - public void setClientId(String clientId) { - this.clientId = clientId; - } - - /** - * @return scope - */ - public String getScope() { - return scope; - } - - /** - * @param scope 要设置的 scope - */ - public void setScope(String scope) { - this.scope = scope; - } - - /** - * @return loginId - */ - public Object getLoginId() { - return loginId; - } - - /** - * @param loginId 要设置的 loginId - */ - public void setLoginId(Object loginId) { - this.loginId = loginId; - } - - /** - * @return redirectUri - */ - public String getRedirectUri() { - return redirectUri; - } - - /** - * @param redirectUri 要设置的 redirectUri - */ - public void setRedirectUri(String redirectUri) { - this.redirectUri = redirectUri; - } - - @Override - public String toString() { - return "CodeModel [code=" + code + ", clientId=" + clientId + ", scope=" + scope + ", loginId=" + loginId - + ", redirectUri=" + redirectUri + "]"; - } - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RefreshTokenModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RefreshTokenModel.java deleted file mode 100644 index 4414d56dc..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RefreshTokenModel.java +++ /dev/null @@ -1,59 +0,0 @@ -package cn.dev33.satoken.oauth2.model; - -import java.io.Serializable; - -/** - * Model: Refresh-Token - * @author kong - * - */ -public class RefreshTokenModel implements Serializable { - - private static final long serialVersionUID = -6541180061782004705L; - - /** - * Refresh-Token 值 - */ - public String refreshToken; - - /** - * Refresh-Token 到期时间 - */ - public long expiresTime; - - /** - * 应用id - */ - public String clientId; - - /** - * 授权范围 - */ - public String scope; - - /** - * 对应账号id - */ - public Object loginId; - - /** - * 对应账号id - */ - public String openid; - - @Override - public String toString() { - return "RefreshTokenModel [refreshToken=" + refreshToken + ", expiresTime=" + expiresTime - + ", clientId=" + clientId + ", scope=" + scope + ", loginId=" + loginId + ", openid=" + openid + "]"; - } - - /** - * 获取:此 Refresh-Token 的剩余有效期(秒) - * @return see note - */ - public long getExpiresIn() { - long s = (expiresTime - System.currentTimeMillis()) / 1000; - return s < 1 ? -2 : s; - } - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RequestAuthModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RequestAuthModel.java deleted file mode 100644 index 7dcb3775d..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/RequestAuthModel.java +++ /dev/null @@ -1,165 +0,0 @@ -package cn.dev33.satoken.oauth2.model; - -import java.io.Serializable; - -import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * 请求授权参数的Model - * @author kong - * - */ -public class RequestAuthModel implements Serializable { - - private static final long serialVersionUID = -6541180061782004705L; - - /** - * 应用id - */ - public String clientId; - - /** - * 授权范围 - */ - public String scope; - - /** - * 对应的账号id - */ - public Object loginId; - - /** - * 待重定向URL - */ - public String redirectUri; - - /** - * 授权类型, 非必填 - */ - public String responseType; - - /** - * 状态标识, 可为null - */ - public String state; - - - /** - * @return clientId - */ - public String getClientId() { - return clientId; - } - - /** - * @param clientId 要设置的 clientId - * @return 对象自身 - */ - public RequestAuthModel setClientId(String clientId) { - this.clientId = clientId; - return this; - } - - /** - * @return scope - */ - public String getScope() { - return scope; - } - - /** - * @param scope 要设置的 scope - * @return 对象自身 - */ - public RequestAuthModel setScope(String scope) { - this.scope = scope; - return this; - } - - /** - * @return loginId - */ - public Object getLoginId() { - return loginId; - } - - /** - * @param loginId 要设置的 loginId - * @return 对象自身 - */ - public RequestAuthModel setLoginId(Object loginId) { - this.loginId = loginId; - return this; - } - - /** - * @return redirectUri - */ - public String getRedirectUri() { - return redirectUri; - } - - /** - * @param redirectUri 要设置的 redirectUri - * @return 对象自身 - */ - public RequestAuthModel setRedirectUri(String redirectUri) { - this.redirectUri = redirectUri; - return this; - } - - /** - * @return responseType - */ - public String getResponseType() { - return responseType; - } - - /** - * @param responseType 要设置的 responseType - * @return 对象自身 - */ - public RequestAuthModel setResponseType(String responseType) { - this.responseType = responseType; - return this; - } - - /** - * @return state - */ - public String getState() { - return state; - } - - /** - * @param state 要设置的 state - * @return 对象自身 - */ - public RequestAuthModel setState(String state) { - this.state = state; - return this; - } - - /** - * 检查此Model参数是否有效 - * @return 对象自身 - */ - public RequestAuthModel checkModel() { - if(SaFoxUtil.isEmpty(clientId)) { - throw new SaTokenException("无效client_id"); - } - if(SaFoxUtil.isEmpty(scope)) { - throw new SaTokenException("无效scope"); - } - if(SaFoxUtil.isEmpty(redirectUri)) { - throw new SaTokenException("无效redirect_uri"); - } - if(SaFoxUtil.isEmpty(String.valueOf(loginId))) { - throw new SaTokenException("无效LoginId"); - } - return this; - } - - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/SaClientModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/SaClientModel.java deleted file mode 100644 index 952f696c9..000000000 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/model/SaClientModel.java +++ /dev/null @@ -1,115 +0,0 @@ -package cn.dev33.satoken.oauth2.model; - -import java.io.Serializable; - -/** - * Client应用信息 Model - * @author kong - * - */ -public class SaClientModel implements Serializable { - - private static final long serialVersionUID = -6541180061782004705L; - - /** - * 应用id - */ - public String clientId; - - /** - * 应用秘钥 - */ - public String clientSecret; - - /** - * 应用签约的所有权限, 多个用逗号隔开 - */ - public String contractScope; - - /** - * 应用允许授权的所有URL, 多个用逗号隔开 - */ - public String allowUrl; - - public SaClientModel() { - - } - public SaClientModel(String clientId, String clientSecret, String contractScope, String allowUrl) { - super(); - this.clientId = clientId; - this.clientSecret = clientSecret; - this.contractScope = contractScope; - this.allowUrl = allowUrl; - } - - /** - * @return 应用id - */ - public String getClientId() { - return clientId; - } - - /** - * @param clientId 应用id - * @return 对象自身 - */ - public SaClientModel setClientId(String clientId) { - this.clientId = clientId; - return this; - } - - /** - * @return 应用秘钥 - */ - public String getClientSecret() { - return clientSecret; - } - - /** - * @param clientSecret 应用秘钥 - * @return 对象自身 - */ - public SaClientModel setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - return this; - } - - /** - * @return 应用签约的所有权限, 多个用逗号隔开 - */ - public String getContractScope() { - return contractScope; - } - - /** - * @param contractScope 应用签约的所有权限, 多个用逗号隔开 - * @return 对象自身 - */ - public SaClientModel setContractScope(String contractScope) { - this.contractScope = contractScope; - return this; - } - - /** - * @return 应用允许授权的所有URL, 多个用逗号隔开 - */ - public String getAllowUrl() { - return allowUrl; - } - - /** - * @param allowUrl 应用允许授权的所有URL, 多个用逗号隔开 - * @return 对象自身 - */ - public SaClientModel setAllowUrl(String allowUrl) { - this.allowUrl = allowUrl; - return this; - } - - @Override - public String toString() { - return "SaClientModel [clientId=" + clientId + ", clientSecret=" + clientSecret + ", contractScope=" - + contractScope + ", allowUrl=" + allowUrl + "]"; - } - -} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java new file mode 100644 index 000000000..4a6039f79 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java @@ -0,0 +1,395 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.processor; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.consts.GrantType; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Api; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Param; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.ResponseType; +import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +import cn.dev33.satoken.oauth2.template.SaOAuth2Template; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.util.SaResult; + +import java.util.List; + +/** + * Sa-Token OAuth2 请求处理器 + * + * @author click33 + * @since 1.23.0 + */ +public class SaOAuth2ServerProcessor { + + /** + * 全局默认实例 + */ + public static SaOAuth2ServerProcessor instance = new SaOAuth2ServerProcessor(); + + /** + * 处理 Server 端请求, 路由分发 + * @return 处理结果 + */ + public Object dister() { + + // 获取变量 + SaRequest req = SaHolder.getRequest(); + + // ------------------ 路由分发 ------------------ + + // 模式一:Code授权码 || 模式二:隐藏式 + if(req.isPath(Api.authorize)) { + return authorize(); + } + + // Code 换 Access-Token || 模式三:密码式 + if(req.isPath(Api.token)) { + return token(); + } + + // Refresh-Token 刷新 Access-Token + if(req.isPath(Api.refresh)) { + return refresh(); + } + + // 回收 Access-Token + if(req.isPath(Api.revoke)) { + return revoke(); + } + + // doLogin 登录接口 + if(req.isPath(Api.doLogin)) { + return doLogin(); + } + + // doConfirm 确认授权接口 + if(req.isPath(Api.doConfirm)) { + return doConfirm(); + } + + // 模式四:凭证式 + if(req.isPath(Api.client_token)) { + return clientToken(); + } + + // 默认返回 + return SaOAuth2Consts.NOT_HANDLE; + } + + /** + * 模式一:Code授权码 / 模式二:隐藏式 + * @return 处理结果 + */ + public Object authorize() { + + // 获取变量 + SaRequest req = SaHolder.getRequest(); + SaResponse res = SaHolder.getResponse(); + SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); + SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); + SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); + String responseType = req.getParamNotNull(Param.response_type); + + // 1、先判断是否开启了指定的授权模式 + checkAuthorizeResponseType(responseType, req, cfg); + + // 2、如果尚未登录, 则先去登录 + Object loginId = SaOAuth2Manager.getStpLogic().getLoginIdDefaultNull(); + if( loginId == null) { + return SaOAuth2Strategy.instance.notLoginView.get(); + } + + // 3、构建请求 Model + RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId); + + // 4、开发者自定义的授权前置检查 + SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId); + + // 5、校验:重定向域名是否合法 + oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri); + + // 6、校验:此次申请的Scope,该Client是否已经签约 + oauth2Template.checkContractScope(ra.clientId, ra.scopes); + + // 7、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面 + boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes); + if(isNeedCarefulConfirm) { + SaClientModel cm = oauth2Template.checkClientModel(ra.clientId); + if( ! cm.getIsAutoConfirm()) { + return SaOAuth2Strategy.instance.confirmView.apply(ra.clientId, ra.scopes); + } + } + + // 8、判断授权类型,重定向到不同地址 + // 如果是 授权码式,则:开始重定向授权,下放code + if(ResponseType.code.equals(ra.responseType)) { + CodeModel codeModel = dataGenerate.generateCode(ra); + String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); + return res.redirect(redirectUri); + } + + // 如果是 隐藏式,则:开始重定向授权,下放 token + if(ResponseType.token.equals(ra.responseType)) { + AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null); + String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); + return res.redirect(redirectUri); + } + + // 默认返回 + throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125); + } + + /** + * Code 换 Access-Token / 模式三:密码式 / 自定义 grant_type + * @return 处理结果 + */ + public Object token() { + AccessTokenModel accessTokenModel = SaOAuth2Strategy.instance.grantTypeAuth.apply(SaHolder.getRequest()); + return SaOAuth2Manager.getDataResolver().buildAccessTokenReturnValue(accessTokenModel); + } + + /** + * Refresh-Token 刷新 Access-Token + * @return 处理结果 + */ + public Object refresh() { + SaRequest req = SaHolder.getRequest(); + + // 校验 grant_type 必须为 refresh_token + String grantType = req.getParamNotNull(Param.grant_type); + SaOAuth2Exception.throwBy(!grantType.equals(GrantType.refresh_token), "无效 grant_type:" + grantType, SaOAuth2ErrorCode.CODE_30126); + + // 刷新 Access-Token + AccessTokenModel accessTokenModel = SaOAuth2Strategy.instance.grantTypeAuth.apply(req); + return SaOAuth2Manager.getDataResolver().buildRefreshTokenReturnValue(accessTokenModel); + } + + /** + * 回收 Access-Token + * @return 处理结果 + */ + public Object revoke() { + // 获取变量 + SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); + SaRequest req = SaHolder.getRequest(); + + // 获取参数 + ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); + String clientId = clientIdAndSecret.clientId; + String clientSecret = clientIdAndSecret.clientSecret; + String accessToken = req.getParamNotNull(Param.access_token); + + // 如果 Access-Token 不存在,直接返回 + if(oauth2Template.getAccessToken(accessToken) == null) { + return SaResult.ok("access_token 不存在:" + accessToken); + } + + // 校验参数 + oauth2Template.checkAccessTokenParam(clientId, clientSecret, accessToken); + + // 回收 Access-Token + oauth2Template.revokeAccessToken(accessToken); + + // 返回 + return SaOAuth2Manager.getDataResolver().buildRevokeTokenReturnValue(); + } + + /** + * doLogin 登录接口 + * @return 处理结果 + */ + public Object doLogin() { + SaRequest req = SaHolder.getRequest(); + return SaOAuth2Strategy.instance.doLoginHandle.apply(req.getParam(Param.name), req.getParam(Param.pwd)); + } + + /** + * doConfirm 确认授权接口 + * @return 处理结果 + */ + public Object doConfirm() { + // 获取变量 + SaRequest req = SaHolder.getRequest(); + String clientId = req.getParamNotNull(Param.client_id); + Object loginId = SaOAuth2Manager.getStpLogic().getLoginId(); + String scope = req.getParamNotNull(Param.scope); + List scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(scope); + SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); + SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); + + // 此请求只允许 POST 方式 + if(!req.isMethod(SaHttpMethod.POST)) { + throw new SaOAuth2Exception("无效请求方式:" + req.getMethod()).setCode(SaOAuth2ErrorCode.CODE_30151); + } + + // 确认授权 + oauth2Template.saveGrantScope(clientId, loginId, scopes); + + // 判断所需的返回结果模式 + boolean buildRedirectUri = req.isParam(Param.build_redirect_uri, "true"); + + // -------- 情况1:只返回确认结果即可 + if( ! buildRedirectUri ) { + oauth2Template.saveGrantScope(clientId, loginId, scopes); + return SaResult.ok(); + } + + // -------- 情况2:需要返回最终的 redirect_uri 地址 + + // 构建请求 Model + RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId); + + // 判断授权类型,构建不同的重定向地址 + // 如果是 授权码式,则:开始重定向授权,下放code + if(ResponseType.code.equals(ra.responseType)) { + CodeModel codeModel = dataGenerate.generateCode(ra); + String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); + return SaResult.ok().set(Param.redirect_uri, redirectUri); + } + + // 如果是 隐藏式,则:开始重定向授权,下放 token + if(ResponseType.token.equals(ra.responseType)) { + AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null); + String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); + return SaResult.ok().set(Param.redirect_uri, redirectUri); + } + + // 默认返回 + throw new SaOAuth2Exception("无效response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125); + } + + /** + * 模式四:凭证式 + * @return 处理结果 + */ + public Object clientToken() { + // 获取变量 + SaRequest req = SaHolder.getRequest(); + SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); + SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); + + String grantType = req.getParamNotNull(Param.grant_type); + if(!grantType.equals(GrantType.client_credentials)) { + throw new SaOAuth2Exception("无效 grant_type:" + grantType).setCode(SaOAuth2ErrorCode.CODE_30126); + } + if(!cfg.enableClientCredentials) { + throwErrorSystemNotEnableModel(); + } + if(!currClientModel().getAllowGrantTypes().contains(GrantType.client_credentials)) { + throwErrorClientNotEnableModel(); + } + + // 获取参数 + ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); + String clientId = clientIdAndSecret.clientId; + String clientSecret = clientIdAndSecret.clientSecret; + List scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(Param.scope)); + + // 校验 ClientScope + oauth2Template.checkContractScope(clientId, scopes); + + // 校验 ClientSecret + oauth2Template.checkClientSecret(clientId, clientSecret); + + // 生成 + ClientTokenModel ct = SaOAuth2Manager.getDataGenerate().generateClientToken(clientId, scopes); + + // 返回 + return SaOAuth2Manager.getDataResolver().buildClientTokenReturnValue(ct); + } + + + // ----------- 代码块封装 -------------- + + /** + * 根据当前请求提交的 client_id 参数获取 SaClientModel 对象 + * @return / + */ + public SaClientModel currClientModel() { + SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); + ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(SaHolder.getRequest()); + return oauth2Template.checkClientModel(clientIdAndSecret.clientId); + } + + /** + * 校验当前请求中提交的 clientId 和 clientSecret 是否正确,如果正确则返回 SaClientModel 对象 + * + * @return / + */ + public SaClientModel checkCurrClientSecret() { + SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); + ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(SaHolder.getRequest()); + return oauth2Template.checkClientSecret(clientIdAndSecret.clientId, clientIdAndSecret.clientSecret); + } + + /** + * 校验 authorize 路由的 ResponseType 参数 + */ + public void checkAuthorizeResponseType(String responseType, SaRequest req, SaOAuth2ServerConfig cfg) { + // 模式一:Code授权码 + if(responseType.equals(ResponseType.code)) { + if(!cfg.enableAuthorizationCode) { + throwErrorSystemNotEnableModel(); + } + if(!currClientModel().getAllowGrantTypes().contains(GrantType.authorization_code)) { + throwErrorClientNotEnableModel(); + } + } + // 模式二:隐藏式 + else if(responseType.equals(ResponseType.token)) { + if(!cfg.enableImplicit) { + throwErrorSystemNotEnableModel(); + } + if(!currClientModel().getAllowGrantTypes().contains(GrantType.implicit)) { + throwErrorClientNotEnableModel(); + } + } + // 其它 + else { + throw new SaOAuth2Exception("无效 response_type: " + responseType).setCode(SaOAuth2ErrorCode.CODE_30125); + } + } + + /** + * 系统未开放此授权模式时抛出异常 + */ + public void throwErrorSystemNotEnableModel() { + throw new SaOAuth2Exception("系统暂未开放此授权模式").setCode(SaOAuth2ErrorCode.CODE_30141); + } + + /** + * 应用未开放此授权模式时抛出异常 + */ + public void throwErrorClientNotEnableModel() { + throw new SaOAuth2Exception("应用暂未开放此授权模式").setCode(SaOAuth2ErrorCode.CODE_30142); + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/CommonScope.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/CommonScope.java new file mode 100644 index 000000000..425cb3d57 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/CommonScope.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.scope; + +/** + * OAuth2 常见 Scope 定义 + * + * @author click33 + * @since 1.39.0 + */ +public final class CommonScope { + + private CommonScope() { + } + + /** + * 获取 openid + */ + public static final String OPENID = "openid"; + + /** + * 获取 unionid + */ + public static final String UNIONID = "unionid"; + + /** + * 获取 userid + */ + public static final String USERID = "userid"; + + /** + * 获取 id_token + */ + public static final String OIDC = "oidc"; + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java new file mode 100644 index 000000000..5ed1b24aa --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java @@ -0,0 +1,183 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.scope.handler; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.jwt.SaJwtUtil; +import cn.dev33.satoken.jwt.error.SaJwtErrorCode; +import cn.dev33.satoken.jwt.exception.SaJwtException; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.oidc.IdTokenModel; +import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.oauth2.scope.CommonScope; +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * id_token 权限处理器:在 AccessToken 扩展参数中追加 id_token 字段 + * + * @author click33 + * @since 1.39.0 + */ +public class OidcScopeHandler implements SaOAuth2ScopeHandlerInterface { + + public String getHandlerScope() { + return CommonScope.OIDC; + } + + @Override + public void workAccessToken(AccessTokenModel at) { + SaRequest req = SaHolder.getRequest(); + ClientIdAndSecretModel client = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); + + // 基础参数 + IdTokenModel idToken = new IdTokenModel(); + idToken.iss = getIss(); + idToken.sub = at.loginId; + idToken.aud = client.clientId; + idToken.iat = System.currentTimeMillis() / 1000; + idToken.exp = idToken.iat + SaOAuth2Manager.getServerConfig().getOidc().getIdTokenTimeout(); + idToken.authTime = SaOAuth2Manager.getStpLogic().getSessionByLoginId(at.loginId).getCreateTime() / 1000; + idToken.nonce = getNonce(); + idToken.acr = null; + idToken.amr = null; + idToken.azp = client.clientId; + + // 额外参数 + idToken.extraData = new LinkedHashMap<>(); + idToken = workExtraData(idToken); + + // 构建 jwtIdToken + String jwtIdToken = generateJwtIdToken(idToken); + + // 放入 AccessTokenModel + at.extraData.put("id_token", jwtIdToken); + } + + @Override + public void workClientToken(ClientTokenModel ct) { + + } + + @Override + public boolean refreshAccessTokenIsWork() { + return true; + } + + /** + * 获取 iss + * @return / + */ + public String getIss() { + // 如果开发者配置了 iss,则使用开发者配置的 iss + String cfgIss = SaOAuth2Manager.getServerConfig().getOidc().getIss(); + if(SaFoxUtil.isNotEmpty(cfgIss)) { + return cfgIss; + } + // 否则根据请求的 url 计算 iss + // 例如请求 url 为: http://localhost:8081/abc/xyz?name=张三 + // 则计算的 iss 为: http://localhost:8081 + String urlString = SaHolder.getRequest().getUrl(); + try { + URL url = new URL(urlString); + String iss = url.getProtocol() + "://" + url.getHost(); + if(url.getPort() != -1) { + iss += ":" + url.getPort(); + } + return iss; + } catch (MalformedURLException e) { + throw new SaOAuth2Exception(e); + } + } + + /** + * 获取 nonce + * @return / + */ + public String getNonce() { + String nonce = SaHolder.getRequest().getParam(SaOAuth2Consts.Param.nonce); + if(SaFoxUtil.isEmpty(nonce)) { + // 通过 code 查找nonce + // 为了避免其它 handler 可能会用到 nonce, 任由其自然过期,只取用不删除 + nonce = SaOAuth2Manager.getDao().getNonce(SaHolder.getRequest().getParam(SaOAuth2Consts.Param.code)); + } + if(SaFoxUtil.isEmpty(nonce)) { + nonce = SaFoxUtil.getRandomString(32); + } + SaSignManager.getSaSignTemplate().checkNonce(nonce); + return nonce; + } + + /** + * 加工 IdTokenModel + * @return / + */ + public IdTokenModel workExtraData(IdTokenModel idToken) { + // 留给开发者扩展 + return idToken; + } + + /** + * 将 IdTokenModel 转化为 Map 数据 + * @return / + */ + public Map convertIdTokenToMap(IdTokenModel idToken) { + // 基础参数 + Map map = new LinkedHashMap<>(); + map.put("iss", idToken.iss); + map.put("sub", idToken.sub); + map.put("aud", idToken.aud); + map.put("exp", idToken.exp); + map.put("iat", idToken.iat); + map.put("auth_time", idToken.authTime); + map.put("nonce", idToken.nonce); + map.put("acr", idToken.acr); + map.put("amr", idToken.amr); + map.put("azp", idToken.azp); + + // 移除 null 值 + idToken.extraData.entrySet().removeIf(entry -> entry.getValue() == null); + + // 扩展参数 + map.putAll(idToken.extraData); + + // 返回 + return map; + } + + /** + * 生成 jwt 格式的 id_token + * @param idToken / + * @return / + */ + public String generateJwtIdToken(IdTokenModel idToken) { + Map dataMap = convertIdTokenToMap(idToken); + String keyt = SaOAuth2Manager.getStpLogic().getConfigOrGlobal().getJwtSecretKey(); + SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); + return SaJwtUtil.createToken(dataMap, keyt); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OpenIdScopeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OpenIdScopeHandler.java new file mode 100644 index 000000000..d4aee79f0 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OpenIdScopeHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.scope.handler; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.scope.CommonScope; + +/** + * OpenId 权限处理器:在 AccessToken 扩展参数中追加 openid 字段 + * + * @author click33 + * @since 1.39.0 + */ +public class OpenIdScopeHandler implements SaOAuth2ScopeHandlerInterface { + + public String getHandlerScope() { + return CommonScope.OPENID; + } + + @Override + public void workAccessToken(AccessTokenModel at) { + at.extraData.put(SaOAuth2Consts.ExtraField.openid, SaOAuth2Manager.getDataLoader().getOpenid(at.clientId, at.loginId)); + } + + @Override + public void workClientToken(ClientTokenModel ct) { + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/SaOAuth2ScopeHandlerInterface.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/SaOAuth2ScopeHandlerInterface.java new file mode 100644 index 000000000..31be52ba5 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/SaOAuth2ScopeHandlerInterface.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.scope.handler; + +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; + +/** + * 所有 OAuth2 权限处理器的父接口,如果要自定义 Scope 处理器,必须实现此接口 + * + * @author click33 + * @since 1.39.0 + */ +public interface SaOAuth2ScopeHandlerInterface { + + /** + * 获取所要处理的权限 + * + * @return / + */ + String getHandlerScope(); + + /** + * 当构建的 AccessToken 具有此权限时,所需要执行的方法 + * + * @param at / + */ + void workAccessToken(AccessTokenModel at); + + /** + * 当构建的 ClientToken 具有此权限时,所需要执行的方法 + * + * @param ct / + */ + void workClientToken(ClientTokenModel ct); + + /** + * 当使用 RefreshToken 刷新 AccessToken 时,是否重新执行 workAccessToken 构建方法 + * + * @return / + */ + default boolean refreshAccessTokenIsWork() { + return false; + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UnionIdScopeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UnionIdScopeHandler.java new file mode 100644 index 000000000..3bebfc310 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UnionIdScopeHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.scope.handler; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.scope.CommonScope; + +/** + * UnionId Scope 处理器,在返回的 AccessToken 中增加 unionid 字段 + * + * @author click33 + * @since 1.40.0 + */ +public class UnionIdScopeHandler implements SaOAuth2ScopeHandlerInterface { + + @Override + public String getHandlerScope() { + return CommonScope.UNIONID; + } + + @Override + public void workAccessToken(AccessTokenModel at) { + SaOAuth2DataLoader dataLoader = SaOAuth2Manager.getDataLoader(); + SaClientModel cm = dataLoader.getClientModelNotNull(at.clientId); + at.extraData.put(SaOAuth2Consts.ExtraField.unionid, dataLoader.getUnionid(cm.subjectId, at.loginId)); + } + + @Override + public void workClientToken(ClientTokenModel ct) { + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UserIdScopeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UserIdScopeHandler.java new file mode 100644 index 000000000..f9e65f0f6 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UserIdScopeHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.scope.handler; + +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.scope.CommonScope; + +/** + * UserId 权限处理器:在 AccessToken 扩展参数中追加 userid 字段 + * + * @author click33 + * @since 1.39.0 + */ +public class UserIdScopeHandler implements SaOAuth2ScopeHandlerInterface { + + public String getHandlerScope() { + return CommonScope.USERID; + } + + @Override + public void workAccessToken(AccessTokenModel at) { + at.extraData.put(SaOAuth2Consts.ExtraField.userid, at.loginId); + } + + @Override + public void workClientToken(ClientTokenModel ct) { + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java new file mode 100644 index 000000000..270deb2d4 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java @@ -0,0 +1,290 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.strategy; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.fun.SaTwoParamFunction; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.consts.GrantType; +import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; +import cn.dev33.satoken.oauth2.function.SaOAuth2ConfirmViewFunction; +import cn.dev33.satoken.oauth2.function.SaOAuth2DoLoginHandleFunction; +import cn.dev33.satoken.oauth2.function.SaOAuth2NotLoginViewFunction; +import cn.dev33.satoken.oauth2.function.strategy.*; +import cn.dev33.satoken.oauth2.granttype.handler.AuthorizationCodeGrantTypeHandler; +import cn.dev33.satoken.oauth2.granttype.handler.PasswordGrantTypeHandler; +import cn.dev33.satoken.oauth2.granttype.handler.RefreshTokenGrantTypeHandler; +import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; +import cn.dev33.satoken.oauth2.scope.CommonScope; +import cn.dev33.satoken.oauth2.scope.handler.*; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Sa-Token OAuth2 相关策略 + * + * @author click33 + * @since 1.39.0 + */ +public final class SaOAuth2Strategy { + + private SaOAuth2Strategy() { + registerDefaultScopeHandler(); + registerDefaultGrantTypeHandler(); + } + + /** + * 全局单例引用 + */ + public static final SaOAuth2Strategy instance = new SaOAuth2Strategy(); + + + // ------------------ 权限处理器 ------------------ + + /** + * Scope 权限处理器集合 + */ + public Map scopeHandlerMap = new LinkedHashMap<>(); + + /** + * 注册所有默认的 Scope 权限处理器 + */ + public void registerDefaultScopeHandler() { + scopeHandlerMap.put(CommonScope.OPENID, new OpenIdScopeHandler()); + scopeHandlerMap.put(CommonScope.UNIONID, new UnionIdScopeHandler()); + scopeHandlerMap.put(CommonScope.USERID, new UserIdScopeHandler()); + scopeHandlerMap.put(CommonScope.OIDC, new OidcScopeHandler()); + } + + /** + * 注册一个权限处理器 + */ + public void registerScopeHandler(SaOAuth2ScopeHandlerInterface handler) { + scopeHandlerMap.put(handler.getHandlerScope(), handler); + SaManager.getLog().info("自定义 SCOPE [{}] (处理器: {})", handler.getHandlerScope(), handler.getClass().getCanonicalName()); + } + + /** + * 移除一个权限处理器 + */ + public void removeScopeHandler(String scope) { + scopeHandlerMap.remove(scope); + } + + /** + * 根据 scope 信息对一个 AccessTokenModel 进行加工处理 + */ + public SaOAuth2ScopeWorkAccessTokenFunction workAccessTokenByScope = (at) -> { + if(at.scopes != null && !at.scopes.isEmpty()) { + for (String scope : at.scopes) { + SaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope); + if(handler != null) { + handler.workAccessToken(at); + } + } + } + SaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE); + if(finallyWorkScopeHandler != null) { + finallyWorkScopeHandler.workAccessToken(at); + } + }; + + /** + * 当使用 RefreshToken 刷新 AccessToken 时,根据 scope 信息对一个 AccessTokenModel 进行加工处理 + */ + public SaOAuth2ScopeWorkAccessTokenFunction refreshAccessTokenWorkByScope = (at) -> { + if(at.scopes != null && !at.scopes.isEmpty()) { + for (String scope : at.scopes) { + SaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope); + if(handler != null && handler.refreshAccessTokenIsWork()) { + handler.workAccessToken(at); + } + } + } + SaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE); + if(finallyWorkScopeHandler != null && finallyWorkScopeHandler.refreshAccessTokenIsWork()) { + finallyWorkScopeHandler.workAccessToken(at); + } + }; + + /** + * 根据 scope 信息对一个 ClientTokenModel 进行加工处理 + */ + public SaOAuth2ScopeWorkClientTokenFunction workClientTokenByScope = (ct) -> { + if(ct.scopes != null && !ct.scopes.isEmpty()) { + for (String scope : ct.scopes) { + SaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope); + if(handler != null) { + handler.workClientToken(ct); + } + } + } + SaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE); + if(finallyWorkScopeHandler != null) { + finallyWorkScopeHandler.workClientToken(ct); + } + }; + + + // ------------------ grant_type 处理器 ------------------ + + /** + * grant_type 处理器集合 + */ + public Map grantTypeHandlerMap = new LinkedHashMap<>(); + + /** + * 注册所有默认的 grant_type 处理器 + */ + public void registerDefaultGrantTypeHandler() { + grantTypeHandlerMap.put(GrantType.authorization_code, new AuthorizationCodeGrantTypeHandler()); + grantTypeHandlerMap.put(GrantType.password, new PasswordGrantTypeHandler()); + grantTypeHandlerMap.put(GrantType.refresh_token, new RefreshTokenGrantTypeHandler()); + } + + /** + * 注册一个 grant_type 处理器 + */ + public void registerGrantTypeHandler(SaOAuth2GrantTypeHandlerInterface handler) { + grantTypeHandlerMap.put(handler.getHandlerGrantType(), handler); + SaManager.getLog().info("自定义 GRANT_TYPE [{}] (处理器: {})", handler.getHandlerGrantType(), handler.getClass().getCanonicalName()); + } + + /** + * 移除一个 grant_type 处理器 + */ + public void removeGrantTypeHandler(String scope) { + grantTypeHandlerMap.remove(scope); + } + + /** + * 根据 grantType 构造一个 AccessTokenModel + */ + public SaOAuth2GrantTypeAuthFunction grantTypeAuth = (req) -> { + // 先校验提供的 grant_type 是否有效 + String grantType = req.getParamNotNull(SaOAuth2Consts.Param.grant_type); + SaOAuth2GrantTypeHandlerInterface grantTypeHandler = grantTypeHandlerMap.get(grantType); + if(grantTypeHandler == null) { + throw new SaOAuth2Exception("无效 grant_type: " + grantType).setCode(SaOAuth2ErrorCode.CODE_30126); + } + + // 针对 authorization_code 与 password 两种特殊 grant_type,需要判断全局是否开启 + SaOAuth2ServerConfig config = SaOAuth2Manager.getServerConfig(); + if(grantType.equals(GrantType.authorization_code) && !config.getEnableAuthorizationCode() ) { + throw new SaOAuth2Exception("系统未开放的 grant_type: " + grantType).setCode(SaOAuth2ErrorCode.CODE_30126); + } + if(grantType.equals(GrantType.password) && !config.getEnablePassword() ) { + throw new SaOAuth2Exception("系统未开放的 grant_type: " + grantType).setCode(SaOAuth2ErrorCode.CODE_30126); + } + + // 校验 clientSecret 和 scope + ClientIdAndSecretModel clientIdAndSecretModel = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); + List scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(SaOAuth2Consts.Param.scope)); + SaClientModel clientModel = SaOAuth2Manager.getTemplate().checkClientSecretAndScope(clientIdAndSecretModel.getClientId(), clientIdAndSecretModel.getClientSecret(), scopes); + + // 检测应用是否开启此 grantType + if(!clientModel.getAllowGrantTypes().contains(grantType)) { + throw new SaOAuth2Exception("应用未开放的 grant_type: " + grantType).setCode(SaOAuth2ErrorCode.CODE_30141); + } + + // 调用 处理器构建 Access-Token + return grantTypeHandler.getAccessToken(req, clientIdAndSecretModel.getClientId(), scopes); + }; + + + // ------------------ 凭证创建 ------------------ + + /** + * 创建一个 code value + */ + public SaOAuth2CreateCodeValueFunction createCodeValue = (clientId, loginId, scopes) -> { + return SaFoxUtil.getRandomString(60); + }; + + /** + * 创建一个 AccessToken value + */ + public SaOAuth2CreateAccessTokenValueFunction createAccessToken = (clientId, loginId, scopes) -> { + return SaFoxUtil.getRandomString(60); + }; + + /** + * 创建一个 RefreshToken value + */ + public SaOAuth2CreateRefreshTokenValueFunction createRefreshToken = (clientId, loginId, scopes) -> { + return SaFoxUtil.getRandomString(60); + }; + + /** + * 创建一个 ClientToken value + */ + public SaOAuth2CreateClientTokenValueFunction createClientToken = (clientId, scopes) -> { + return SaFoxUtil.getRandomString(60); + }; + + + // ------------------ 认证流程回调 ------------------ + + /** + * OAuth-Server端:未登录时返回的View + */ + public SaOAuth2NotLoginViewFunction notLoginView = () -> "当前会话在 OAuth-Server 认证中心尚未登录"; + + /** + * OAuth-Server端:确认授权时返回的View + */ + public SaOAuth2ConfirmViewFunction confirmView = (clientId, scopes) -> "本次操作需要用户授权"; + + /** + * OAuth-Server端:登录函数 + */ + public SaOAuth2DoLoginHandleFunction doLoginHandle = (name, pwd) -> SaResult.error(); + + /** + * OAuth-Server端:用户在授权指定 client 前的检查,如果检查不通过,请直接抛出异常 + */ + public SaTwoParamFunction userAuthorizeClientCheck = (loginId, clientId) -> { + + }; + + + // ------------------ 其它 ------------------ + + /** + * 在创建 SaClientModel 时,设置其默认字段 + */ + public SaParamFunction setSaClientModelDefaultFields = (clientModel) -> { + SaOAuth2ServerConfig config = SaOAuth2Manager.getServerConfig(); + clientModel.accessTokenTimeout = config.getAccessTokenTimeout(); + clientModel.refreshTokenTimeout = config.getRefreshTokenTimeout(); + clientModel.clientTokenTimeout = config.getClientTokenTimeout(); + clientModel.maxAccessTokenCount = config.getMaxAccessTokenCount(); + clientModel.maxRefreshTokenCount = config.getMaxRefreshTokenCount(); + clientModel.maxClientTokenCount = config.getMaxClientTokenCount(); + clientModel.isNewRefresh = config.getIsNewRefresh(); + }; + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java new file mode 100644 index 000000000..c2cc29d5b --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java @@ -0,0 +1,779 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.template; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; +import cn.dev33.satoken.oauth2.exception.*; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.List; + +/** + * Sa-Token-OAuth2 模块 代码实现 + * + * @author click33 + * @since 1.23.0 + */ +public class SaOAuth2Template { + + // ----------------- SaClientModel 相关 ----------------- + + /** + * 获取 ClientModel,根据 clientId + * + * @param clientId / + * @return / + */ + public SaClientModel getClientModel(String clientId) { + return SaOAuth2Manager.getDataLoader().getClientModel(clientId); + } + + /** + * 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 + * @param clientId / + * @return / + */ + public SaClientModel checkClientModel(String clientId) { + SaClientModel clientModel = getClientModel(clientId); + if(clientModel == null) { + throw new SaOAuth2ClientModelException("无效 client_id: " + clientId) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30105); + } + return clientModel; + } + + /** + * 校验:clientId 与 clientSecret 是否正确,正确返回 SaClientModel,不正确抛出异常 + * @param clientId 应用id + * @param clientSecret 秘钥 + * @return SaClientModel对象 + */ + public SaClientModel checkClientSecret(String clientId, String clientSecret) { + SaClientModel cm = checkClientModel(clientId); + if(cm.clientSecret == null || ! cm.clientSecret.equals(clientSecret)) { + throw new SaOAuth2ClientModelException("无效 client_secret: " + clientSecret) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30115); + } + return cm; + } + + /** + * 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes + * @param clientId 应用id + * @param clientSecret 秘钥 + * @param scopes 权限 + * @return SaClientModel对象 + */ + public SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List scopes) { + SaClientModel cm = checkClientSecret(clientId, clientSecret); + checkContractScope(cm, scopes); + return cm; + } + + /** + * 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false + * @param clientId 应用id + * @param scopes 权限 + * @return / + */ + public boolean isContractScope(String clientId, List scopes) { + try { + checkContractScope(clientId, scopes); + return true; + } catch (SaOAuth2ClientModelException e) { + return false; + } + } + + /** + * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 + * @param clientId 应用id + * @param scopes 权限列表 + * @return / + */ + public SaClientModel checkContractScope(String clientId, List scopes) { + return checkContractScope(checkClientModel(clientId), scopes); + } + + /** + * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 + * @param cm 应用 + * @param scopes 权限列表 + * @return / + */ + public SaClientModel checkContractScope(SaClientModel cm, List scopes) { + if(SaFoxUtil.isEmptyList(scopes)) { + return cm; + } + for (String scope : scopes) { + if(! cm.contractScopes.contains(scope)) { + throw new SaOAuth2ClientModelScopeException("该 client 暂未签约 scope: " + scope) + .setClientId(cm.clientId) + .setScope(scope) + .setCode(SaOAuth2ErrorCode.CODE_30112); + } + } + return cm; + } + + // --------- redirect_uri 相关 + + /** + * 校验:该 Client 使用指定 url 作为回调地址,是否合法 + * @param clientId 应用id + * @param url 指定url + */ + public void checkRedirectUri(String clientId, String url) { + // 1、是否是一个有效的url + if( ! SaFoxUtil.isUrl(url)) { + throw new SaOAuth2ClientModelException("无效 redirect_url:" + url) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30113); + } + + // 2、截取掉?后面的部分 + int qIndex = url.indexOf("?"); + if(qIndex != -1) { + url = url.substring(0, qIndex); + } + + // 3、不允许出现@字符 + if(url.contains("@")) { + // 为什么不允许出现 @ 字符呢,因为这有可能导致 redirect_url 参数绕过 AllowUrl 列表的校验 + // + // 举个例子 SaClientModel 配置: + // allow-url=http://sa-oauth-client.com* + // + // 开发者原意是为了允许 sa-oauth-client.com 下的所有地址都可以下放 code + // + // 但是如果攻击者精心构建一个url: + // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com@sa-token.com + // + // 那么这个url就会绕过 allow-url 的校验,code 被下发到了第三方服务器地址: + // http://sa-token.com/?code=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul + // + // 造成了 code 参数劫持 + // 所以此处需要禁止在 url 中出现 @ 字符 + // + // 这么一刀切的做法,可能会导致一些特殊的正常url也无法通过校验,例如: + // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com/@getInfo + // + // 但是为了安全起见,这么做还是有必要的 + throw new SaOAuth2ClientModelException("无效 redirect_url(不允许出现@字符):" + url) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30113); + } + + // 4、是否在[允许地址列表]之中 + SaClientModel clientModel = checkClientModel(clientId); + checkRedirectUriListNormal(clientModel.allowRedirectUris); + if( ! SaStrategy.instance.hasElement.apply(clientModel.allowRedirectUris, url)) { + throw new SaOAuth2ClientModelException("非法 redirect_url: " + url) + .setClientId(clientId) + .setCode(SaOAuth2ErrorCode.CODE_30114); + } + } + + /** + * 校验配置的 allowRedirectUris 是否合规,如果不合规则抛出异常 + * @param redirectUriList 待校验的 allow-url 地址列表 + */ + public void checkRedirectUriListNormal(List redirectUriList){ + checkRedirectUriListNormalStaticMethod(redirectUriList); + } + + /** + * 校验配置的 allowRedirectUris 是否合规,如果不合规则抛出异常,静态方法内部实现 + * @param redirectUriList 待校验的 allow-url 地址列表 + */ + public static void checkRedirectUriListNormalStaticMethod(List redirectUriList){ + for (String url : redirectUriList) { + int index = url.indexOf("*"); + // 如果配置了 * 字符,则必须出现在最后一位,否则属于无效配置项 + if(index != -1 && index != url.length() - 1) { + // 为什么不允许 * 字符出现在中间位置呢,因为这有可能导致 redirect 参数绕过 allow-url 列表的校验 + // + // 举个例子 SaClientModel 配置: + // allow-url=http://*.sa-oauth-client.com/ + // + // 开发者原意是为了允许 sa-oauth-client.com 下的所有子域名都可以下放 ticket + // 例如:http://shop.sa-oauth-client.com/ + // + // 但是如果攻击者精心构建一个url: + // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-token.com/a.sa-oauth-client.com/ + // + // 那么这个 url 就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: + // http://sa-token.com/a.sa-oauth-client.com/?code=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx + // + // 造成了 ticket 参数劫持 + // 所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符(出现在末尾是没有问题的) + // + // 这么一刀切的做法,可能会导致正常场景下的子域名url也无法通过校验,例如: + // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://shop.sa-oauth2-client.com/ + // + // 但是为了安全起见,这么做还是有必要的 + throw new SaOAuth2Exception("无效的 allow-url 配置(*通配符只允许出现在最后一位):" + url) + .setCode(SaOAuth2ErrorCode.CODE_30114); + } + } + } + + // --------- 授权相关 + + /** + * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope + * @param loginId 账号id + * @param clientId 应用id + * @param scopes 权限 + * @return 是否已经授权 + */ + public boolean isGrantScope(Object loginId, String clientId, List scopes) { + List grantScopeList = SaOAuth2Manager.getDao().getGrantScope(clientId, loginId); + return SaFoxUtil.list1ContainList2AllElement(grantScopeList, scopes); + } + + /** + * 判断:指定 loginId 在指定 Client 请求指定 Scope 时,是否需要手动确认授权 + * @param loginId 账号id + * @param clientId 应用id + * @param scopes 权限 + * @return 是否已经授权 + */ + public boolean isNeedCarefulConfirm(Object loginId, String clientId, List scopes) { + // 如果请求的权限为空,则不需要确认 + if(scopes == null || scopes.isEmpty()) { + return false; + } + + // 如果包含高级权限,则必须手动确认授权 + List higherScopeList = getHigherScopeList(); + if(SaFoxUtil.list1ContainList2AnyElement(scopes, higherScopeList)) { + return true; + } + + // 如果包含低级权限,则先将低级权限剔除掉 + List lowerScopeList = getLowerScopeList(); + scopes = SaFoxUtil.list1RemoveByList2(scopes, lowerScopeList); + + // 如果剔除后的权限为空,则不需要确认 + if(scopes.isEmpty()) { + return false; + } + + // 根据近期授权记录,判断是否需要确认 + return !isGrantScope(loginId, clientId, scopes); + } + + /** + * 删除:指定 loginId 针对指定 Client 的授权信息 + * @param loginId 账号id + * @param clientId 应用id + */ + public void deleteGrantScope(Object loginId, String clientId) { + SaOAuth2Manager.getDao().deleteGrantScope(clientId, loginId); + } + + + // --------- 请求数据校验相关 + + /** + * 校验:使用 code 获取 token 时提供的参数校验 + * @param code 授权码 + * @param clientId 应用id + * @param clientSecret 秘钥 + * @param redirectUri 重定向地址 + * @return CodeModel对象 + */ + public CodeModel checkGainTokenParam(String code, String clientId, String clientSecret, String redirectUri) { + + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + // 校验:Code是否存在 + CodeModel cm = dao.getCode(code); + SaOAuth2AuthorizationCodeException.throwBy(cm == null, "无效 code: " + code, code, SaOAuth2ErrorCode.CODE_30110); + + // 校验:ClientId是否一致 + SaOAuth2ClientModelException.throwBy( ! cm.clientId.equals(clientId), "无效 client_id: " + clientId, clientId, SaOAuth2ErrorCode.CODE_30105); + + // 校验:Secret是否正确 + String dbSecret = checkClientModel(clientId).clientSecret; + SaOAuth2ClientModelException.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), "无效 client_secret: " + clientSecret, clientId, SaOAuth2ErrorCode.CODE_30115); + + // 如果提供了redirectUri,则校验其是否与请求Code时提供的一致 + if( ! SaFoxUtil.isEmpty(redirectUri)) { + SaOAuth2ClientModelException.throwBy( ! redirectUri.equals(cm.redirectUri), "无效 redirect_uri: " + redirectUri, clientId, SaOAuth2ErrorCode.CODE_30120); + } + + // 返回CodeModel + return cm; + } + + /** + * 校验:使用 Refresh-Token 刷新 Access-Token 时提供的参数校验 + * @param clientId 应用id + * @param clientSecret 秘钥 + * @param refreshToken Refresh-Token + * @return CodeModel对象 + */ + public RefreshTokenModel checkRefreshTokenParam(String clientId, String clientSecret, String refreshToken) { + + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + // 校验:Refresh-Token是否存在 + RefreshTokenModel rt = dao.getRefreshToken(refreshToken); + SaOAuth2RefreshTokenException.throwBy(rt == null, "无效 refresh_token: " + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111); + + // 校验:ClientId 是否一致 + SaOAuth2ClientModelException.throwBy( ! rt.clientId.equals(clientId), "无效 client_id: " + clientId, clientId, SaOAuth2ErrorCode.CODE_30122); + + // 校验:Secret 是否正确 + String dbSecret = checkClientModel(clientId).clientSecret; + SaOAuth2ClientModelException.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), "无效 client_secret: " + clientSecret, + clientId, SaOAuth2ErrorCode.CODE_30115); + + // 返回 Refresh-Token + return rt; + } + + /** + * 校验:Access-Token、clientId、clientSecret 三者是否匹配成功 + * @param clientId 应用id + * @param clientSecret 秘钥 + * @param accessToken Access-Token + * @return SaClientModel对象 + */ + public AccessTokenModel checkAccessTokenParam(String clientId, String clientSecret, String accessToken) { + AccessTokenModel at = checkAccessToken(accessToken); + SaOAuth2ClientModelException.throwBy( ! at.clientId.equals(clientId), "无效 client_id:" + clientId, clientId, SaOAuth2ErrorCode.CODE_30122); + checkClientSecret(clientId, clientSecret); + return at; + } + + + // ----------------- Code 相关 ----------------- + + /** + * 获取 CodeModel,无效的 code 会返回 null + * @param code / + * @return / + */ + public CodeModel getCode(String code) { + return SaOAuth2Manager.getDao().getCode(code); + } + + /** + * 校验 Code,成功返回 CodeModel,失败则抛出异常 + * @param code / + * @return / + */ + public CodeModel checkCode(String code) { + CodeModel cm = SaOAuth2Manager.getDao().getCode(code); + if(cm == null) { + throw new SaOAuth2AuthorizationCodeException("无效 code: " + code) + .setAuthorizationCode(code) + .setCode(SaOAuth2ErrorCode.CODE_30110); + } + return cm; + } + + /** + * 获取 Code,根据索引: clientId、loginId + * @param clientId / + * @param loginId / + * @return / + */ + public String getCodeValue(String clientId, Object loginId) { + return SaOAuth2Manager.getDao().getCodeValue(clientId, loginId); + } + + + // ----------------- Access-Token 相关 ----------------- + + /** + * 获取 AccessTokenModel,无效的 AccessToken 会返回 null + * @param accessToken / + * @return / + */ + public AccessTokenModel getAccessToken(String accessToken) { + return SaOAuth2Manager.getDao().getAccessToken(accessToken); + } + + /** + * 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 + * @param accessToken / + * @return / + */ + public AccessTokenModel checkAccessToken(String accessToken) { + AccessTokenModel at = SaOAuth2Manager.getDao().getAccessToken(accessToken); + if(at == null) { + throw new SaOAuth2AccessTokenException("无效 access_token: " + accessToken) + .setAccessToken(accessToken) + .setCode(SaOAuth2ErrorCode.CODE_30106); + } + return at; + } + + /** + * 获取 Access-Token 列表:此应用下 对 某个用户 签发的所有 Access-token + * + * @param clientId / + * @param loginId / + * @return / + */ + public List getAccessTokenValueList(String clientId, Object loginId) { + return SaOAuth2Manager.getDao().getAccessTokenValueList_FromAdjustAfter(clientId, loginId); + } + + /** + * 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false + * @param accessToken Access-Token + * @param scopes 需要校验的权限列表 + */ + public boolean hasAccessTokenScope(String accessToken, String... scopes) { + try { + checkAccessTokenScope(accessToken, scopes); + return true; + } catch (SaOAuth2AccessTokenException e) { + return false; + } + } + + /** + * 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 + * @param accessToken Access-Token + * @param scopes 需要校验的权限列表 + */ + public void checkAccessTokenScope(String accessToken, String... scopes) { + AccessTokenModel at = checkAccessToken(accessToken); + if(SaFoxUtil.isEmptyArray(scopes)) { + return; + } + for (String scope : scopes) { + if(! at.scopes.contains(scope)) { + throw new SaOAuth2AccessTokenScopeException("该 access_token 不具备 scope:" + scope) + .setAccessToken(accessToken) + .setScope(scope) + .setCode(SaOAuth2ErrorCode.CODE_30108); + } + } + } + + /** + * 获取 Access-Token 所代表的LoginId + * @param accessToken Access-Token + * @return LoginId + */ + public Object getLoginIdByAccessToken(String accessToken) { + return checkAccessToken(accessToken).loginId; + } + + /** + * 获取 Access-Token 所代表的 clientId + * @param accessToken Access-Token + * @return LoginId + */ + public Object getClientIdByAccessToken(String accessToken) { + return checkAccessToken(accessToken).clientId; + } + + /** + * 回收一个 Access-Token + * @param accessToken Access-Token值 + */ + public void revokeAccessToken(String accessToken) { + AccessTokenModel at = getAccessToken(accessToken); + if(at == null) { + return; + } + + // 删 at、索引 + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + dao.deleteAccessToken(accessToken); + dao.deleteAccessTokenIndex_BySingleData(at.clientId, at.loginId, accessToken); + } + + /** + * 回收全部 Access-Token:指定应用下 指定用户 的全部 Access-Token + * @param clientId / + * @param loginId / + */ + public void revokeAccessTokenByIndex(String clientId, Object loginId) { + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + List accessTokenList = getAccessTokenValueList(clientId, loginId); + if( ! accessTokenList.isEmpty()) { + // 删 AT + for (String accessToken : accessTokenList) { + dao.deleteAccessToken(accessToken); + } + // 删索引 + dao.deleteAccessTokenIndex(clientId, loginId); + } + } + + + // ----------------- Refresh-Token 相关 ----------------- + + /** + * 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null + * @param refreshToken / + * @return / + */ + public RefreshTokenModel getRefreshToken(String refreshToken) { + return SaOAuth2Manager.getDao().getRefreshToken(refreshToken); + } + + /** + * 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 + * @param refreshToken / + * @return / + */ + public RefreshTokenModel checkRefreshToken(String refreshToken) { + RefreshTokenModel rt = SaOAuth2Manager.getDao().getRefreshToken(refreshToken); + if(rt == null) { + throw new SaOAuth2RefreshTokenException("无效 refresh_token: " + refreshToken) + .setRefreshToken(refreshToken) + .setCode(SaOAuth2ErrorCode.CODE_30111); + } + return rt; + } + + /** + * 获取 Refresh-Token 列表:此应用下 对 某个用户 签发的所有 Refresh-Token + * + * @param clientId / + * @param loginId / + * @return / + */ + public List getRefreshTokenValueList(String clientId, Object loginId) { + return SaOAuth2Manager.getDao().getRefreshTokenValueList_FromAdjustAfter(clientId, loginId); + } + + /** + * 回收一个 Refresh-Token + * + * @param refreshToken Refresh-Token 值 + */ + public void revokeRefreshToken(String refreshToken) { + RefreshTokenModel rt = getRefreshToken(refreshToken); + if(rt == null) { + return; + } + + // 删 rt、索引 + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + dao.deleteRefreshToken(refreshToken); + dao.deleteRefreshTokenIndex_BySingleData(rt.clientId, rt.loginId, refreshToken); + } + + /** + * 回收全部 Refresh-Token:指定应用下 指定用户 的全部 Refresh-Token + * + * @param clientId / + * @param loginId / + */ + public void revokeRefreshTokenByIndex(String clientId, Object loginId) { + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + List refreshTokenList = getRefreshTokenValueList(clientId, loginId); + if( ! refreshTokenList.isEmpty()) { + // 删 RT + for (String refreshToken : refreshTokenList) { + dao.deleteRefreshToken(refreshToken); + } + // 删索引 + dao.deleteRefreshTokenIndex(clientId, loginId); + } + } + + /** + * 根据 RefreshToken 刷新出一个 AccessToken + * @param refreshToken / + * @return / + */ + public AccessTokenModel refreshAccessToken(String refreshToken) { + return SaOAuth2Manager.getDataGenerate().refreshAccessToken(refreshToken); + } + + + // ----------------- Client-Token 相关 ----------------- + + /** + * 获取 ClientTokenModel,无效的 ClientToken 会返回 null + * @param clientToken / + * @return / + */ + public ClientTokenModel getClientToken(String clientToken) { + return SaOAuth2Manager.getDao().getClientToken(clientToken); + } + + /** + * 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 + * @param clientToken / + * @return / + */ + public ClientTokenModel checkClientToken(String clientToken) { + ClientTokenModel ct = getClientToken(clientToken); + if(ct == null) { + throw new SaOAuth2ClientTokenException("无效 client_token: " + clientToken) + .setClientToken(clientToken) + .setCode(SaOAuth2ErrorCode.CODE_30107); + } + return ct; + } + + /** + * 获取 Client-Token 列表:此应用下 对 某个用户 签发的所有 Client-token + * + * @param clientId / + * @return / + */ + public List getClientTokenValueList(String clientId) { + return SaOAuth2Manager.getDao().getClientTokenValueList_FromAdjustAfter(clientId); + } + + /** + * 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false + * @param clientToken Client-Token + * @param scopes 需要校验的权限列表 + */ + public boolean hasClientTokenScope(String clientToken, String... scopes) { + try { + checkClientTokenScope(clientToken, scopes); + return true; + } catch (SaOAuth2ClientTokenException e) { + return false; + } + } + + /** + * 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 + * @param clientToken Client-Token + * @param scopes 需要校验的权限列表 + */ + public void checkClientTokenScope(String clientToken, String... scopes) { + ClientTokenModel ct = checkClientToken(clientToken); + if(SaFoxUtil.isEmptyArray(scopes)) { + return; + } + for (String scope : scopes) { + if(! ct.scopes.contains(scope)) { + throw new SaOAuth2ClientTokenScopeException("该 client_token 不具备 scope:" + scope) + .setClientToken(clientToken) + .setScope(scope) + .setCode(SaOAuth2ErrorCode.CODE_30109); + } + } + } + + /** + * 回收一个 ClientToken + * + * @param clientToken / + */ + public void revokeClientToken(String clientToken) { + ClientTokenModel ct = getClientToken(clientToken); + if(ct == null) { + return; + } + // 删 ct、删索引 + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + dao.deleteClientToken(clientToken); + dao.deleteClientTokenIndex_BySingleData(ct.clientId, clientToken); + } + + /** + * 回收全部 Client-Token:指定应用下的全部 Client-Token + * 回收 ClientToken,根据索引: clientId + * + * @param clientId / + */ + public void revokeClientTokenByIndex(String clientId) { + SaOAuth2Dao dao = SaOAuth2Manager.getDao(); + + List clientTokenList = getClientTokenValueList(clientId); + if( ! clientTokenList.isEmpty()) { + // 删 AT + for (String clientToken : clientTokenList) { + dao.deleteClientToken(clientToken); + } + // 删索引 + dao.deleteClientTokenIndex(clientId); + } + } + + + // ------------------- 请求查询 + + /** + * 数据读取:从当前请求对象中读取 access_token,并查询到 AccessTokenModel 信息,无效 access_token 抛出异常 + *
    1、请求参数 access_token,2、请求头 Authorization Bearer access_token + */ + public AccessTokenModel currentAccessToken() { + String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); + return checkAccessToken(accessToken); + } + + /** + * 数据读取:从当前请求对象中读取 client_token,并查询到 ClientTokenModel 信息,无效 client_token 抛出异常 + *
    1、请求参数 client_token,2、请求头 Authorization Bearer client_token + */ + public ClientTokenModel currentClientToken() { + String clientToken = SaOAuth2Manager.getDataResolver().readClientToken(SaHolder.getRequest()); + return checkClientToken(clientToken); + } + + + // ----------------- 包装其它 bean 的方法 ----------------- + + /** + * 持久化:用户授权记录 + * @param clientId 应用id + * @param loginId 账号id + * @param scopes 权限列表 + */ + public void saveGrantScope(String clientId, Object loginId, List scopes) { + SaOAuth2Manager.getDao().saveGrantScope(clientId, loginId, scopes); + } + + /** + * 获取高级权限列表 + * @return / + */ + public List getHigherScopeList() { + return SaOAuth2Manager.getDataLoader().getHigherScopeList(); + } + + /** + * 获取低级权限列表 + * @return / + */ + public List getLowerScopeList() { + return SaOAuth2Manager.getDataLoader().getLowerScopeList(); + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java new file mode 100644 index 000000000..ad45dc76b --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java @@ -0,0 +1,400 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.oauth2.template; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; + +import java.util.List; + +/** + * Sa-Token OAuth2 模块 工具类 + * + * @author click33 + * @since 1.23.0 + */ +public class SaOAuth2Util { + + // ----------------- ClientModel 相关 ----------------- + + /** + * 获取 ClientModel,根据 clientId + * + * @param clientId / + * @return / + */ + public static SaClientModel getClientModel(String clientId) { + return SaOAuth2Manager.getTemplate().getClientModel(clientId); + } + + /** + * 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 + * @param clientId / + * @return / + */ + public static SaClientModel checkClientModel(String clientId) { + return SaOAuth2Manager.getTemplate().checkClientModel(clientId); + } + + /** + * 校验:clientId 与 clientSecret 是否正确 + * @param clientId 应用id + * @param clientSecret 秘钥 + * @return SaClientModel对象 + */ + public static SaClientModel checkClientSecret(String clientId, String clientSecret) { + return SaOAuth2Manager.getTemplate().checkClientSecret(clientId, clientSecret); + } + + /** + * 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes + * @param clientId 应用id + * @param clientSecret 秘钥 + * @param scopes 权限 + * @return SaClientModel对象 + */ + public static SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List scopes) { + return SaOAuth2Manager.getTemplate().checkClientSecretAndScope(clientId, clientSecret, scopes); + } + + /** + * 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false + * @param clientId 应用id + * @param scopes 权限 + * @return / + */ + public static boolean isContractScope(String clientId, List scopes) { + return SaOAuth2Manager.getTemplate().isContractScope(clientId, scopes); + } + + /** + * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 + * @param clientId 应用id + * @param scopes 权限列表 + * @return / + */ + public static SaClientModel checkContractScope(String clientId, List scopes) { + return SaOAuth2Manager.getTemplate().checkContractScope(clientId, scopes); + } + + /** + * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 + * @param cm 应用 + * @param scopes 权限列表 + * @return / + */ + public static SaClientModel checkContractScope(SaClientModel cm, List scopes) { + return SaOAuth2Manager.getTemplate().checkContractScope(cm, scopes); + } + + // --------- redirect_uri 相关 + + /** + * 校验:该 Client 使用指定 url 作为回调地址,是否合法 + * @param clientId 应用id + * @param url 指定url + */ + public static void checkRedirectUri(String clientId, String url) { + SaOAuth2Manager.getTemplate().checkRedirectUri(clientId, url); + } + + // --------- 授权相关 + + /** + * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope + * @param loginId 账号id + * @param clientId 应用id + * @param scopes 权限 + * @return 是否已经授权 + */ + public static boolean isGrantScope(Object loginId, String clientId, List scopes) { + return SaOAuth2Manager.getTemplate().isGrantScope(loginId, clientId, scopes); + } + + /** + * 删除:指定 loginId 针对指定 Client 的授权信息 + * @param loginId 账号id + * @param clientId 应用id + */ + public static void deleteGrantScope(Object loginId, String clientId) { + SaOAuth2Manager.getTemplate().deleteGrantScope(loginId, clientId); + } + + + // ----------------- Code 相关 ----------------- + + /** + * 获取 CodeModel,无效的 code 会返回 null + * @param code / + * @return / + */ + public static CodeModel getCode(String code) { + return SaOAuth2Manager.getTemplate().getCode(code); + } + + /** + * 校验 Code,成功返回 CodeModel,失败则抛出异常 + * @param code / + * @return / + */ + public static CodeModel checkCode(String code) { + return SaOAuth2Manager.getTemplate().checkCode(code); + } + + /** + * 获取 Code,根据索引: clientId、loginId + * @param clientId / + * @param loginId / + * @return / + */ + public static String getCodeValue(String clientId, Object loginId) { + return SaOAuth2Manager.getTemplate().getCodeValue(clientId, loginId); + } + + + // ----------------- Access-Token 相关 ----------------- + + /** + * 获取 AccessTokenModel,无效的 AccessToken 会返回 null + * @param accessToken / + * @return / + */ + public static AccessTokenModel getAccessToken(String accessToken) { + return SaOAuth2Manager.getTemplate().getAccessToken(accessToken); + } + + /** + * 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 + * @param accessToken / + * @return / + */ + public static AccessTokenModel checkAccessToken(String accessToken) { + return SaOAuth2Manager.getTemplate().checkAccessToken(accessToken); + } + + /** + * 获取 Access-Token 列表:此应用下 对 某个用户 签发的所有 Access-token + * @param clientId / + * @param loginId / + * @return / + */ + public static List getAccessTokenValueList(String clientId, Object loginId) { + return SaOAuth2Manager.getTemplate().getAccessTokenValueList(clientId, loginId); + } + + /** + * 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false + * @param accessToken Access-Token + * @param scopes 需要校验的权限列表 + */ + public static boolean hasAccessTokenScope(String accessToken, String... scopes) { + return SaOAuth2Manager.getTemplate().hasAccessTokenScope(accessToken, scopes); + } + + /** + * 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 + * @param accessToken Access-Token + * @param scopes 需要校验的权限列表 + */ + public static void checkAccessTokenScope(String accessToken, String... scopes) { + SaOAuth2Manager.getTemplate().checkAccessTokenScope(accessToken, scopes); + } + + /** + * 获取 Access-Token 所代表的LoginId + * @param accessToken Access-Token + * @return LoginId + */ + public static Object getLoginIdByAccessToken(String accessToken) { + return SaOAuth2Manager.getTemplate().getLoginIdByAccessToken(accessToken); + } + + /** + * 获取 Access-Token 所代表的 clientId + * @param accessToken Access-Token + * @return LoginId + */ + public static Object getClientIdByAccessToken(String accessToken) { + return SaOAuth2Manager.getTemplate().getClientIdByAccessToken(accessToken); + } + + /** + * 回收一个 Access-Token + * @param accessToken Access-Token值 + */ + public static void revokeAccessToken(String accessToken) { + SaOAuth2Manager.getTemplate().revokeAccessToken(accessToken); + } + + /** + * 回收全部 Access-Token:指定应用下 指定用户 的全部 Access-Token + * @param clientId / + * @param loginId / + */ + public static void revokeAccessTokenByIndex(String clientId, Object loginId) { + SaOAuth2Manager.getTemplate().revokeAccessTokenByIndex(clientId, loginId); + } + + + // ----------------- Refresh-Token 相关 ----------------- + + /** + * 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null + * @param refreshToken / + * @return / + */ + public static RefreshTokenModel getRefreshToken(String refreshToken) { + return SaOAuth2Manager.getTemplate().getRefreshToken(refreshToken); + } + + /** + * 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 + * @param refreshToken / + * @return / + */ + public static RefreshTokenModel checkRefreshToken(String refreshToken) { + return SaOAuth2Manager.getTemplate().checkRefreshToken(refreshToken); + } + + /** + * 获取 Refresh-Token 列表:此应用下 对 某个用户 签发的所有 Refresh-Token + * + * @param clientId / + * @param loginId / + * @return / + */ + public static List getRefreshTokenValueList(String clientId, Object loginId) { + return SaOAuth2Manager.getTemplate().getRefreshTokenValueList(clientId, loginId); + } + + /** + * 回收一个 Refresh-Token + * + * @param refreshToken Refresh-Token 值 + */ + public static void revokeRefreshToken(String refreshToken) { + SaOAuth2Manager.getTemplate().revokeRefreshToken(refreshToken); + } + + /** + * 回收全部 Refresh-Token:指定应用下 指定用户 的全部 Refresh-Token + * @param clientId / + * @param loginId / + */ + public static void revokeRefreshTokenByIndex(String clientId, Object loginId) { + SaOAuth2Manager.getTemplate().revokeRefreshTokenByIndex(clientId, loginId); + } + + /** + * 根据 RefreshToken 刷新出一个 AccessToken + * @param refreshToken / + * @return / + */ + public static AccessTokenModel refreshAccessToken(String refreshToken) { + return SaOAuth2Manager.getTemplate().refreshAccessToken(refreshToken); + } + + + // ----------------- Client-Token 相关 ----------------- + + /** + * 获取 ClientTokenModel,无效的 ClientToken 会返回 null + * @param clientToken / + * @return / + */ + public static ClientTokenModel getClientToken(String clientToken) { + return SaOAuth2Manager.getTemplate().getClientToken(clientToken); + } + + /** + * 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 + * @param clientToken / + * @return / + */ + public static ClientTokenModel checkClientToken(String clientToken) { + return SaOAuth2Manager.getTemplate().checkClientToken(clientToken); + } + + /** + * 获取 Client-Token 列表:此应用下 对 某个用户 签发的所有 Client-token + * + * @param clientId / + * @return / + */ + public static List getClientTokenValueList(String clientId) { + return SaOAuth2Manager.getTemplate().getClientTokenValueList(clientId); + } + + /** + * 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false + * @param clientToken Client-Token + * @param scopes 需要校验的权限列表 + */ + public static boolean hasClientTokenScope(String clientToken, String... scopes) { + return SaOAuth2Manager.getTemplate().hasClientTokenScope(clientToken, scopes); + } + + /** + * 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 + * @param clientToken Client-Token + * @param scopes 需要校验的权限列表 + */ + public static void checkClientTokenScope(String clientToken, String... scopes) { + SaOAuth2Manager.getTemplate().checkClientTokenScope(clientToken, scopes); + } + + /** + * 回收一个 ClientToken + * + * @param clientToken / + */ + public static void revokeClientToken(String clientToken) { + SaOAuth2Manager.getTemplate().revokeClientToken(clientToken); + } + + /** + * 回收全部 Client-Token:指定应用下的全部 Client-Token + * + * @param clientId / + */ + public static void revokeClientTokenByIndex(String clientId) { + SaOAuth2Manager.getTemplate().revokeClientTokenByIndex(clientId); + } + + + // ------------------- 请求查询 + + /** + * 数据读取:从当前请求对象中读取 access_token,并查询到 AccessTokenModel 信息,无效 access_token 抛出异常 + *
    1、请求参数 access_token,2、请求头 Authorization Bearer access_token + */ + public static AccessTokenModel currentAccessToken() { + return SaOAuth2Manager.getTemplate().currentAccessToken(); + } + + /** + * 数据读取:从当前请求对象中读取 client_token,并查询到 ClientTokenModel 信息,无效 client_token 抛出异常 + *
    1、请求参数 client_token,2、请求头 Authorization Bearer client_token + */ + public static ClientTokenModel currentClientToken() { + return SaOAuth2Manager.getTemplate().currentClientToken(); + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOAuth2.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOAuth2.java new file mode 100644 index 000000000..92ec8bfc2 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOAuth2.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.oauth2.annotation.handler.SaCheckAccessTokenHandler; +import cn.dev33.satoken.oauth2.annotation.handler.SaCheckClientIdSecretHandler; +import cn.dev33.satoken.oauth2.annotation.handler.SaCheckClientTokenHandler; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; + +/** + * SaToken 插件安装:OAuth2 相关功能 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginForOAuth2 implements SaTokenPlugin { + + @Override + public void install() { + // 安装 OAuth2 鉴权注解 + SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckAccessTokenHandler()); + SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckClientTokenHandler()); + SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckClientIdSecretHandler()); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-oauth2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..33727cef8 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForOAuth2 \ No newline at end of file diff --git a/sa-token-plugin/sa-token-okhttps/pom.xml b/sa-token-plugin/sa-token-okhttps/pom.xml new file mode 100644 index 000000000..fa45d845b --- /dev/null +++ b/sa-token-plugin/sa-token-okhttps/pom.xml @@ -0,0 +1,27 @@ + + + + sa-token-plugin + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + + sa-token-okhttps + sa-token-okhttps + sa-token integrate OkHttps + + + + cn.dev33 + sa-token-core + + + cn.zhxu + okhttps + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForOkHttps.java b/sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForOkHttps.java new file mode 100644 index 000000000..b8c385728 --- /dev/null +++ b/sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForOkHttps.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.http; + +import cn.dev33.satoken.SaManager; +import cn.zhxu.okhttps.OkHttps; + +import java.util.Map; + +/** + * Http 请求处理器, OkHttps 版实现 + * + * @author click33 + * @since 1.43.0 + */ +public class SaHttpTemplateForOkHttps implements SaHttpTemplate { + + @Override + public String get(String url) { + SaManager.log.debug("发起请求,GET:{}", url); + String res = OkHttps.sync(url).get().getBody().toString(); + SaManager.log.debug("返回结果:{}", res); + return res; + } + + @Override + public String postByFormData(String url, Map params) { + SaManager.log.debug("发起请求,POST:{}\t参数:{}", url, params); + String res = OkHttps.sync(url).addBodyPara(params).post().getBody().toString(); + SaManager.log.debug("返回结果:{}", res); + return res; + } + +} diff --git a/sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOkHttps.java b/sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOkHttps.java new file mode 100644 index 000000000..e1d228be4 --- /dev/null +++ b/sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOkHttps.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.http.SaHttpTemplateForOkHttps; + +/** + * SaToken 插件安装:Http 请求处理器 - OkHttps 版 + * + * @author click33 + * @since 1.43.0 + */ +public class SaTokenPluginForOkHttps implements SaTokenPlugin { + + @Override + public void install() { + // 设置 OkHttps 作为 Http 请求处理器 + SaManager.setSaHttpTemplate(new SaHttpTemplateForOkHttps()); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-okhttps/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-okhttps/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..aff8e9133 --- /dev/null +++ b/sa-token-plugin/sa-token-okhttps/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForOkHttps \ No newline at end of file diff --git a/sa-token-plugin/sa-token-quick-login/.gitignore b/sa-token-plugin/sa-token-quick-login/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-plugin/sa-token-quick-login/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-plugin/sa-token-quick-login/pom.xml b/sa-token-plugin/sa-token-quick-login/pom.xml index c4a52fcc2..a805af34d 100644 --- a/sa-token-plugin/sa-token-quick-login/pom.xml +++ b/sa-token-plugin/sa-token-quick-login/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-plugin - 1.24.0 + ${revision} + ../pom.xml jar @@ -18,15 +19,20 @@ - cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} - + cn.dev33 + sa-token-spring-boot-starter + true + + + cn.dev33 + sa-token-spring-boot3-starter + true + + org.springframework.boot spring-boot-starter-thymeleaf - 2.0.0.RELEASE diff --git a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickBean.java b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickBean.java deleted file mode 100644 index 6055e238a..000000000 --- a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickBean.java +++ /dev/null @@ -1,79 +0,0 @@ -package cn.dev33.satoken.quick; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.annotation.Order; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import cn.dev33.satoken.context.SaHolder; -import cn.dev33.satoken.exception.NotLoginException; -import cn.dev33.satoken.filter.SaServletFilter; -import cn.dev33.satoken.quick.config.SaQuickConfig; -import cn.dev33.satoken.quick.web.SaQuickController; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaTokenConsts; - -/** - * 自动注入 - * - * @author kong - * - */ -@Configuration -@Import({ SaQuickController.class }) -public class SaQuickBean implements WebMvcConfigurer { - - /** - * quick-login 配置 - * - * @return see note - */ - @Bean - @ConfigurationProperties(prefix = "sa") - public SaQuickConfig getSaQuickConfig() { - return new SaQuickConfig(); - } - - /** - * 注入quick-login 配置 - * - * @param saQuickConfig 配置对象 - */ - @Autowired - public void setSaQuickConfig(SaQuickConfig saQuickConfig) { - SaQuickManager.setConfig(saQuickConfig); - } - - /** - * 注册 [sa-token全局过滤器] - * - * @return see note - */ - @Bean - @Order(SaTokenConsts.ASSEMBLY_ORDER - 1) - public SaServletFilter getSaServletFilter() { - return new SaServletFilter() - // 拦截路由 & 放行路由 - .addInclude(SaQuickManager.getConfig().getInclude().split(",")) - .addExclude(SaQuickManager.getConfig().getExclude().split(",")) - .addExclude("/favicon.ico", "/saLogin", "/doLogin", "/sa-res/**"). - // 认证函数: 每次请求执行 - setAuth(r -> { - // 未登录时直接转发到login.html页面 - if (SaQuickManager.getConfig().getAuth() && StpUtil.isLogin() == false) { - SaHolder.getRequest().forward("/saLogin"); - // 抛出异常,不再继续执行 - throw NotLoginException.newInstance(StpUtil.getLoginType(), ""); - } - }). - - // 异常处理函数:每次认证函数发生异常时执行此函数 - setError(e -> { - return e.getMessage(); - }); - } - -} diff --git a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickInject.java b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickInject.java new file mode 100644 index 000000000..4ff6f048c --- /dev/null +++ b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickInject.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.quick; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import cn.dev33.satoken.quick.config.SaQuickConfig; +import cn.dev33.satoken.quick.web.SaQuickController; + +/** + * Quick-Bean 注入 + * + * @author click33 + * @since 1.30.0 + */ +@Configuration +@Import({ SaQuickController.class, SaQuickRegister.class}) +public class SaQuickInject { + + /** + * 注入 quick-login 配置 + * + * @param saQuickConfig 配置对象 + */ + @Autowired(required = false) + public void setSaQuickConfig(SaQuickConfig saQuickConfig) { + SaQuickManager.setConfig(saQuickConfig); + } + +} diff --git a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickManager.java b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickManager.java index aa9c65682..da07febed 100644 --- a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickManager.java +++ b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickManager.java @@ -1,22 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.quick; import cn.dev33.satoken.quick.config.SaQuickConfig; import cn.dev33.satoken.util.SaFoxUtil; /** - * SaQuickManager - * @author kong + * SaQuickManager,持有 SaQuickConfig 配置对象全局引用 * + * @author click33 + * @since 1.19.0 */ public class SaQuickManager { /** * 配置文件 Bean */ - private static SaQuickConfig config; + private static volatile SaQuickConfig config; public static void setConfig(SaQuickConfig config) { SaQuickManager.config = config; - // 如果配置了随机密码 + // 如果配置了 auto=true,则随机生成账号名密码 if(config.getAuto()) { config.setName(SaFoxUtil.getRandomString(8)); config.setPwd(SaFoxUtil.getRandomString(8)); diff --git a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickRegister.java b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickRegister.java new file mode 100644 index 000000000..f62a00d7a --- /dev/null +++ b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickRegister.java @@ -0,0 +1,111 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.quick; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicAccount; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.quick.config.SaQuickConfig; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +/** + * Quick Login 相关 Bean 注册 + * + * @author click33 + * @since 1.30.0 + */ +@Configuration +public class SaQuickRegister { + + /** + * 使用一个比较短的前缀,尽量提高 cmd 命令台启动时指定参数的便利性 + */ + public static final String CONFIG_VERSION = "sa"; + + /** + * 注册 Quick-Login 配置 + * + * @return see note + */ + @Bean + @ConfigurationProperties(prefix = CONFIG_VERSION) + public SaQuickConfig getSaQuickConfig() { + return new SaQuickConfig(); + } + + + /** + * 注册 Sa-Token 全局过滤器 + * + * @return / + */ + @Bean + @Order(SaTokenConsts.ASSEMBLY_ORDER - 1) + SaServletFilter getSaServletFilterForQuickLogin() { + return new SaServletFilter() + + // 拦截路由 + .addInclude("/**") + + // 排除掉登录相关接口,不需要鉴权的 + .addExclude("/favicon.ico", "/saLogin", "/doLogin", "/sa-res/**") + + // 认证函数: 每次请求执行 + .setAuth(obj -> { + SaRouter + .match(SaFoxUtil.convertStringToList(SaQuickManager.getConfig().getInclude())) + .notMatch(SaFoxUtil.convertStringToList(SaQuickManager.getConfig().getExclude())) + .check(r -> { + + // 如果已关闭认证要求,则直接通过 + if (!SaQuickManager.getConfig().getAuth()) { + return; + } + + // 如果请求端提供了 Http Basic 认证信息,那么直接使用此认证信息进行登录判断 + SaHttpBasicAccount hba = SaHttpBasicUtil.getHttpBasicAccount(); + if(hba != null) { + SaResult res = SaQuickManager.getConfig().doLoginHandle.apply(hba.getUsername(), hba.getPassword()); + if(res.getCode() != SaResult.CODE_SUCCESS) { + SaRouter.back(res); + } + } else { + // 未登录时直接转发到 login.html 页面 + if (! StpUtil.isLogin()) { + SaHolder.getRequest().forward("/saLogin"); + SaRouter.back(); + } + } + + }); + }). + + // 异常处理函数:每次认证函数发生异常时执行此函数 + setError(e -> { + return e.getMessage(); + }); + } + +} diff --git a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/config/SaQuickConfig.java b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/config/SaQuickConfig.java index 867056700..4b1e993df 100644 --- a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/config/SaQuickConfig.java +++ b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/config/SaQuickConfig.java @@ -1,14 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.quick.config; +import cn.dev33.satoken.quick.function.DoLoginHandleFunction; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; + /** * sa-quick 配置类 Model * - * @author kong - * + * @author click33 + * @since 1.19.0 */ public class SaQuickConfig { - /** 是否开启全局认证 */ + /** 是否开启全局登录校验,如果为 false,则不再拦截请求出现登录页 */ private Boolean auth = true; /** 用户名 */ @@ -17,7 +37,7 @@ public class SaQuickConfig { /** 密码 */ private String pwd = "123456"; - /** 是否自动生成一个账号和密码 */ + /** 是否自动生成一个账号和密码,此配置项为 true 后,name、pwd 字段将失效 */ private Boolean auto = false; /** 登录页面的标题 */ @@ -95,8 +115,27 @@ public String getExclude() { public void setExclude(String exclude) { this.exclude = exclude; } - - + + /** + * 登录处理函数 + */ + public DoLoginHandleFunction doLoginHandle = (name, pwd) -> { + + // 参数完整性校验 + if(SaFoxUtil.isEmpty(name) || SaFoxUtil.isEmpty(pwd)) { + return SaResult.get(500, "请输入账号和密码", null); + } + + // 密码校验:将前端提交的 name、pwd 与配置文件中的配置项进行比对 + if(name.equals(this.getName()) && pwd.equals(this.getPwd())) { + StpUtil.login(this.getName()); + return SaResult.data(StpUtil.getTokenInfo()); + } else { + return SaResult.error("账号或密码输入错误"); + } + }; + + @Override public String toString() { return "SaQuickConfig{" + diff --git a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/function/DoLoginHandleFunction.java b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/function/DoLoginHandleFunction.java new file mode 100644 index 000000000..6df582d2e --- /dev/null +++ b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/function/DoLoginHandleFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.quick.function; + +import cn.dev33.satoken.util.SaResult; + +import java.util.function.BiFunction; + +/** + * 函数式接口:登录处理函数 + * + *

    参数:账号、密码

    + *

    返回:登录结果

    + * + * @author click33 + * @since 1.41.0 + */ +@FunctionalInterface +public interface DoLoginHandleFunction extends BiFunction { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/web/SaQuickController.java b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/web/SaQuickController.java index aac4a8531..da34d5f23 100644 --- a/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/web/SaQuickController.java +++ b/sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/web/SaQuickController.java @@ -1,34 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.quick.web; -import javax.servlet.http.HttpServletRequest; - +import cn.dev33.satoken.quick.SaQuickManager; +import cn.dev33.satoken.util.SaResult; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import cn.dev33.satoken.quick.SaQuickManager; -import cn.dev33.satoken.quick.config.SaQuickConfig; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.util.SaFoxUtil; -import cn.dev33.satoken.util.SaResult; - /** - * 登录Controller - * @author kong + * 登录Controller,处理登录相关请求 * + * @author click33 + * @since 1.19.0 */ @Controller public class SaQuickController { /** * 进入登录页面 - * @param request see note - * @return see note + * @param model / + * @return / */ @GetMapping("/saLogin") - public String saLogin(HttpServletRequest request) { - request.setAttribute("cfg", SaQuickManager.getConfig()); + public String saLogin(Model model) { + model.addAttribute("cfg", SaQuickManager.getConfig()); return "sa-login.html"; } @@ -40,22 +52,8 @@ public String saLogin(HttpServletRequest request) { */ @PostMapping("/doLogin") @ResponseBody - public SaResult doLogin(String name, String pwd) { - - // 参数完整性校验 - if(SaFoxUtil.isEmpty(name) || SaFoxUtil.isEmpty(pwd)) { - return SaResult.get(500, "请输入账号和密码", null); - } - - // 密码校验 - SaQuickConfig config = SaQuickManager.getConfig(); - if(name.equals(config.getName()) && pwd.equals(config.getPwd())) { - StpUtil.login(config.getName()); - return SaResult.get(200, "ok", StpUtil.getTokenInfo()); - } else { - // 校验失败 - return SaResult.get(500, "账号或密码输入错误", null); - } + public SaResult doLogin(@RequestParam("name") String name, @RequestParam("pwd") String pwd) { + return SaQuickManager.getConfig().doLoginHandle.apply(name, pwd); } } diff --git a/sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring.factories index 11ec6b6e2..6e3133473 100644 --- a/sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring.factories +++ b/sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring.factories @@ -1 +1,2 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.quick.SaQuickBean \ No newline at end of file +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.dev33.satoken.quick.SaQuickInject \ No newline at end of file diff --git a/sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..517a45c52 --- /dev/null +++ b/sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.quick.SaQuickInject \ No newline at end of file diff --git a/sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/login.js b/sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/login.js index 5e619d17d..0af902dfc 100644 --- a/sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/login.js +++ b/sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/login.js @@ -61,5 +61,5 @@ $('[name=name],[name=pwd]').bind('keypress', function(event){ $("[name=name]").focus(); // 打印信息 -var str = "This page is provided by Sa-Token, Please refer to: " + "http://sa-token.dev33.cn/"; +var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.com/"; console.log(str); diff --git a/sa-token-plugin/sa-token-redis-jackson/pom.xml b/sa-token-plugin/sa-token-redis-jackson/pom.xml new file mode 100644 index 000000000..23dbabbfb --- /dev/null +++ b/sa-token-plugin/sa-token-redis-jackson/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-redis-jackson + sa-token-redis-jackson + sa-token integrate redis (to jackson) + + + + cn.dev33 + sa-token-jackson + + + cn.dev33 + sa-token-redis-template + + + + diff --git a/sa-token-plugin/sa-token-redis-template-jdk-serializer/pom.xml b/sa-token-plugin/sa-token-redis-template-jdk-serializer/pom.xml new file mode 100644 index 000000000..8ce204249 --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template-jdk-serializer/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-redis-template-jdk-serializer + sa-token-redis-template-jdk-serializer + sa-token integrate RedisTemplate (jdk-serializer) + + + + + cn.dev33 + sa-token-core + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + diff --git a/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java b/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java new file mode 100644 index 000000000..d04b3c14f --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java @@ -0,0 +1,179 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; + +import cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString; +import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Sa-Token 持久层实现 [ RedisTemplate 存储 ] (可用环境: SpringBoot2、SpringBoot3、SpringBoot4) + *
    copy by: sa-token-redis-template 插件 + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenDaoForRedisTemplate implements SaTokenDaoByObjectFollowString, SaTokenDao { + + public StringRedisTemplate stringRedisTemplate; + + /** + * 标记:当前 redis 连接信息是否已初始化成功 + */ + public boolean isInit; + + @Autowired + public void init(RedisConnectionFactory connectionFactory) { + // 如果已经初始化成功了,就立刻退出,不重复初始化 + if(this.isInit) { + return; + } + + // 构建StringRedisTemplate + StringRedisTemplate stringTemplate = new StringRedisTemplate(); + stringTemplate.setConnectionFactory(connectionFactory); + stringTemplate.afterPropertiesSet(); + this.stringRedisTemplate = stringTemplate; + + initMore(connectionFactory); + + // 打上标记,表示已经初始化成功,后续无需再重新初始化 + this.isInit = true; + } + + protected void initMore(RedisConnectionFactory connectionFactory) { + + } + + + /** + * 获取Value,如无返空 + */ + @Override + public String get(String key) { + String finalKey = wrapKey(key); + return stringRedisTemplate.opsForValue().get(finalKey); + } + + /** + * 写入Value,并设定存活时间 (单位: 秒) + */ + @Override + public void set(String key, String value, long timeout) { + String finalKey = wrapKey(key); + if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // 判断是否为永不过期 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + stringRedisTemplate.opsForValue().set(finalKey, value); + } else { + stringRedisTemplate.opsForValue().set(finalKey, value, timeout, TimeUnit.SECONDS); + } + } + + /** + * 修改指定key-value键值对 (过期时间不变) + */ + @Override + public void update(String key, String value) { + String finalKey = wrapKey(key); + @SuppressWarnings("all") + long expireMs = stringRedisTemplate.getExpire(finalKey, TimeUnit.MILLISECONDS); + // -2 = 无此键 + if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // -1 = 永不过期 + if(expireMs == SaTokenDao.NEVER_EXPIRE) { + stringRedisTemplate.opsForValue().set(finalKey, value); + } else { + stringRedisTemplate.opsForValue().set(finalKey, value, expireMs, TimeUnit.MILLISECONDS); + } + } + + /** + * 删除Value + */ + @Override + public void delete(String key) { + String finalKey = wrapKey(key); + stringRedisTemplate.delete(finalKey); + } + + /** + * 获取Value的剩余存活时间 (单位: 秒) + */ + @Override + public long getTimeout(String key) { + String finalKey = wrapKey(key); + return stringRedisTemplate.getExpire(finalKey); + } + + /** + * 修改Value的剩余存活时间 (单位: 秒) + */ + @Override + public void updateTimeout(String key, long timeout) { + String finalKey = wrapKey(key); + // 判断是否想要设置为永久 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + // 调用本类其它方法时使用原始 key,避免二次 wrap + long expire = getTimeout(key); + if(expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.set(key, this.get(key), timeout); + } + return; + } + stringRedisTemplate.expire(finalKey, timeout, TimeUnit.SECONDS); + } + + + + /** + * 搜索数据 + */ + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + // 对完整匹配串做 wrap,避免用户在 key 尾部等位置加工时只 wrap(prefix) 拼错 pattern + String finalPattern = wrapKey(prefix + "*" + keyword + "*"); + Set keys = stringRedisTemplate.keys(finalPattern); + List list = new ArrayList<>(keys); + return SaFoxUtil.searchList(list, start, size, sortType); + } + + /** + * 包装 key(默认原样返回)。需要给 Redis 键加统一前缀时,可重写此方法。 + * + * @param key 原始 key + * @return 包装后的 key + */ + public String wrapKey(String key) { + return key; + } + + +} diff --git a/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplateUseJdkSerializer.java b/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplateUseJdkSerializer.java new file mode 100644 index 000000000..310d358e9 --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplateUseJdkSerializer.java @@ -0,0 +1,156 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; + +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.util.concurrent.TimeUnit; + +/** + * Sa-Token 持久层实现 [ RedisTemplate 存储、JDK默认序列化 ] (可用环境: SpringBoot2、SpringBoot3、SpringBoot4) + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenDaoForRedisTemplateUseJdkSerializer extends SaTokenDaoForRedisTemplate implements SaTokenDao { + + /** + * Object 读写专用 + */ + public RedisTemplate objectRedisTemplate; + + @Override + protected void initMore(RedisConnectionFactory connectionFactory) { + + // 指定相应的序列化方案 + StringRedisSerializer keySerializer = new StringRedisSerializer(); + JdkSerializationRedisSerializer valueSerializer = new JdkSerializationRedisSerializer(); + + // 构建RedisTemplate + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(keySerializer); + template.setHashKeySerializer(keySerializer); + template.setValueSerializer(valueSerializer); + template.setHashValueSerializer(valueSerializer); + template.afterPropertiesSet(); + this.objectRedisTemplate = template; + } + + + /** + * 获取Object,如无返空 + */ + @Override + public Object getObject(String key) { + String finalKey = wrapKey(key); + return objectRedisTemplate.opsForValue().get(finalKey); + } + + /** + * 获取 Object (指定反序列化类型),如无返空 + * + * @param key 键名称 + * @return object + */ + @SuppressWarnings("unchecked") + @Override + public T getObject(String key, Class classType) { + String finalKey = wrapKey(key); + return (T) objectRedisTemplate.opsForValue().get(finalKey); + } + + /** + * 写入Object,并设定存活时间 (单位: 秒) + */ + @Override + public void setObject(String key, Object object, long timeout) { + String finalKey = wrapKey(key); + if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // 判断是否为永不过期 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + objectRedisTemplate.opsForValue().set(finalKey, object); + } else { + objectRedisTemplate.opsForValue().set(finalKey, object, timeout, TimeUnit.SECONDS); + } + } + + /** + * 更新Object (过期时间不变) + */ + @Override + public void updateObject(String key, Object object) { + String finalKey = wrapKey(key); + @SuppressWarnings("all") + long expireMs = stringRedisTemplate.getExpire(finalKey, TimeUnit.MILLISECONDS); + // -2 = 无此键 + if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // -1 = 永不过期 + if(expireMs == SaTokenDao.NEVER_EXPIRE) { + objectRedisTemplate.opsForValue().set(finalKey, object); + } else { + objectRedisTemplate.opsForValue().set(finalKey, object, expireMs, TimeUnit.MILLISECONDS); + } + } + + /** + * 删除Object + */ + @Override + public void deleteObject(String key) { + String finalKey = wrapKey(key); + objectRedisTemplate.delete(finalKey); + } + + /** + * 获取Object的剩余存活时间 (单位: 秒) + */ + @Override + public long getObjectTimeout(String key) { + String finalKey = wrapKey(key); + return objectRedisTemplate.getExpire(finalKey); + } + + /** + * 修改Object的剩余存活时间 (单位: 秒) + */ + @Override + public void updateObjectTimeout(String key, long timeout) { + String finalKey = wrapKey(key); + // 判断是否想要设置为永久 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + // 调用本类其它方法时使用原始 key,避免二次 wrap + long expire = getObjectTimeout(key); + if(expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.setObject(key, this.getObject(key), timeout); + } + return; + } + objectRedisTemplate.expire(finalKey, timeout, TimeUnit.SECONDS); + } + + +} diff --git a/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..c34609376 --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer \ No newline at end of file diff --git a/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..8518af0a3 --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer \ No newline at end of file diff --git a/sa-token-plugin/sa-token-redis-template/pom.xml b/sa-token-plugin/sa-token-redis-template/pom.xml new file mode 100644 index 000000000..8b1741bd5 --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-redis-template + sa-token-redis-template + sa-token integrate RedisTemplate + + + + + cn.dev33 + sa-token-core + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + diff --git a/sa-token-plugin/sa-token-redis-template/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java b/sa-token-plugin/sa-token-redis-template/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java new file mode 100644 index 000000000..2bba15c29 --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java @@ -0,0 +1,178 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; + +import cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString; +import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Sa-Token 持久层实现 [ Redis 存储 ] (可用环境: SpringBoot2、SpringBoot3、SpringBoot4) + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenDaoForRedisTemplate implements SaTokenDaoByObjectFollowString, SaTokenDao { + + public StringRedisTemplate stringRedisTemplate; + + /** + * 标记:当前 redis 连接信息是否已初始化成功 + */ + public boolean isInit; + + @Autowired + public void init(RedisConnectionFactory connectionFactory) { + // 如果已经初始化成功了,就立刻退出,不重复初始化 + if(this.isInit) { + return; + } + + // 构建StringRedisTemplate + StringRedisTemplate stringTemplate = new StringRedisTemplate(); + stringTemplate.setConnectionFactory(connectionFactory); + stringTemplate.afterPropertiesSet(); + this.stringRedisTemplate = stringTemplate; + + initMore(connectionFactory); + + // 打上标记,表示已经初始化成功,后续无需再重新初始化 + this.isInit = true; + } + + protected void initMore(RedisConnectionFactory connectionFactory) { + + } + + + /** + * 获取Value,如无返空 + */ + @Override + public String get(String key) { + String finalKey = wrapKey(key); + return stringRedisTemplate.opsForValue().get(finalKey); + } + + /** + * 写入Value,并设定存活时间 (单位: 秒) + */ + @Override + public void set(String key, String value, long timeout) { + String finalKey = wrapKey(key); + if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // 判断是否为永不过期 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + stringRedisTemplate.opsForValue().set(finalKey, value); + } else { + stringRedisTemplate.opsForValue().set(finalKey, value, timeout, TimeUnit.SECONDS); + } + } + + /** + * 修改指定key-value键值对 (过期时间不变) + */ + @Override + public void update(String key, String value) { + String finalKey = wrapKey(key); + @SuppressWarnings("all") + long expireMs = stringRedisTemplate.getExpire(finalKey, TimeUnit.MILLISECONDS); + // -2 = 无此键 + if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // -1 = 永不过期 + if(expireMs == SaTokenDao.NEVER_EXPIRE) { + stringRedisTemplate.opsForValue().set(finalKey, value); + } else { + stringRedisTemplate.opsForValue().set(finalKey, value, expireMs, TimeUnit.MILLISECONDS); + } + } + + /** + * 删除Value + */ + @Override + public void delete(String key) { + String finalKey = wrapKey(key); + stringRedisTemplate.delete(finalKey); + } + + /** + * 获取Value的剩余存活时间 (单位: 秒) + */ + @Override + public long getTimeout(String key) { + String finalKey = wrapKey(key); + return stringRedisTemplate.getExpire(finalKey); + } + + /** + * 修改Value的剩余存活时间 (单位: 秒) + */ + @Override + public void updateTimeout(String key, long timeout) { + String finalKey = wrapKey(key); + // 判断是否想要设置为永久 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + // 调用本类其它方法时使用原始 key,避免二次 wrap + long expire = getTimeout(key); + if(expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.set(key, this.get(key), timeout); + } + return; + } + stringRedisTemplate.expire(finalKey, timeout, TimeUnit.SECONDS); + } + + + + /** + * 搜索数据 + */ + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + // 对完整匹配串做 wrap,避免用户在 key 尾部等位置加工时只 wrap(prefix) 拼错 pattern + String finalPattern = wrapKey(prefix + "*" + keyword + "*"); + Set keys = stringRedisTemplate.keys(finalPattern); + List list = new ArrayList<>(keys); + return SaFoxUtil.searchList(list, start, size, sortType); + } + + /** + * 包装 key(默认原样返回)。需要给 Redis 键加统一前缀时,可重写此方法。 + * + * @param key 原始 key + * @return 包装后的 key + */ + public String wrapKey(String key) { + return key; + } + + +} diff --git a/sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..78085e767 --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate \ No newline at end of file diff --git a/sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..6d293f5e7 --- /dev/null +++ b/sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate \ No newline at end of file diff --git a/sa-token-plugin/sa-token-redisson-spring-boot-starter/pom.xml b/sa-token-plugin/sa-token-redisson-spring-boot-starter/pom.xml new file mode 100644 index 000000000..1100ad615 --- /dev/null +++ b/sa-token-plugin/sa-token-redisson-spring-boot-starter/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-redisson-spring-boot-starter + sa-token-redisson-spring-boot-starter + sa-token integrate redisson (to jackson) + + + + + cn.dev33 + sa-token-core + + + + + cn.dev33 + sa-token-redisson + + + + + org.redisson + redisson-spring-boot-starter + + + + + + + diff --git a/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenDaoForRedissonBeanRegister.java b/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenDaoForRedissonBeanRegister.java new file mode 100644 index 000000000..fb26a6690 --- /dev/null +++ b/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenDaoForRedissonBeanRegister.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisson; +import org.redisson.api.RedissonClient; +import org.springframework.context.annotation.Bean; + +/** + * 注册 SaTokenDaoForRedisson Bean + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenDaoForRedissonBeanRegister { + + @Bean + public SaTokenDao getSaTokenDaoForRedisson(RedissonClient redissonClient) { + return new SaTokenDaoForRedisson(redissonClient); + } + +} diff --git a/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..5bfefa9aa --- /dev/null +++ b/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.spring.SaTokenDaoForRedissonBeanRegister \ No newline at end of file diff --git a/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..b003810e4 --- /dev/null +++ b/sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.spring.SaTokenDaoForRedissonBeanRegister \ No newline at end of file diff --git a/sa-token-plugin/sa-token-redisson/README.md b/sa-token-plugin/sa-token-redisson/README.md new file mode 100644 index 000000000..8524b1dbc --- /dev/null +++ b/sa-token-plugin/sa-token-redisson/README.md @@ -0,0 +1,115 @@ +## sa-token-redisson + +此扩展,不与生态绑定。可用于不同的生态(SpringBoot,Solon,JFinal等)。 + +### 1、例 solon 集成 + +添加关键依赖 + +```xml + + + cn.dev33 + sa-token-redisson + ${sa-token.version} + + + org.noear + redisson-solon-plugin + ${solon.version} + + +``` + +添加 dao 配置 + +```yaml +sa-token-dao: + config: | + singleServerConfig: + password: "123456" + address: "redis://localhost:6379" + database: 0 +``` + +开始组装 + +```java +@Configuration +public class SaTokenConfigure { + /** + * 构造 RedissonClient + * */ + @Bean + public RedissonClient saTokenDaoInit(@Inject("${sa-token-dao}") RedissonSupplier supplier) { + return supplier.get(); + } + + /** + * 构建 SaTokenDao + * */ + @Bean + public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) { + return new SaTokenDaoForRedisson(redissonClient); + } +} +``` + + +### 2、例 springboot 集成 + + +添加关键依赖 + +```xml + + + cn.dev33 + sa-token-redisson + ${sa-token.version} + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + +``` + +添加 dao 配置 + +```yaml +spring.redis: + redisson: + file: classpath:redisson.yml +``` + +开始组装 + +```java +@Configuration +public class SaTokenConfigure { + /** + * 构建 SaTokenDao + * */ + @Bean + public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) { + return new SaTokenDaoForRedisson(redissonClient); + } +} +``` + + +### 3、Codec + +`SaTokenDaoForRedisson` 默认使用 `StringCodec`,与业务 `RedissonClient` 全局 codec 隔离: + +```java +// 默认 StringCodec +return new SaTokenDaoForRedisson(redissonClient); + +// 指定 codec(例如兼容升级前的 Kryo5 缓存) +return new SaTokenDaoForRedisson(redissonClient, new Kryo5Codec()); +``` + +**升级注意:** 此前跟随 Redisson 全局 codec(未自定义时为 `Kryo5Codec`)。升级后旧缓存无法用 `StringCodec` 读取。请清空 Redis 中的 Sa-Token 数据,或构造时传入 `new Kryo5Codec()`。 diff --git a/sa-token-plugin/sa-token-redisson/pom.xml b/sa-token-plugin/sa-token-redisson/pom.xml new file mode 100644 index 000000000..a15e8193d --- /dev/null +++ b/sa-token-plugin/sa-token-redisson/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-redisson + sa-token-redisson + sa-token integrate Redisson + + + + + + + + cn.dev33 + sa-token-core + + + + + org.redisson + redisson + + + + + diff --git a/sa-token-plugin/sa-token-redisson/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisson.java b/sa-token-plugin/sa-token-redisson/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisson.java new file mode 100644 index 000000000..2d8068ea9 --- /dev/null +++ b/sa-token-plugin/sa-token-redisson/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisson.java @@ -0,0 +1,167 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; + +import cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString; +import cn.dev33.satoken.util.SaFoxUtil; +import org.redisson.api.RBatch; +import org.redisson.api.RBucket; +import org.redisson.api.RBucketAsync; +import org.redisson.api.RedissonClient; +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.StringCodec; + +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Sa-Token 持久层实现 [ Redisson客户端、Redis存储 ] + * + *

    默认使用 {@link StringCodec},与业务 RedissonClient 的全局 codec 隔离。 + * + * @author 疯狂的狮子Li + * @author noear + * @since 1.34.0 + */ +public class SaTokenDaoForRedisson implements SaTokenDaoByObjectFollowString, SaTokenDao { + + /** + * redisson 客户端 + */ + public final RedissonClient redissonClient; + + /** + * 读写 Bucket 使用的 codec,默认 {@link StringCodec} + */ + public final Codec codec; + + /** + * 使用默认 {@link StringCodec} + */ + public SaTokenDaoForRedisson(RedissonClient redissonClient) { + this(redissonClient, StringCodec.INSTANCE); + } + + /** + * 指定 codec,与业务 RedissonClient 全局 codec 隔离 + * + * @param redissonClient Redisson 客户端 + * @param codec Bucket 编解码器 + * @since 1.46.0 + */ + public SaTokenDaoForRedisson(RedissonClient redissonClient, Codec codec) { + this.redissonClient = redissonClient; + this.codec = codec; + } + + /** + * 按指定 codec 获取 Bucket + */ + private RBucket getBucket(String key) { + return redissonClient.getBucket(key, codec); + } + + + /** + * 获取Value,如无返空 + */ + @Override + public String get(String key) { + return getBucket(key).get(); + } + + /** + * 写入Value,并设定存活时间 (单位: 秒) + */ + @Override + public void set(String key, String value, long timeout) { + if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // 判断是否为永不过期 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + getBucket(key).set(value); + } else { + RBatch batch = redissonClient.createBatch(); + RBucketAsync bucket = batch.getBucket(key, codec); + bucket.setAsync(value); + bucket.expireAsync(Duration.ofSeconds(timeout)); + batch.execute(); + } + } + + /** + * 修修改指定key-value键值对 (过期时间不变) + */ + @Override + public void update(String key, String value) { + long expire = getTimeout(key); + // -2 = 无此键 + if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.set(key, value, expire); + } + + /** + * 删除Value + */ + @Override + public void delete(String key) { + getBucket(key).delete(); + } + + /** + * 获取Value的剩余存活时间 (单位: 秒) + */ + @Override + public long getTimeout(String key) { + long timeout = getBucket(key).remainTimeToLive(); + return timeout < 0 ? timeout : timeout / 1000; + } + + /** + * 修改Value的剩余存活时间 (单位: 秒) + */ + @Override + public void updateTimeout(String key, long timeout) { + // 判断是否想要设置为永久 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + long expire = getTimeout(key); + if(expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.set(key, this.get(key), timeout); + } + return; + } + getBucket(key).expire(Duration.ofSeconds(timeout)); + } + + + /** + * 搜索数据 + */ + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + Stream stream = redissonClient.getKeys().getKeysStreamByPattern(prefix + "*" + keyword + "*"); + List list = stream.collect(Collectors.toList()); + return SaFoxUtil.searchList(list, start, size, sortType); + } +} diff --git a/sa-token-plugin/sa-token-redisx/README.md b/sa-token-plugin/sa-token-redisx/README.md new file mode 100644 index 000000000..eb83983d7 --- /dev/null +++ b/sa-token-plugin/sa-token-redisx/README.md @@ -0,0 +1,36 @@ + +sa-token-redisx 是中立的扩展。可任何应用开发框架下使用(springboot, solon, jfinal 等..) + +### 使用示例 + +#### 1.配置 + +```yaml +sa-token: #名字可以随意取 + redis: + server: "localhost:6379" + password: 123456 + db: 1 +# serializer: "org.noear.redisx.utils.SerializerJson" #指定自定义序列化实现(默认为 SerializerDefault) +``` + +#### 2.代码 + +**注入风格** + +```java +@Configuration +public class Config { + @Bean + public SaTokenDao saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoOfRedis saTokenDao) { + return saTokenDao; + } +} +``` + +**手动风格** + +```java +SaTokenDaoOfRedis saTokenDao = new SaTokenDaoOfRedis(props); +SaManager.setSaTokenDao(saTokenDao); +``` \ No newline at end of file diff --git a/sa-token-plugin/sa-token-redisx/pom.xml b/sa-token-plugin/sa-token-redisx/pom.xml new file mode 100644 index 000000000..88f5896ea --- /dev/null +++ b/sa-token-plugin/sa-token-redisx/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-redisx + sa-token-redisx + sa-token integrate redis + + + + + cn.dev33 + sa-token-core + + + + org.noear + redisx + + + + org.noear + solon-test + test + + + + + + diff --git a/sa-token-plugin/sa-token-redisx/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisx.java b/sa-token-plugin/sa-token-redisx/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisx.java new file mode 100644 index 000000000..ea5e51e4d --- /dev/null +++ b/sa-token-plugin/sa-token-redisx/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisx.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.dao; + +import cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString; +import cn.dev33.satoken.util.SaFoxUtil; +import org.noear.redisx.RedisClient; +import org.noear.redisx.plus.RedisBucket; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +/** + * SaTokenDao 的 redis 适配(基于json序列化,不能完全精准还原所有类型) + * + * @author noear + * @since 1.34.0 + * @since 1.41.0 + */ +public class SaTokenDaoForRedisx implements SaTokenDaoByObjectFollowString, SaTokenDao { + private final RedisBucket redisBucket; + + public SaTokenDaoForRedisx(Properties props) { + this(new RedisClient(props)); + } + + public SaTokenDaoForRedisx(RedisClient redisClient) { + redisBucket = redisClient.getBucket(); + } + + /** + * 获取Value,如无返空 + */ + @Override + public String get(String key) { + return redisBucket.get(key); + } + + /** + * 写入Value,并设定存活时间 (单位: 秒) + */ + @Override + public void set(String key, String value, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + + // 判断是否为永不过期 + if (timeout == SaTokenDao.NEVER_EXPIRE) { + redisBucket.store(key, value); + } else { + redisBucket.store(key, value, (int) timeout); + } + } + + /** + * 修改指定key-value键值对 (过期时间不变) + */ + @Override + public void update(String key, String value) { + long expire = getTimeout(key); + // -2 = 无此键 + if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + + this.set(key, value, expire); + } + + /** + * 删除Value + */ + @Override + public void delete(String key) { + redisBucket.remove(key); + } + + /** + * 获取Value的剩余存活时间 (单位: 秒) + */ + @Override + public long getTimeout(String key) { + return redisBucket.ttl(key); + } + + /** + * 修改Value的剩余存活时间 (单位: 秒) + */ + @Override + public void updateTimeout(String key, long timeout) { + // 判断是否想要设置为永久 + if (timeout == SaTokenDao.NEVER_EXPIRE) { + long expire = getTimeout(key); + if (expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.set(key, this.get(key), timeout); + } + return; + } + + + redisBucket.delay(key, (int) timeout); + } + + /** + * 搜索数据 + */ + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + Set keys = redisBucket.keys(prefix + "*" + keyword + "*"); + List list = new ArrayList<>(keys); + return SaFoxUtil.searchList(list, start, size, sortType); + } +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-redisx/src/test/java/demo/App.java b/sa-token-plugin/sa-token-redisx/src/test/java/demo/App.java new file mode 100644 index 000000000..ac68f579b --- /dev/null +++ b/sa-token-plugin/sa-token-redisx/src/test/java/demo/App.java @@ -0,0 +1,12 @@ +package demo; + +import org.noear.solon.Solon; + +/** + * @author noear 2022/3/30 created + */ +public class App { + public static void main(String[] args) { + Solon.start(App.class, args); + } +} diff --git a/sa-token-plugin/sa-token-redisx/src/test/java/demo/Config.java b/sa-token-plugin/sa-token-redisx/src/test/java/demo/Config.java new file mode 100644 index 000000000..84ad39ff6 --- /dev/null +++ b/sa-token-plugin/sa-token-redisx/src/test/java/demo/Config.java @@ -0,0 +1,27 @@ +package demo; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisx; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * @author noear 2022/3/30 created + */ +@Configuration +public class Config { + @Bean + public void saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoForRedisx saTokenDao) { + //手动操作,可适用于任何框架 + SaManager.setSaTokenDao(saTokenDao); + } + + + @Bean + public SaTokenDao saTokenDaoInit2(@Inject("${sa-token.redis}") SaTokenDaoForRedisx saTokenDao) { + //Solon 项目,可用此案 + return saTokenDao; + } +} diff --git a/sa-token-plugin/sa-token-redisx/src/test/resources/app.yml b/sa-token-plugin/sa-token-redisx/src/test/resources/app.yml new file mode 100644 index 000000000..e71d125c0 --- /dev/null +++ b/sa-token-plugin/sa-token-redisx/src/test/resources/app.yml @@ -0,0 +1,10 @@ + + + + +sa-token: #名字可以随意取 + redis: + server: "localhost:6379" + password: 123456 + db: 1 + maxTotal: 200 diff --git a/sa-token-plugin/sa-token-serializer-features/pom.xml b/sa-token-plugin/sa-token-serializer-features/pom.xml new file mode 100644 index 000000000..50abfb4b2 --- /dev/null +++ b/sa-token-plugin/sa-token-serializer-features/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-serializer-features + sa-token-serializer-features + sa-token-serializer-features + + + + + cn.dev33 + sa-token-core + + + + + + diff --git a/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSerializerFeatures.java b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSerializerFeatures.java new file mode 100644 index 000000000..814deb569 --- /dev/null +++ b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSerializerFeatures.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +/** + * SaToken 插件安装:自定义序列化器 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginForSerializerFeatures implements SaTokenPlugin { + + @Override + public void install() { + // 默认不注册,需要开发者手动注册去选择 + // SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan()); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseCustomCharacters.java b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseCustomCharacters.java new file mode 100644 index 000000000..0b2695091 --- /dev/null +++ b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseCustomCharacters.java @@ -0,0 +1,140 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer; + +import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdk; + +import java.util.HashMap; +import java.util.Map; + +/** + * 序列化器,base64 算法,采用自定义字符集 + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerForBase64UseCustomCharacters implements SaSerializerTemplateForJdk { + + // 自定义字符集,需确保包含64个中文字符 + public String CUSTOM_CHARS; + + // 填充符,确保不在字符集中 + public char PAD_CHAR; + + public SaSerializerForBase64UseCustomCharacters(String customChars, char padChar) { + if (customChars.length() != 64) { + throw new IllegalArgumentException("自定义字符集长度必须为64"); + } + if (customChars.indexOf(padChar) != -1) { + throw new IllegalArgumentException("填充符不能在自定义字符集中"); + } + this.CUSTOM_CHARS = customChars; + this.PAD_CHAR = padChar; + } + + @Override + public String bytesToString(byte[] data) { + + StringBuilder encoded = new StringBuilder(); + int length = data.length; + int i = 0; + + // 处理完整的3字节组 + while (i < length - 2) { + int byte1 = data[i++] & 0xFF; + int byte2 = data[i++] & 0xFF; + int byte3 = data[i++] & 0xFF; + + int combined = (byte1 << 16) | (byte2 << 8) | byte3; + encoded.append(CUSTOM_CHARS.charAt((combined >> 18) & 0x3F)); + encoded.append(CUSTOM_CHARS.charAt((combined >> 12) & 0x3F)); + encoded.append(CUSTOM_CHARS.charAt((combined >> 6) & 0x3F)); + encoded.append(CUSTOM_CHARS.charAt(combined & 0x3F)); + } + + // 处理剩余字节(0、1或2个) + int remaining = length - i; + if (remaining > 0) { + int byte1 = data[i++] & 0xFF; + int byte2 = remaining > 1 ? data[i++] & 0xFF : 0; + + int combined = (byte1 << 16) | (byte2 << 8); + encoded.append(CUSTOM_CHARS.charAt((combined >> 18) & 0x3F)); + encoded.append(CUSTOM_CHARS.charAt((combined >> 12) & 0x3F)); + + if (remaining == 1) { + encoded.append(PAD_CHAR).append(PAD_CHAR); + } else { + encoded.append(CUSTOM_CHARS.charAt((combined >> 6) & 0x3F)); + encoded.append(PAD_CHAR); + } + } + + return encoded.toString(); + } + + @Override + public byte[] stringToBytes(String encodedStr) { + if (CUSTOM_CHARS.length() != 64) { + throw new IllegalStateException("自定义字符集长度必须为64"); + } + + Map charMap = new HashMap<>(); + for (int i = 0; i < CUSTOM_CHARS.length(); i++) { + charMap.put(CUSTOM_CHARS.charAt(i), i); + } + + int length = encodedStr.length(); + if (length % 4 != 0) { + throw new IllegalArgumentException("编码字符串长度无效"); + } + + // 计算填充符数量 + int paddingCount = 0; + for (int i = length - 1; i >= 0 && encodedStr.charAt(i) == PAD_CHAR; i--) { + paddingCount++; + } + + int numGroups = length / 4; + byte[] decoded = new byte[numGroups * 3 - paddingCount]; + int decodedIndex = 0; + + for (int group = 0; group < numGroups; group++) { + int[] indices = new int[4]; + for (int j = 0; j < 4; j++) { + char c = encodedStr.charAt(group * 4 + j); + if (c == PAD_CHAR) { + indices[j] = 0; // 填充符处理为0,后续根据paddingCount调整 + } else { + Integer index = charMap.get(c); + if (index == null) { + throw new IllegalArgumentException("无效字符: " + c); + } + indices[j] = index; + } + } + + int combined = (indices[0] << 18) | (indices[1] << 12) | (indices[2] << 6) | indices[3]; + for (int k = 0; k < 3; k++) { + if (decodedIndex < decoded.length) { + decoded[decodedIndex++] = (byte) ((combined >> (16 - 8 * k)) & 0xFF); + } + } + } + + return decoded; + } +} diff --git a/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseEmoji.java b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseEmoji.java new file mode 100644 index 000000000..60265d088 --- /dev/null +++ b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseEmoji.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer; + +import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdk; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 序列化器,base64 算法,采用 64 个 Emoji 小黄脸作为元字符集,无填充字符 + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerForBase64UseEmoji implements SaSerializerTemplateForJdk { + + private final List EMOJI_TABLE = new ArrayList<>(); // 编码表 + private final Map EMOJI_MAP = new HashMap<>(); // 解码表 + + public SaSerializerForBase64UseEmoji() { + // 初始化编码表(64个Emoji,U+1F600 到 U+1F63F) + for (int i = 0; i < 64; i++) { + int codePoint = 0x1F600 + i; + String emoji = new String(Character.toChars(codePoint)); + EMOJI_TABLE.add(emoji); + EMOJI_MAP.put(emoji, i); + } + } + + @Override + public String bytesToString(byte[] data) { + StringBuilder binaryStr = new StringBuilder(); + for (byte b : data) { + binaryStr.append(String.format("%8s", Integer.toBinaryString(b & 0xFF)) + .replace(' ', '0')); + } + + // 补零到6的倍数 + int bitLength = binaryStr.length(); + int paddingBits = (6 - (bitLength % 6)) % 6; + for (int i = 0; i < paddingBits; i++) { + binaryStr.append('0'); + } + + // 转换为索引 + List indices = new ArrayList<>(); + for (int i = 0; i < binaryStr.length(); i += 6) { + String chunk = binaryStr.substring(i, Math.min(i + 6, binaryStr.length())); + indices.add(Integer.parseInt(chunk, 2)); + } + + // 拼接Emoji + StringBuilder result = new StringBuilder(); + for (int index : indices) { + result.append(EMOJI_TABLE.get(index)); + } + return result.toString(); + } + + @Override + public byte[] stringToBytes(String encoded) { + List indices = new ArrayList<>(); + + // 提取索引(每个Emoji占2个char) + for (int i = 0; i < encoded.length(); ) { + if (i + 1 >= encoded.length()) break; + String emoji = encoded.substring(i, i + 2); + i += 2; + + Integer index = EMOJI_MAP.get(emoji); + if (index == null) { + throw new IllegalArgumentException("非法Emoji: " + emoji); + } + indices.add(index); + } + + // 转换为二进制字符串 + StringBuilder binaryStr = new StringBuilder(); + for (int index : indices) { + binaryStr.append(String.format("%6s", Integer.toBinaryString(index)) + .replace(' ', '0')); + } + + // 转换为字节数组(自动处理末尾补零) + List bytes = new ArrayList<>(); + for (int i = 0; i < binaryStr.length(); i += 8) { + int endIndex = Math.min(i + 8, binaryStr.length()); + String byteStr = binaryStr.substring(i, endIndex); + if (byteStr.length() < 8) break; // 忽略末尾不足8位的部分 + bytes.add((byte) Integer.parseInt(byteStr, 2)); + } + + byte[] result = new byte[bytes.size()]; + for (int i = 0; i < bytes.size(); i++) { + result[i] = bytes.get(i); + } + return result; + } + +} diff --git a/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UsePeriodicTable.java b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UsePeriodicTable.java new file mode 100644 index 000000000..1b2661075 --- /dev/null +++ b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UsePeriodicTable.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer; + +/** + * 序列化器,base64 算法,采用 元素周期表 前六十四位作为元字符集 + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerForBase64UsePeriodicTable extends SaSerializerForBase64UseCustomCharacters { + + public SaSerializerForBase64UsePeriodicTable() { + super( + // 自定义字符集,需确保包含64个不重复的字符 + "氢氦锂铍硼碳氮氧" + + "氟氖钠镁铝硅磷硫" + + "氯氩钾钙钪钛钒铬" + + "锰铁钴镍铜锌镓锗" + + "砷硒溴氪铷锶钇锆" + + "铌钼锝钌铑钯银镉" + + "铟锡锑碲碘氙铯钡" + + "镧铈镨钕钷钐铕钆" + , + // 填充符,确保不在字符集中 + '鿫' + ); + } + +} diff --git a/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseSpecialSymbols.java b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseSpecialSymbols.java new file mode 100644 index 000000000..0ee4c14ca --- /dev/null +++ b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseSpecialSymbols.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer; + +/** + * 序列化器,base64 算法,采用64个特殊符号作为元字符集 + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerForBase64UseSpecialSymbols extends SaSerializerForBase64UseCustomCharacters { + + public SaSerializerForBase64UseSpecialSymbols() { + super( + // 自定义字符集,需确保包含64个不重复的字符 + "▲▼●◆■★▶◀" + + "♠♥♦♣▁▂▃▄" + + "▅▆▇█▏▎▍▌" + + "▋▊▉▬〓◤◥◣" + + "◢♩♪♫♬§〼↖" + + "↑↗←→↙↓↘☴" + + "☲☷☳☱☶☵☰◐" + + "◑☀☼▪•‥…∷" + , + // 填充符,确保不在字符集中 + '※' + ); + } + +} diff --git a/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseTianGan.java b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseTianGan.java new file mode 100644 index 000000000..a778d4161 --- /dev/null +++ b/sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseTianGan.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.serializer; + +/** + * 序列化器,base64 算法,采用 十大天干、十二地支 等64个中文字符作为元字符集 + * + * @author click33 + * @since 1.41.0 + */ +public class SaSerializerForBase64UseTianGan extends SaSerializerForBase64UseCustomCharacters { + + public SaSerializerForBase64UseTianGan() { + super( + // 自定义字符集,需确保包含64个不重复的字符 + "甲乙丙丁戊己庚辛" + + "壬癸子丑寅卯辰巳" + + "午未申酉戌亥乾坤" + + "震巽坎离艮兑金木" + + "水火土天地日月山" + + "石田风雷电霜雾露" + + "东南西北中信谷岚" + + "宇宙羽泰铭安鹤纤" + , + // 填充符,确保不在字符集中 + '口' + ); + } + +} diff --git a/sa-token-plugin/sa-token-serializer-features/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-serializer-features/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..57674bf49 --- /dev/null +++ b/sa-token-plugin/sa-token-serializer-features/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForSerializerFeatures \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sign/pom.xml b/sa-token-plugin/sa-token-sign/pom.xml new file mode 100644 index 000000000..02e587dca --- /dev/null +++ b/sa-token-plugin/sa-token-sign/pom.xml @@ -0,0 +1,23 @@ + + + + sa-token-plugin + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + + sa-token-sign + sa-token-sign + sa-token Sign + + + + cn.dev33 + sa-token-core + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSign.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSign.java new file mode 100644 index 000000000..70043e8dd --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSign.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.sign.annotation.handle.SaCheckSignHandler; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; + +/** + * SaToken 插件安装:API 参数签名 组件 + * + * @author click33 + * @since 1.43.0 + */ +public class SaTokenPluginForSign implements SaTokenPlugin { + + @Override + public void install() { + // 安装 API 参数签名 鉴权注解 + SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckSignHandler()); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/SaSignManager.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/SaSignManager.java new file mode 100644 index 000000000..07ee60581 --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/SaSignManager.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.template.SaSignTemplate; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 管理 Sa-Token API 参数签名 所有全局组件 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSignManager { + + /** + * API 参数签名 配置 Bean + */ + private static volatile SaSignConfig config; + public static SaSignConfig getConfig() { + if (config == null) { + // 初始化默认值 + synchronized (SaSignManager.class) { + if (config == null) { + setConfig(new SaSignConfig()); + } + } + } + return config; + } + public static void setConfig(SaSignConfig config) { + SaSignManager.config = config; + } + + /** + * API 签名配置 多实例 配置 Bean + */ + private static volatile Map signMany; + public static Map getSignMany() { + if (signMany == null) { + // 初始化默认值 + synchronized (SaSignManager.class) { + if (signMany == null) { + setSignMany(new LinkedHashMap<>()); + } + } + } + return signMany; + } + public static void setSignMany(Map signMany) { + SaSignManager.signMany = signMany; + } + + /** + * API 参数签名 + */ + private volatile static SaSignTemplate saSignTemplate; + public static void setSaSignTemplate(SaSignTemplate saSignTemplate) { + SaSignManager.saSignTemplate = saSignTemplate; + SaTokenEventCenter.doRegisterComponent("SaSignTemplate", saSignTemplate); + } + public static SaSignTemplate getSaSignTemplate() { + if (saSignTemplate == null) { + synchronized (SaManager.class) { + if (saSignTemplate == null) { + SaSignManager.saSignTemplate = new SaSignTemplate(); + } + } + } + return saSignTemplate; + } + +} diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/SaCheckSign.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/SaCheckSign.java new file mode 100644 index 000000000..73c37392c --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/SaCheckSign.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * API 参数签名校验:必须具有正确的参数签名才可以通过校验 + * + *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.41.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD,ElementType.TYPE}) +public @interface SaCheckSign { + + /** + * 多实例下的 appid 值,用于区分不同的实例,如不填写则代表使用全局默认实例
    + * 允许以 #{} 的形式指定为请求参数,如:#{appid} + * + * @return / + */ + String appid() default ""; + + /** + * 指定参与签名的参数有哪些,如果不填写则默认为全部参数 + * + * @return / + */ + String [] verifyParams() default {}; + +} diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/handle/SaCheckSignHandler.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/handle/SaCheckSignHandler.java new file mode 100644 index 000000000..c488601c6 --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/handle/SaCheckSignHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.annotation.handle; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.sign.annotation.SaCheckSign; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.sign.template.SaSignMany; + +import java.lang.reflect.AnnotatedElement; + +/** + * 注解 SaCheckSign 的处理器 + * + * @author click33 + * @since 1.41.0 + */ +public class SaCheckSignHandler implements SaAnnotationHandlerInterface { + + @Override + public Class getHandlerAnnotationClass() { + return SaCheckSign.class; + } + + @Override + public void checkMethod(SaCheckSign at, AnnotatedElement element) { + _checkMethod(at.appid(), at.verifyParams()); + } + + public static void _checkMethod(String appid, String[] verifyParams) { + SaRequest req = SaHolder.getRequest(); + // 如果 appid 为 #{} 格式,则从请求参数中获取 + if(appid.startsWith("#{") && appid.endsWith("}")) { + String reqParamName = appid.substring(2, appid.length() - 1); + appid = req.getParam(reqParamName); + } + SaSignMany.getSignTemplate(appid).checkRequest(req, verifyParams); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignConfig.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignConfig.java new file mode 100644 index 000000000..be5d54141 --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignConfig.java @@ -0,0 +1,209 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.config; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.fun.SaParamRetFunction; +import cn.dev33.satoken.secure.SaSecureUtil; + +/** + * Sa-Token API 接口签名/验签 相关配置类 + * + * @author click33 + * @since 1.34.0 + */ +public class SaSignConfig { + + /** + * API 调用签名秘钥 + */ + private String secretKey; + + /** + * 接口调用时的时间戳允许的差距(单位:ms),-1 代表不校验差距,默认15分钟 + * + *

    比如此处你配置了60秒,当一个请求从 client 发起后,如果 server 端60秒内没有处理,60秒后再想处理就无法校验通过了。

    + *

    timestamp + nonce 有效防止重放攻击。

    + */ + private long timestampDisparity = 1000 * 60 * 15; + + /** + * 对 fullStr 的摘要算法 + */ + private String digestAlgo = "md5"; + + public SaSignConfig() { + } + + /** + * 构造函数 + * @param secretKey 秘钥 + */ + public SaSignConfig(String secretKey) { + this.secretKey = secretKey; + } + + + // -------------- 扩展方法 + + /** + * 计算保存 nonce 时应该使用的 ttl,单位:秒 + * @return / + */ + public long getSaveNonceExpire() { + // 如果 timestampDisparity >= 0,则 nonceTtl 的值等于 timestampDisparity 的值,单位转秒 + if(timestampDisparity >= 0) { + return timestampDisparity / 1000; + } + // 否则,nonceTtl 的值为 24 小时 + else { + return 60 * 60 * 24; + } + } + + /** + * 复制对象 + * @return / + */ + public SaSignConfig copy() { + SaSignConfig obj = new SaSignConfig(); + obj.secretKey = this.secretKey; + obj.timestampDisparity = this.timestampDisparity; + obj.digestAlgo = this.digestAlgo; + obj.digestMethod = this.digestMethod; + return obj; + } + + + // -------------- 策略函数 + + /** + * 对 fullStr 的摘要算法函数 + */ + public SaParamRetFunction digestMethod = (fullStr) -> { + // md5 + if(digestAlgo.equalsIgnoreCase("md5")) { + return SaSecureUtil.md5(fullStr); + } + // sha1 + if(digestAlgo.equalsIgnoreCase("sha1")) { + return SaSecureUtil.sha1(fullStr); + } + // sha256 + if(digestAlgo.equalsIgnoreCase("sha256")) { + return SaSecureUtil.sha256(fullStr); + } + // sha384 + if(digestAlgo.equalsIgnoreCase("sha384")) { + return SaSecureUtil.sha384(fullStr); + } + // sha512 + if(digestAlgo.equalsIgnoreCase("sha512")) { + return SaSecureUtil.sha512(fullStr); + } + // 未知 + throw new SaTokenException("不支持的摘要算法:" + digestAlgo + ",你可以自定义摘要算法函数实现"); + }; + + /** + * 设置: 对 fullStr 的摘要算法函数 + * + * @param digestMethod / + * @return 对象自身 + */ + public SaSignConfig setDigestMethod(SaParamRetFunction digestMethod) { + this.digestMethod = digestMethod; + return this; + } + + + + // -------------- get/set + + /** + * 获取 API 调用签名秘钥 + * + * @return / + */ + public String getSecretKey() { + return this.secretKey; + } + + /** + * 设置 API 调用签名秘钥 + * + * @param secretKey / + * @return 对象自身 + */ + public SaSignConfig setSecretKey(String secretKey) { + this.secretKey = secretKey; + return this; + } + + /** + * 获取 接口调用时的时间戳允许的差距(单位:ms),-1 代表不校验差距,默认15分钟 + * + *

    比如此处你配置了60秒,当一个请求从 client 发起后,如果 server 端60秒内没有处理,60秒后再想处理就无法校验通过了。

    + *

    timestamp + nonce 有效防止重放攻击。

    + * + * @return / + */ + public long getTimestampDisparity() { + return this.timestampDisparity; + } + + /** + * 设置 接口调用时的时间戳允许的差距(单位:ms),-1 代表不校验差距,默认15分钟 + * + *

    比如此处你配置了60秒,当一个请求从 client 发起后,如果 server 端60秒内没有处理,60秒后再想处理就无法校验通过了。

    + *

    timestamp + nonce 有效防止重放攻击。

    + * + * @param timestampDisparity / + * @return 对象自身 + */ + public SaSignConfig setTimestampDisparity(long timestampDisparity) { + this.timestampDisparity = timestampDisparity; + return this; + } + + /** + * 获取 对 fullStr 的摘要算法 + * + * @return digestAlgo 对 fullStr 的摘要算法 + */ + public String getDigestAlgo() { + return this.digestAlgo; + } + + /** + * 设置 对 fullStr 的摘要算法 + * @param digestAlgo / + * @return / + */ + public SaSignConfig setDigestAlgo(String digestAlgo) { + this.digestAlgo = digestAlgo; + return this; + } + + @Override + public String toString() { + return "SaSignConfig [" + + "secretKey=" + secretKey + + ", timestampDisparity=" + timestampDisparity + + "]"; + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignManyConfigWrapper.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignManyConfigWrapper.java new file mode 100644 index 000000000..4d00e9881 --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignManyConfigWrapper.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * SaSignManyConfig 配置包装类,以更方便框架完成属性注入操作 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSignManyConfigWrapper { + + public Map signMany = new LinkedHashMap<>(); + + /** + * 获取 + * + * @return signMany + */ + public Map getSignMany() { + return this.signMany; + } + + /** + * 设置 + * + * @param signMany + */ + public void setSignMany(Map signMany) { + this.signMany = signMany; + } + + @Override + public String toString() { + return "SaSignManyConfigWrapper{" + + "signMany=" + signMany + + '}'; + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/error/SaSignErrorCode.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/error/SaSignErrorCode.java new file mode 100644 index 000000000..db2572509 --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/error/SaSignErrorCode.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.error; + +/** + * 定义 sa-token-sign 模块所有异常细分状态码 + * + * @author click33 + * @since 1.43.0 + */ +public interface SaSignErrorCode { + + /** 参与参数签名的秘钥不可为空 */ + int CODE_12201 = 12201; + + /** 给定的签名无效 */ + int CODE_12202 = 12202; + + /** timestamp 超出允许的范围 */ + int CODE_12203 = 12203; + + /** 未找到对应 appid 的 SaSignConfig */ + int CODE_12211 = 12211; + +} diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/exception/SaSignException.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/exception/SaSignException.java new file mode 100644 index 000000000..602b0f9f8 --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/exception/SaSignException.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.exception; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 一个异常:代表 API 参数签名校验失败 + * + * @author click33 + * @since 1.34.0 + */ +public class SaSignException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** + * 一个异常:代表 API 参数签名校验失败 + * @param message 异常描述 + */ + public SaSignException(String message) { + super(message); + } + + /** + * 断言 flag 不为 true,否则抛出 message 异常 + * @param flag 表达式 + * @param message 异常信息 + */ + public static void notTrue(boolean flag, String message) { + // notTrue + if(flag) { + throw new SaSignException(message); + } + } + + /** + * 断言 value 不为空,否则抛出 message 异常 + * @param value 值 + * @param message 异常信息 + */ + public static void notEmpty(Object value, String message) { + if(SaFoxUtil.isEmpty(value)) { + throw new SaSignException(message); + } + } + + + // ------------------- 已过期 ------------------- + + /** + * 如果flag==true,则抛出message异常 + *

    已过期:请使用 notTrue 代替,用法不变

    + * + * @param flag 标记 + * @param message 异常信息 + */ + @Deprecated + public static void throwBy(boolean flag, String message) { + if(flag) { + throw new SaSignException(message); + } + } + + /** + * 如果 value isEmpty,则抛出 message 异常 + *

    已过期:请使用 notEmpty 代替,用法不变

    + * + * @param value 值 + * @param message 异常信息 + */ + @Deprecated + public static void throwByNull(Object value, String message) { + if(SaFoxUtil.isEmpty(value)) { + throw new SaSignException(message); + } + } + + +} diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignMany.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignMany.java new file mode 100644 index 000000000..d281076d0 --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignMany.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.template; + +import cn.dev33.satoken.fun.SaParamRetFunction; +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.error.SaSignErrorCode; +import cn.dev33.satoken.sign.exception.SaSignException; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * API 参数签名算法 - 多实例总控类 + * + * @author click33 + * @since 1.41.0 + */ +public class SaSignMany { + + /** + * 根据 appid 获取 SaSignConfig,允许自定义 + */ + public static SaParamRetFunction findSaSignConfigMethod = (appid) -> { + return SaSignManager.getSignMany().get(appid); + }; + + /** + * 获取 SaSignTemplate,根据 appid + * @param appid / + * @return / + */ + public static SaSignTemplate getSignTemplate(String appid) { + + // appid 为空,返回全局默认 SaSignTemplate + if(SaFoxUtil.isEmpty(appid)){ + return SaSignManager.getSaSignTemplate(); + } + + // 获取 SaSignConfig + SaSignConfig config = findSaSignConfigMethod.run(appid); + if(config == null){ + throw new SaSignException("未找到签名配置,appid=" + appid).setCode(SaSignErrorCode.CODE_12211); + } + + // 创建 SaSignTemplate 并返回 + return new SaSignTemplate(config); + } + +} diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignTemplate.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignTemplate.java new file mode 100644 index 000000000..973b215c6 --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignTemplate.java @@ -0,0 +1,416 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.template; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.sign.error.SaSignErrorCode; +import cn.dev33.satoken.sign.exception.SaSignException; +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.Map; +import java.util.TreeMap; + +import static cn.dev33.satoken.SaManager.log; + +/** + * API 参数签名算法,在跨系统接口调用时防参数篡改、防重放攻击。 + * + *

    + * 以 SSO 数据拉取为例,流程大致如下: + *
    1. 以 md5( loginId={账号id}8nonce={随机字符串}8timestamp={13位时间戳}8key={secretkey秘钥} ) 生成签名 sign。 + *
    2. 将 sign 作为参数,拼接到请求地址后面,如:http://xxx.com?loginId=100018nonce=xxx8timestamp=xxx8sign=xxx。 + *
    3. 服务端接收到请求后,以同样的算法生成一次 sign 。 + *
    4. 对比两次 sign 是否一致,一致则通过,否则拒绝 。 + *

    + * + * @author click33 + * @since 1.30.0 + */ +public class SaSignTemplate { + + public SaSignTemplate() { + } + + /** + * 构造函数 + * @param signConfig 签名参数配置对象 + */ + public SaSignTemplate(SaSignConfig signConfig) { + this.signConfig = signConfig; + } + + // ----------- 签名配置 + + SaSignConfig signConfig; + + /** + * 获取:API 签名配置 + * @return / + */ + public SaSignConfig getSignConfig() { + return signConfig; + } + + /** + * 获取:API 签名配置: + * 1. 如果用户自定义了 signConfig ,则使用用户自定义的。 + * 2. 否则使用全局默认配置。 + * + * @return / + */ + public SaSignConfig getSignConfigOrGlobal() { + // 如果用户自定义了 signConfig ,则使用用户自定义的 + if(signConfig != null) { + return signConfig; + } + // 否则使用全局默认配置 + return SaSignManager.getConfig(); + } + + /** + * 获取:API 签名配置的秘钥 + * @return / + */ + public String getSecretKey() { + return getSignConfigOrGlobal().getSecretKey(); + } + + /** + * 设置:API 签名配置 + * @param signConfig / + */ + public SaSignTemplate setSignConfig(SaSignConfig signConfig) { + this.signConfig = signConfig; + return this; + } + + + // ----------- 自定义使用的参数名称 (不声明final,允许开发者自定义修改) + + public static String key = "key"; + public static String timestamp = "timestamp"; + public static String nonce = "nonce"; + public static String sign = "sign"; + + + // ----------- 拼接参数 + + /** + * 将所有参数连接成一个字符串(不排序),形如:b=28a=18c=3 + * @param paramsMap 参数列表 + * @return 拼接出的参数字符串 + */ + public String joinParams(Map paramsMap) { + + // 按照 k1=v1&k2=v2&k3=v3 排列 + StringBuilder sb = new StringBuilder(); + for (String key : paramsMap.keySet()) { + Object value = paramsMap.get(key); + if( ! SaFoxUtil.isEmpty(value) ) { + sb.append(key).append("=").append(value).append("&"); + } + } + + // 删除最后一位 & + if(sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + + // . + return sb.toString(); + } + + /** + * 将所有参数按照字典顺序连接成一个字符串,形如:a=18b=28c=3 + * @param paramsMap 参数列表 + * @return 拼接出的参数字符串 + */ + public String joinParamsDictSort(Map paramsMap) { + // 保证字段按照字典顺序排列 + if( ! (paramsMap instanceof TreeMap) ) { + paramsMap = new TreeMap<>(paramsMap); + } + + // 拼接 + return joinParams(paramsMap); + } + + + // ----------- 创建签名 + + /** + * 创建签名:md5(paramsStr + keyStr) + * @param paramsMap 参数列表 + * @return 签名 + */ + public String createSign(Map paramsMap) { + String secretKey = getSecretKey(); + SaSignException.notEmpty(secretKey, "参与参数签名的秘钥不可为空", SaSignErrorCode.CODE_12201); + + // 如果调用者不小心传入了 sign 参数,则此处需要将 sign 参数排除在外 + if(paramsMap.containsKey(sign)) { + // 为了保证不影响原有的 paramsMap,此处需要再复制一份 + paramsMap = new TreeMap<>(paramsMap); + paramsMap.remove(sign); + } + + // 计算签名 + String paramsStr = joinParamsDictSort(paramsMap); + String fullStr = paramsStr + "&" + key + "=" + secretKey; + String signStr = digestFullStr(fullStr); + + // 输入日志,方便调试 + log.debug("fullStr:{}", fullStr); + log.debug("signStr:{}", signStr); + + // 返回 + return signStr; + } + + /** + * 使用摘要算法创建签名 + * @param fullStr 待摘要的字符串 + * @return 签名 + */ + public String digestFullStr(String fullStr) { + return getSignConfigOrGlobal().digestMethod.run(fullStr); + } + + /** + * 给 paramsMap 追加 timestamp、nonce、sign 三个参数 + * @param paramsMap 参数列表 + * @return 加工后的参数列表 + */ + public Map addSignParams(Map paramsMap) { + paramsMap.put(timestamp, String.valueOf(System.currentTimeMillis())); + paramsMap.put(nonce, SaFoxUtil.getRandomString(32)); + paramsMap.put(sign, createSign(paramsMap)); + return paramsMap; + } + + /** + * 给 paramsMap 追加 timestamp、nonce、sign 三个参数,并转换为参数字符串,形如: + * data=xxx8nonce=xxx8timestamp=xxx8sign=xxx + * @param paramsMap 参数列表 + * @return 加工后的参数列表 转化为的参数字符串 + */ + public String addSignParamsAndJoin(Map paramsMap) { + // 追加参数 + paramsMap = addSignParams(paramsMap); + + // 拼接参数 + return joinParams(paramsMap); + } + + + // ----------- 校验签名 + + /** + * 判断:指定时间戳与当前时间戳的差距是否在允许的范围内 + * @param timestamp 待校验的时间戳 + * @return 是否在允许的范围内 + */ + public boolean isValidTimestamp(long timestamp) { + long allowDisparity = getSignConfigOrGlobal().getTimestampDisparity(); + long disparity = Math.abs(System.currentTimeMillis() - timestamp); + return allowDisparity == -1 || disparity <= allowDisparity; + } + + /** + * 校验:指定时间戳与当前时间戳的差距是否在允许的范围内,如果超出则抛出异常 + * @param timestamp 待校验的时间戳 + */ + public void checkTimestamp(long timestamp) { + if( ! isValidTimestamp(timestamp) ) { + throw new SaSignException("timestamp 超出允许的范围:" + timestamp).setCode(SaSignErrorCode.CODE_12203); + } + } + + /** + * 判断:随机字符串 nonce 是否有效。 + * 注意:同一 nonce 可以被多次判断有效,不会被缓存 + * @param nonce 待判断的随机字符串 + * @return 是否有效 + */ + public boolean isValidNonce(String nonce) { + // 为空代表无效 + if(SaFoxUtil.isEmpty(nonce)) { + return false; + } + + // 校验此 nonce 是否已被使用过 + String key = splicingNonceSaveKey(nonce); + return SaManager.getSaTokenDao().get(key) == null; + } + + /** + * 校验:随机字符串 nonce 是否有效,如果无效则抛出异常。 + * 注意:同一 nonce 只可以被校验通过一次,校验后将保存在缓存中,再次校验将无法通过 + * @param nonce 待校验的随机字符串 + */ + public void checkNonce(String nonce) { + // 为空代表无效 + if(SaFoxUtil.isEmpty(nonce)) { + throw new SaSignException("nonce 为空,无效"); + } + + // 校验此 nonce 是否已被使用过 + String key = splicingNonceSaveKey(nonce); + if(SaManager.getSaTokenDao().get(key) != null) { + throw new SaSignException("此 nonce 已被使用过,不可重复使用:" + nonce); + } + + // 校验通过后,将此 nonce 保存在缓存中,保证下次校验无法通过 + SaManager.getSaTokenDao().set(key, nonce, getSignConfigOrGlobal().getSaveNonceExpire() * 2 + 2); + } + + /** + * 判断:给定的参数 生成的签名是否为有效签名 + * @param paramsMap 参数列表 + * @param sign 待验证的签名 + * @return 签名是否有效 + */ + public boolean isValidSign(Map paramsMap, String sign) { + String theSign = createSign(paramsMap); + return theSign.equals(sign); + } + + /** + * 校验:给定的参数 生成的签名是否为有效签名,如果签名无效则抛出异常 + * @param paramsMap 参数列表 + * @param sign 待验证的签名 + */ + public void checkSign(Map paramsMap, String sign) { + if( ! isValidSign(paramsMap, sign) ) { + throw new SaSignException("无效签名:" + sign).setCode(SaSignErrorCode.CODE_12202); + } + } + + /** + * 判断:参数列表中的 nonce、timestamp、sign 是否均为合法的 + * @param paramMap 待校验的请求参数集合 + * @return 是否合法 + */ + @SuppressWarnings("all") + public boolean isValidParamMap(Map paramMap) { + // 获取必须的三个参数 + String timestampValue = paramMap.get(timestamp); + String nonceValue = paramMap.get(nonce); + String signValue = paramMap.get(sign); + + // 参数非空校验 + // 配置isCheckNonce=false时,可以不传 nonce + if(SaFoxUtil.isEmpty(timestampValue) || SaFoxUtil.isEmpty(signValue)) { + return false; + } + + // 三个值的校验必须全部通过 + return isValidTimestamp(Long.parseLong(timestampValue)) + && isValidNonce(nonceValue) + && isValidSign(paramMap, signValue); + } + + /** + * 校验:参数列表中的 nonce、timestamp、sign 是否均为合法的,如果不合法,则抛出对应的异常 + * @param paramMap 待校验的请求参数集合 + */ + public void checkParamMap(Map paramMap) { + // 获取必须的三个参数 + String timestampValue = paramMap.get(timestamp); + String nonceValue = paramMap.get(nonce); + String signValue = paramMap.get(sign); + + // 参数非空校验 + SaSignException.notEmpty(timestampValue, "缺少 timestamp 字段"); + SaSignException.notEmpty(nonceValue, "缺少 nonce 字段"); + SaSignException.notEmpty(signValue, "缺少 sign 字段"); + + // 依次校验三个参数 + checkTimestamp(Long.parseLong(timestampValue)); + checkNonce(nonceValue); + checkSign(paramMap, signValue); + + // 通过 √ + } + + + // ----------- Web 请求相关 封装 + + /** + * 判断:一个请求中的 nonce、timestamp、sign 是否均为合法的 + * @param request 待校验的请求对象 + * @param paramNames 指定参与签名的参数有哪些,如果不填写则默认为全部参数 + * @return 是否合法 + */ + public boolean isValidRequest(SaRequest request, String... paramNames) { + if(paramNames.length == 0) { + return isValidParamMap(request.getParamMap()); + } else { + return isValidParamMap(takeRequestParam(request, paramNames)); + } + } + + /** + * 校验:一个请求的 nonce、timestamp、sign 是否均为合法的,如果不合法,则抛出对应的异常 + * @param request 待校验的请求对象 + * @param paramNames 指定参与签名的参数有哪些,如果不填写则默认为全部参数 + */ + public void checkRequest(SaRequest request, String... paramNames) { + if (paramNames.length == 0) { + checkParamMap(request.getParamMap()); + } else { + checkParamMap(takeRequestParam(request, paramNames)); + } + } + + /** + * 从请求中提取指定的参数 + * @param request 请求对象 + * @param paramNames 指定的参数名称,不可为空,如果传入空数组则代表只拿 timestamp、nonce、sign 三个参数 + * @return 提取出的参数 + */ + protected Map takeRequestParam(SaRequest request, String [] paramNames) { + Map paramMap = new TreeMap<>(); + + // 此三个参数是必须获取的 + paramMap.put(timestamp, request.getParam(timestamp)); + paramMap.put(nonce, request.getParam(nonce)); + paramMap.put(sign, request.getParam(sign)); + + // 获取指定的参数 + for (String paramName : paramNames) { + paramMap.put(paramName, request.getParam(paramName)); + } + + // 返回 + return paramMap; + } + + // ------------------- 返回相应key ------------------- + + /** + * 拼接key:存储 nonce 时使用的 key + * @param nonce nonce 值 + * @return key + */ + public String splicingNonceSaveKey(String nonce) { + return SaManager.getConfig().getTokenName() + ":sign:nonce:" + nonce; + } + +} diff --git a/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignUtil.java b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignUtil.java new file mode 100644 index 000000000..d896288ab --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignUtil.java @@ -0,0 +1,179 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sign.template; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.sign.SaSignManager; + +import java.util.Map; + +/** + * API 参数签名算法 - 工具类 + * + * @author click33 + * @since 1.34.0 + */ +public class SaSignUtil { + + // ----------- 拼接参数 + + /** + * 将所有参数连接成一个字符串(不排序),形如:b=28a=18c=3 + * @param paramsMap 参数列表 + * @return 拼接出的参数字符串 + */ + public static String joinParams(Map paramsMap) { + return SaSignManager.getSaSignTemplate().joinParams(paramsMap); + } + + /** + * 将所有参数按照字典顺序连接成一个字符串,形如:a=18b=28c=3 + * @param paramsMap 参数列表 + * @return 拼接出的参数字符串 + */ + public static String joinParamsDictSort(Map paramsMap) { + return SaSignManager.getSaSignTemplate().joinParamsDictSort(paramsMap); + } + + + // ----------- 创建签名 + + /** + * 创建签名:md5(paramsStr + keyStr) + * @param paramsMap 参数列表 + * @return 签名 + */ + public static String createSign(Map paramsMap) { + return SaSignManager.getSaSignTemplate().createSign(paramsMap); + } + + /** + * 给 paramsMap 追加 timestamp、nonce、sign 三个参数 + * @param paramsMap 参数列表 + * @return 加工后的参数列表 + */ + public static Map addSignParams(Map paramsMap) { + return SaSignManager.getSaSignTemplate().addSignParams(paramsMap); + } + + /** + * 给 paramsMap 追加 timestamp、nonce、sign 三个参数,并转换为参数字符串,形如: + * data=xxx8nonce=xxx8timestamp=xxx8sign=xxx + * @param paramsMap 参数列表 + * @return 加工后的参数列表 转化为的参数字符串 + */ + public static String addSignParamsAndJoin(Map paramsMap) { + return SaSignManager.getSaSignTemplate().addSignParamsAndJoin(paramsMap); + } + + + // ----------- 校验签名 + + /** + * 判断:指定时间戳与当前时间戳的差距是否在允许的范围内 + * @param timestamp 待校验的时间戳 + * @return 是否在允许的范围内 + */ + public static boolean isValidTimestamp(long timestamp) { + return SaSignManager.getSaSignTemplate().isValidTimestamp(timestamp); + } + + /** + * 校验:指定时间戳与当前时间戳的差距是否在允许的范围内,如果超出则抛出异常 + * @param timestamp 待校验的时间戳 + */ + public static void checkTimestamp(long timestamp) { + SaSignManager.getSaSignTemplate().checkTimestamp(timestamp); + } + + /** + * 判断:随机字符串 nonce 是否有效。 + * 注意:同一 nonce 可以被多次判断有效,不会被缓存 + * @param nonce 待判断的随机字符串 + * @return 是否有效 + */ + public static boolean isValidNonce(String nonce) { + return SaSignManager.getSaSignTemplate().isValidNonce(nonce); + } + + /** + * 校验:随机字符串 nonce 是否有效,如果无效则抛出异常。 + * 注意:同一 nonce 只可以被校验通过一次,校验后将保存在缓存中,再次校验将无法通过 + * @param nonce 待校验的随机字符串 + */ + public static void checkNonce(String nonce) { + SaSignManager.getSaSignTemplate().checkNonce(nonce); + } + + /** + * 判断:给定的参数 生成的签名是否为有效签名 + * @param paramsMap 参数列表 + * @param sign 待验证的签名 + * @return 签名是否有效 + */ + public static boolean isValidSign(Map paramsMap, String sign) { + return SaSignManager.getSaSignTemplate().isValidSign(paramsMap, sign); + } + + /** + * 校验:给定的参数 生成的签名是否为有效签名,如果签名无效则抛出异常 + * @param paramsMap 参数列表 + * @param sign 待验证的签名 + */ + public static void checkSign(Map paramsMap, String sign) { + SaSignManager.getSaSignTemplate().checkSign(paramsMap, sign); + } + + /** + * 判断:参数列表中的 nonce、timestamp、sign 是否均为合法的 + * @param paramMap 待校验的请求参数集合 + * @return 是否合法 + */ + public static boolean isValidParamMap(Map paramMap) { + return SaSignManager.getSaSignTemplate().isValidParamMap(paramMap); + } + + /** + * 校验:参数列表中的 nonce、timestamp、sign 是否均为合法的,如果不合法,则抛出对应的异常 + * @param paramMap 待校验的请求参数集合 + */ + public static void checkParamMap(Map paramMap) { + SaSignManager.getSaSignTemplate().checkParamMap(paramMap); + } + + + // ----------- Web 请求相关 封装 + + /** + * 判断:一个请求中的 nonce、timestamp、sign 是否均为合法的 + * @param request 待校验的请求对象 + * @param paramNames 指定参与签名的参数有哪些,如果不填写则默认为全部参数 + * @return 是否合法 + */ + public static boolean isValidRequest(SaRequest request, String... paramNames) { + return SaSignManager.getSaSignTemplate().isValidRequest(request, paramNames); + } + + /** + * 校验:一个请求的 nonce、timestamp、sign 是否均为合法的,如果不合法,则抛出对应的异常 + * @param request 待校验的请求对象 + * @param paramNames 指定参与签名的参数有哪些,如果不填写则默认为全部参数 + */ + public static void checkRequest(SaRequest request, String... paramNames) { + SaSignManager.getSaSignTemplate().checkRequest(request, paramNames); + } + +} diff --git a/sa-token-plugin/sa-token-sign/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-sign/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..68cd70d7c --- /dev/null +++ b/sa-token-plugin/sa-token-sign/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForSign \ No newline at end of file diff --git a/sa-token-plugin/sa-token-snack3/pom.xml b/sa-token-plugin/sa-token-snack3/pom.xml new file mode 100644 index 000000000..7afb8ff13 --- /dev/null +++ b/sa-token-plugin/sa-token-snack3/pom.xml @@ -0,0 +1,27 @@ + + + + sa-token-plugin + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + + sa-token-snack3 + sa-token-snack3 + sa-token integrate Snack3 + + + + cn.dev33 + sa-token-core + + + org.noear + snack3 + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack3.java b/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack3.java new file mode 100644 index 000000000..d32c6f707 --- /dev/null +++ b/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack3.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.json; + +import cn.dev33.satoken.util.SaFoxUtil; +import org.noear.snack.ONode; +import org.noear.snack.core.Feature; + +/** + * JSON 转换器, Snack3 版实现 + * + * @author click33 + * @author noear + * @since 1.41.0 + */ +public class SaJsonTemplateForSnack3 implements SaJsonTemplate { + + /** + * 序列化:对象 -> json 字符串 + */ + @Override + public String objectToJson(Object obj) { + if (SaFoxUtil.isEmpty(obj)) { + return null; + } + return ONode.loadObj(obj, Feature.WriteClassName, Feature.NotWriteRootClassName).toJson(); + } + + /** + * 反序列化:json 字符串 → 对象 + */ + @Override + public T jsonToObject(String jsonStr, Class type) { + if (SaFoxUtil.isEmpty(jsonStr)) { + return null; + } + return ONode.deserialize(jsonStr, type); + } +} diff --git a/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack3.java b/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack3.java new file mode 100644 index 000000000..29a5aa27a --- /dev/null +++ b/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack3.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.json.SaJsonTemplateForSnack3; +import cn.dev33.satoken.session.SaSessionForSnack3Customized; +import cn.dev33.satoken.strategy.SaStrategy; + +/** + * SaToken 插件安装:JSON 转换器 - Snack3 版 + * + * @author click33 + * @author noear + * @since 1.41.0 + */ +public class SaTokenPluginForSnack3 implements SaTokenPlugin { + + @Override + public void install() { + + // 设置JSON转换器:Snack3 版 + SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack3()); + + // 重写 SaSession 生成策略 + SaStrategy.instance.createSession = SaSessionForSnack3Customized::new; + + // 指定 SaSession 类型 + SaStrategy.instance.sessionClassType = SaSessionForSnack3Customized.class; + + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/session/SaSessionForSnack3Customized.java b/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/session/SaSessionForSnack3Customized.java new file mode 100644 index 000000000..3d3f088a3 --- /dev/null +++ b/sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/session/SaSessionForSnack3Customized.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session; + +import cn.dev33.satoken.util.SaFoxUtil; +import org.noear.snack.ONode; + +/** + * Fastjson 定制版 SaSession,重写类型转换API + * + * @author click33 + * @author noear + * @since 1.34.0 + */ +public class SaSessionForSnack3Customized extends SaSession { + + private static final long serialVersionUID = -7600983549653130681L; + + /** + * 构建一个 SaSession 对象 + */ + public SaSessionForSnack3Customized() { + super(); + } + + /** + * 构建一个 SaSession 对象 + * + * @param id Session 的 id + */ + public SaSessionForSnack3Customized(String id) { + super(id); + } + + /** + * 取值 (指定转换类型) + * + * @param 泛型 + * @param key key + * @param cs 指定转换类型 + * @return 值 + */ + @Override + public T getModel(String key, Class cs) { + // 如果是想取出为基础类型 + Object value = get(key); + if (SaFoxUtil.isBasicType(cs)) { + return SaFoxUtil.getValueByType(value, cs); + } + // 为空提前返回 + if (valueIsNull(value)) { + return null; + } + // 如果是 JSONObject 类型直接转,否则先转为 String 再转 + if (value instanceof ONode) { + ONode jo = (ONode) value; + return jo.toObject(cs); + } else if (value instanceof String) { + return ONode.deserialize((String) value, cs); + } else { + //有可能是 Map + return ONode.load(value).toObject(cs); + } + } +} diff --git a/sa-token-plugin/sa-token-snack3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-snack3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..d69d44916 --- /dev/null +++ b/sa-token-plugin/sa-token-snack3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForSnack3 \ No newline at end of file diff --git a/sa-token-plugin/sa-token-snack4/pom.xml b/sa-token-plugin/sa-token-snack4/pom.xml new file mode 100644 index 000000000..8ac4be444 --- /dev/null +++ b/sa-token-plugin/sa-token-snack4/pom.xml @@ -0,0 +1,27 @@ + + + + sa-token-plugin + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + + sa-token-snack4 + sa-token-snack4 + sa-token integrate Snack4 + + + + cn.dev33 + sa-token-core + + + org.noear + snack4 + + + \ No newline at end of file diff --git a/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack4.java b/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack4.java new file mode 100644 index 000000000..9eb574dde --- /dev/null +++ b/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack4.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.json; + +import cn.dev33.satoken.util.SaFoxUtil; +import org.noear.snack4.ONode; +import org.noear.snack4.Feature; +import org.noear.snack4.Options; + +/** + * JSON 转换器, Snack3 版实现 + * + * @author click33 + * @author noear + * @since 1.41.0 + */ +public class SaJsonTemplateForSnack4 implements SaJsonTemplate { + private final Options options = Options.of(Feature.Write_ClassName, Feature.Write_NotRootClassName, Feature.Read_AutoType); + + /** + * 序列化:对象 -> json 字符串 + */ + @Override + public String objectToJson(Object obj) { + if (SaFoxUtil.isEmpty(obj)) { + return null; + } + return ONode.ofBean(obj, options).toJson(); + } + + /** + * 反序列化:json 字符串 → 对象 + */ + @Override + public T jsonToObject(String jsonStr, Class type) { + if (SaFoxUtil.isEmpty(jsonStr)) { + return null; + } + return ONode.deserialize(jsonStr, type, options); + } +} diff --git a/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack4.java b/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack4.java new file mode 100644 index 000000000..23a6284b1 --- /dev/null +++ b/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack4.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.json.SaJsonTemplateForSnack4; +import cn.dev33.satoken.session.SaSessionForSnack4Customized; +import cn.dev33.satoken.strategy.SaStrategy; + +/** + * SaToken 插件安装:JSON 转换器 - Snack3 版 + * + * @author click33 + * @author noear + * @since 1.41.0 + */ +public class SaTokenPluginForSnack4 implements SaTokenPlugin { + + @Override + public void install() { + + // 设置JSON转换器:Snack3 版 + SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack4()); + + // 重写 SaSession 生成策略 + SaStrategy.instance.createSession = SaSessionForSnack4Customized::new; + + // 指定 SaSession 类型 + SaStrategy.instance.sessionClassType = SaSessionForSnack4Customized.class; + + } +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/session/SaSessionForSnack4Customized.java b/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/session/SaSessionForSnack4Customized.java new file mode 100644 index 000000000..ceee1ecdd --- /dev/null +++ b/sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/session/SaSessionForSnack4Customized.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.session; + +import cn.dev33.satoken.util.SaFoxUtil; +import org.noear.snack4.ONode; + +/** + * Fastjson 定制版 SaSession,重写类型转换API + * + * @author click33 + * @author noear + * @since 1.34.0 + */ +public class SaSessionForSnack4Customized extends SaSession { + + private static final long serialVersionUID = -7600983549653130681L; + + /** + * 构建一个 SaSession 对象 + */ + public SaSessionForSnack4Customized() { + super(); + } + + /** + * 构建一个 SaSession 对象 + * + * @param id Session 的 id + */ + public SaSessionForSnack4Customized(String id) { + super(id); + } + + /** + * 取值 (指定转换类型) + * + * @param 泛型 + * @param key key + * @param cs 指定转换类型 + * @return 值 + */ + @Override + public T getModel(String key, Class cs) { + // 如果是想取出为基础类型 + Object value = get(key); + if (SaFoxUtil.isBasicType(cs)) { + return SaFoxUtil.getValueByType(value, cs); + } + // 为空提前返回 + if (valueIsNull(value)) { + return null; + } + // 如果是 JSONObject 类型直接转,否则先转为 String 再转 + if (value instanceof ONode) { + ONode jo = (ONode) value; + return jo.toBean(cs); + } else if (value instanceof String) { + return ONode.deserialize((String) value, cs); + } else { + //有可能是 Map + return ONode.ofBean(value).toBean(cs); + } + } +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-snack4/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-snack4/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..896c95355 --- /dev/null +++ b/sa-token-plugin/sa-token-snack4/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForSnack4 \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-aop/.gitignore b/sa-token-plugin/sa-token-spring-aop/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-plugin/sa-token-spring-aop/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-aop/pom.xml b/sa-token-plugin/sa-token-spring-aop/pom.xml index 37924ff35..572a9e921 100644 --- a/sa-token-plugin/sa-token-spring-aop/pom.xml +++ b/sa-token-plugin/sa-token-spring-aop/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-plugin - 1.24.0 + ${revision} + ../pom.xml jar @@ -19,14 +20,12 @@ cn.dev33 - sa-token-spring-boot-starter - ${sa-token-version} + sa-token-core org.springframework.boot spring-boot-starter-aop - 2.0.0.RELEASE diff --git a/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAopPointcutAdvisorBeanRegister.java b/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAopPointcutAdvisorBeanRegister.java new file mode 100644 index 000000000..8fe1a649b --- /dev/null +++ b/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAopPointcutAdvisorBeanRegister.java @@ -0,0 +1,98 @@ +package cn.dev33.satoken.aop; + +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * Sa-Token AOP 环绕切入 Bean 注册 + *

    + * 参考资料:
    + * https://www.jb51.net/program/297714rev.htm
    + * https://www.bilibili.com/video/BV1WZ421W7Qx
    + * https://blog.csdn.net/Tomwildboar/article/details/139199801
    + *

    + * + * @author click33 + * @since 2024/8/3 + */ +@Configuration +public class SaAopPointcutAdvisorBeanRegister { + + /** + * Advisor 静态全局引用 + */ + public static SaAroundAnnotationPointcutAdvisor saAroundAnnoAdvisor; + + @Bean + public SaAroundAnnotationPointcutAdvisor saAroundAnnotationHandlePointcutAdvisor (List> handlerList) { + SaAroundAnnotationPointcutAdvisor advisor = new SaAroundAnnotationPointcutAdvisor(); + + // 定义切入规则 + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + String expression = calcExpression(handlerList); + pointcut.setExpression(expression); + advisor.setPointcut(pointcut); + + // 定义执行的方法 + advisor.setAdvice(new SaAroundAnnotationMethodInterceptor()); + + // 保存全局引用 + SaAopPointcutAdvisorBeanRegister.saAroundAnnoAdvisor = advisor; + return advisor; + } + + /** + * 计算切入表达式 + * @param appendHandlerList 追加的 SaAnnotationAbstractHandler 处理器 + * @return / + */ + public static String calcExpression(List> appendHandlerList) { + + // 框架内置的 + List> list = new ArrayList<>(SaAnnotationStrategy.instance.annotationHandlerMap.keySet()); + + // 额外追加的 + if(appendHandlerList != null) { + for (SaAnnotationHandlerInterface handler : appendHandlerList) { + Class cls = handler.getHandlerAnnotationClass(); + if(!list.contains(cls)) { + list.add(handler.getHandlerAnnotationClass()); + } + } + } + + // 计算 + return calcClassListExpression(list); + } + + /** + * 计算 class 列表的切入表达式, + * 最终样例形如: +
    +         public static final String POINTCUT_SIGN =
    +         "@within(cn.dev33.satoken.annotation.SaCheckLogin) || @annotation(cn.dev33.satoken.annotation.SaCheckLogin) || "
    +         + "@within(cn.dev33.satoken.annotation.SaCheckRole) || @annotation(cn.dev33.satoken.annotation.SaCheckRole) || "
    +         + "@within(cn.dev33.satoken.annotation.SaCheckPermission) || @annotation(cn.dev33.satoken.annotation.SaCheckPermission)";
    +     
    + * @param list / + * @return / + */ + public static String calcClassListExpression(List> list) { + String pointcutExpression = ""; + for (Class cls : list) { + if(!pointcutExpression.isEmpty()) { + pointcutExpression += " || "; + } + pointcutExpression += "@within(" + cls.getName() + ") || @annotation(" + cls.getName() + ")"; + } + return pointcutExpression; + } + + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationMethodInterceptor.java b/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationMethodInterceptor.java new file mode 100644 index 000000000..5f04e5518 --- /dev/null +++ b/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationMethodInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.aop; + +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import java.lang.reflect.Method; + +/** + * Sa-Token 注解方法拦截器 AOP环绕切入 + * + * @author click33 + * @since 1.39.0 + */ +public class SaAroundAnnotationMethodInterceptor implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + // 注解鉴权 + try{ + Method method = invocation.getMethod(); + SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); + } catch (StopMatchException ignored) { + } + // 执行原有防范 + return invocation.proceed(); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationPointcutAdvisor.java b/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationPointcutAdvisor.java new file mode 100644 index 000000000..795d4a6ff --- /dev/null +++ b/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationPointcutAdvisor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.aop; + +import org.springframework.aop.support.DefaultPointcutAdvisor; + +/** + * Sa-Token 注解方法 Advisor AOP环绕切入 + * + * @author click33 + * @since 1.39.0 + */ +public class SaAroundAnnotationPointcutAdvisor extends DefaultPointcutAdvisor { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaCheckAspect.java b/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaCheckAspect.java deleted file mode 100644 index 0ddfb0d97..000000000 --- a/sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaCheckAspect.java +++ /dev/null @@ -1,69 +0,0 @@ -package cn.dev33.satoken.aop; - -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.util.SaTokenConsts; - -/** - * sa-token 基于 Spring Aop 的注解鉴权 - * - * @author kong - */ -@Aspect -@Component -@Order(SaTokenConsts.ASSEMBLY_ORDER) -public class SaCheckAspect { - - /** - * 构建 - */ - public SaCheckAspect() { - } - - /** - * 定义AOP签名 (切入所有使用sa-token鉴权注解的方法) - */ - public static final String POINTCUT_SIGN = - "@within(cn.dev33.satoken.annotation.SaCheckLogin) || @annotation(cn.dev33.satoken.annotation.SaCheckLogin) || " - + "@within(cn.dev33.satoken.annotation.SaCheckRole) || @annotation(cn.dev33.satoken.annotation.SaCheckRole) || " - + "@within(cn.dev33.satoken.annotation.SaCheckPermission) || @annotation(cn.dev33.satoken.annotation.SaCheckPermission) || " - + "@within(cn.dev33.satoken.annotation.SaCheckSafe) || @annotation(cn.dev33.satoken.annotation.SaCheckSafe)"; - - /** - * 声明AOP签名 - */ - @Pointcut(POINTCUT_SIGN) - public void pointcut() { - } - - /** - * 环绕切入 - * - * @param joinPoint 切面对象 - * @return 底层方法执行后的返回值 - * @throws Throwable 底层方法抛出的异常 - */ - @Around("pointcut()") - public Object around(ProceedingJoinPoint joinPoint) throws Throwable { - - // 注解鉴权 - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - SaManager.getSaTokenAction().checkMethodAnnotation(signature.getMethod()); - - try { - // 执行原有逻辑 - Object obj = joinPoint.proceed(); - return obj; - } catch (Throwable e) { - throw e; - } - } - -} diff --git a/sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring.factories index 9c26ddeec..b4ad11e6c 100644 --- a/sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring.factories +++ b/sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring.factories @@ -1 +1,2 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.aop.SaCheckAspect \ No newline at end of file +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.dev33.satoken.aop.SaAopPointcutAdvisorBeanRegister \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..ac2902594 --- /dev/null +++ b/sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.aop.SaAopPointcutAdvisorBeanRegister \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-el/pom.xml b/sa-token-plugin/sa-token-spring-el/pom.xml new file mode 100644 index 000000000..494ac51d9 --- /dev/null +++ b/sa-token-plugin/sa-token-spring-el/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-spring-el + sa-token-spring-el + sa-token authentication by spring-el + + + + + cn.dev33 + sa-token-core + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + diff --git a/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/annotation/SaCheckEL.java b/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/annotation/SaCheckEL.java new file mode 100644 index 000000000..5f4cf025a --- /dev/null +++ b/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/annotation/SaCheckEL.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 注解鉴权:根据 EL 表达式执行鉴权 + * + *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.40.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaCheckEL { + + /** + * 需要执行的 EL 表达式 + * + * @return / + */ + String value() default ""; + +} diff --git a/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELAspect.java b/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELAspect.java new file mode 100644 index 000000000..3a76c3059 --- /dev/null +++ b/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELAspect.java @@ -0,0 +1,146 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.aop; + +import cn.dev33.satoken.annotation.SaCheckEL; +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.context.expression.MapAccessor; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.ObjectUtils; + +import java.lang.reflect.Method; + +/** + * Sa-Token 注解鉴权 EL 表达式 AOP 切入 (用于处理 @SaCheckEL 注解) + * + * @author click33 + * @since 1.40.0 + */ +@Aspect +public class SaCheckELAspect implements BeanFactoryAware { + + /** + * 表达式解析器 (用于解析 EL 表达式) + */ + private final ExpressionParser parser = new SpelExpressionParser(); + + /** + * 参数名发现器 (用于获取方法参数名) + */ + private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer(); + + /** + * Spring Bean 工厂 (用于解析 Spring 容器中的 Bean 对象) + */ + private BeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + /** + * 前置通知 (所有被 SaCheckEL 注解修饰的方法或类) + * + * @param joinPoint / + */ + @Before("@within(cn.dev33.satoken.annotation.SaCheckEL) || @annotation(cn.dev33.satoken.annotation.SaCheckEL)") + public void atBefore(JoinPoint joinPoint) { + + // 获取方法签名与参数列表 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + Object[] args = joinPoint.getArgs(); + + // 如果标注了 @SaIgnore 注解,则跳过,代表不进行校验 + if(SaAnnotationStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)) { + return; + } + + // 1、根数据对象构建 + // 构建校验上下文根数据对象 + SaCheckELRootMap rootMap = new SaCheckELRootMap(method, extractArgs(method, args), joinPoint.getTarget() ); + + // 添加 this 指针指向注解函数所在类,使之可以在表达式中通过 this.xx 访问类的属性和方法 (与Target一致,此处只是为了更加语义化) + rootMap.put(SaCheckELRootMap.KEY_THIS, joinPoint.getTarget()); + + // 添加全局默认的 StpLogic 对象,使之可以在表达式中通过 stp.checkLogin() 方式调用校验方法 + rootMap.put(SaCheckELRootMap.KEY_STP, StpUtil.getStpLogic()); + + // 添加 JoinPoint 对象,使开发者在扩展时可以根据 JoinPoint 对象获取更多信息 + rootMap.put(SaCheckELRootMap.KEY_JOIN_POINT, joinPoint); + + // 执行开发者自定义的增强策略 + SaAnnotationStrategy.instance.checkELRootMapExtendFunction.accept(rootMap); + + // 2、表达式解析方案构建 + // 创建表达式解析上下文 + MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(rootMap, method, args, pnd); + + // 添加属性访问器,使之可以解析 Map 对象的属性作为根上下文 + context.addPropertyAccessor(new MapAccessor()); + + // 设置 Bean 解析器,使之可以在表达式中引用 Spring 容器管理的所有 Bean 对象 + context.setBeanResolver(new BeanFactoryResolver(beanFactory)); + + // 3、开始校验 + // 先校验 Method 所属 Class 上的注解表达式 + SaCheckEL ofClass = (SaCheckEL) SaAnnotationStrategy.instance.getAnnotation.apply(method.getDeclaringClass(), SaCheckEL.class); + if (ofClass != null) { + parser.parseExpression(ofClass.value()).getValue(context); + } + + // 再校验 Method 上的注解表达式 + SaCheckEL ofMethod = (SaCheckEL) SaAnnotationStrategy.instance.getAnnotation.apply(method, SaCheckEL.class); + if (ofMethod != null) { + parser.parseExpression(ofMethod.value()).getValue(context); + } + + } + + /** + * 如果是可变长参数,则展开并返回,否则原样返回 + * + * @param method / + * @param args / + * @return / + */ + private Object[] extractArgs(Method method, Object[] args) { + if (!method.isVarArgs()) { + return args; + } else { + Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]); + Object[] combinedArgs = new Object[args.length - 1 + varArgs.length]; + System.arraycopy(args, 0, combinedArgs, 0, args.length - 1); + System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length); + return combinedArgs; + } + } +} diff --git a/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELRootMap.java b/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELRootMap.java new file mode 100644 index 000000000..4dcbd412f --- /dev/null +++ b/sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELRootMap.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.aop; + +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.SaTokenException; + +import java.lang.reflect.Method; +import java.util.HashMap; + +/** + * Sa-Token 注解鉴权 EL 表达式解析器的根数据对象 + * + * @author click33 + * @since 1.40.0 + */ +public class SaCheckELRootMap extends HashMap { + + /** + * KEY标记:被切入的函数 + */ + public static final String KEY_METHOD = "method"; + + /** + * KEY标记:被切入的函数参数 + */ + public static final String KEY_ARGS = "args"; + + /** + * KEY标记:被切入的目标对象 + */ + public static final String KEY_TARGET = "target"; + + /** + * KEY标记:注解所在类对象引用 + */ + public static final String KEY_THIS = "this"; + + /** + * KEY标记:全局默认 StpLogic 对象 + */ + public static final String KEY_STP = "stp"; + + /** + * KEY标记:本次切入的 JoinPoint 对象 + */ + public static final String KEY_JOIN_POINT = "joinPoint"; + + public SaCheckELRootMap(Method method, Object[] args, Object target) { + this.put(KEY_METHOD, method); + this.put(KEY_ARGS, args); + this.put(KEY_TARGET, target); + } + + /** + * 获取 被切入的函数 + * + * @return method 被切入的函数 + */ + public Method getMethod() { + return (Method) this.get(KEY_METHOD); + } + + /** + * 获取 被切入的函数参数 + * + * @return args 被切入的函数参数 + */ + public Object[] getArgs() { + return (Object[]) this.get(KEY_ARGS); + } + + /** + * 获取 被切入的目标对象 + * + * @return target 被切入的目标对象 + */ + public Object getTarget() { + return this.get(KEY_TARGET); + } + + /** + * 获取 注解所在类对象引用 + * + * @return this 注解所在类对象引用 + */ + public Object getThis() { + return this.get(KEY_THIS); + } + + /** + * 获取本次切入的 JoinPoint 对象 + */ + public Object getJoinPoint() { + return this.get(KEY_JOIN_POINT); + } + + /** + * 断言函数, 表达式执行结果为true才能通过 + * + * @param flag 执行结果 + */ + public void NEED(boolean flag) { + NEED(flag, SaErrorCode.CODE_UNDEFINED, "未通过 EL 表达式校验"); + } + + /** + * 断言函数, 表达式执行结果为true才能通过,并在未通过时抛出 SaTokenException 异常,异常描述信息为 errorMessage + * + * @param flag 执行结果 + */ + public void NEED(boolean flag, String errorMessage) { + NEED(flag, SaErrorCode.CODE_UNDEFINED, errorMessage); + } + + /** + * 断言函数, 表达式执行结果为true才能通过,并在未通过时抛出 SaTokenException 异常,异常码为 errorCode,异常描述信息为 errorMessage + * + * @param flag 执行结果 + */ + public void NEED(boolean flag, int errorCode, String errorMessage) { + if(!flag) { + throw new SaTokenException(errorCode, errorMessage); + } + } + + +} diff --git a/sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..c28787106 --- /dev/null +++ b/sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.dev33.satoken.aop.SaCheckELAspect \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..1da378a21 --- /dev/null +++ b/sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.aop.SaCheckELAspect \ No newline at end of file diff --git a/sa-token-plugin/sa-token-spring-el/src/main/resources/spel-extension.json b/sa-token-plugin/sa-token-spring-el/src/main/resources/spel-extension.json new file mode 100644 index 000000000..931a7be83 --- /dev/null +++ b/sa-token-plugin/sa-token-spring-el/src/main/resources/spel-extension.json @@ -0,0 +1,15 @@ +{ + "cn.dev33.satoken.annotation.SaCheckEL@value": { + "method": { + "parameters": true, + "parametersPrefix": [ + "p", + "a" + ] + }, + "fields": { + "root": "cn.dev33.satoken.aop.SaCheckELRootMap", + "stp": "cn.dev33.satoken.stp.StpLogic" + } + } +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/pom.xml b/sa-token-plugin/sa-token-sso/pom.xml new file mode 100644 index 000000000..32457cc51 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-sso + sa-token-sso + sa-token realization sso + + + + + cn.dev33 + sa-token-core + + + + cn.dev33 + sa-token-sign + + + + + + diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/SaSsoManager.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/SaSsoManager.java new file mode 100644 index 000000000..d5724b918 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/SaSsoManager.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso; + +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; + +/** + * Sa-Token-SSO 模块 总控类 + * + * @author click33 + * @since 1.30.0 + */ +public class SaSsoManager { + + /** + * Sso Server 端 配置 Bean + */ + private volatile static SaSsoServerConfig serverConfig; + public static SaSsoServerConfig getServerConfig() { + if (serverConfig == null) { + synchronized (SaSsoManager.class) { + if (serverConfig == null) { + setServerConfig(new SaSsoServerConfig()); + } + } + } + return serverConfig; + } + public static void setServerConfig(SaSsoServerConfig serverConfig) { + SaSsoManager.serverConfig = serverConfig; + // 如果配置了 is-check-sign=false,则打印一条警告日志 + if ( ! serverConfig.getIsCheckSign()) { + printNoCheckSignWarningByStartup(); + } + } + + /** + * Sso Client 端 配置 Bean + */ + private volatile static SaSsoClientConfig clientConfig; + public static SaSsoClientConfig getClientConfig() { + if (clientConfig == null) { + synchronized (SaSsoManager.class) { + if (clientConfig == null) { + setClientConfig(new SaSsoClientConfig()); + } + } + } + return clientConfig; + } + public static void setClientConfig(SaSsoClientConfig clientConfig) { + SaSsoManager.clientConfig = clientConfig; + // 如果配置了 is-check-sign=false,则打印一条警告日志 + if ( ! clientConfig.getIsCheckSign()) { + printNoCheckSignWarningByStartup(); + } + } + + // 在启动时检测到 sa-token.sso-[server/client].is-check-sign=false 时,输出警告信息 + public static void printNoCheckSignWarningByStartup() { + System.err.println("-----------------------------------------------------------------------------"); + System.err.println("警告信息:"); + System.err.println("当前配置项 sa-token.sso-[server/client].is-check-sign=false 代表跳过 SSO 参数签名校验"); + System.err.println("此模式仅为方便本地调试使用,生产环境下请务必配置为 true (配置项默认为true)"); + System.err.println("-----------------------------------------------------------------------------"); + } + + // 在运行时检测到 sa-token.sso-[server/client].is-check-sign=false 时,输出警告信息 + public static void printNoCheckSignWarningByRuntime() { + System.err.println("警告信息:当前配置项 sa-token.sso-[server/client].is-check-sign=false 已跳过参数签名校验," + + "此模式仅为方便本地调试使用,生产环境下请务必配置为 true (配置项默认为true)"); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientConfig.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientConfig.java new file mode 100644 index 000000000..0ee50c7a6 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientConfig.java @@ -0,0 +1,405 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.config; + + +import cn.dev33.satoken.util.SaFoxUtil; + +import java.io.Serializable; + +/** + * Sa-Token SSO Client 端 配置类 + * + * @author click33 + * @since 1.30.0 + */ +public class SaSsoClientConfig implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) + */ + public String mode = ""; + + /** + * 当前 Client 标识(非必填,不填时代表当前应用是一个匿名应用) + */ + public String client; + + /** + * 配置 SSO Server 端主机总地址 + */ + public String serverUrl; + + /** + * 单独配置 Server 端:单点登录授权地址 + */ + public String authUrl = "/sso/auth"; + + /** + * 单独配置 Server 端:单点注销地址 + */ + public String signoutUrl = "/sso/signout"; + + /** + * 单独配置 Server 端:推送消息地址 + */ + public String pushUrl = "/sso/pushS"; + + /** + * 单独配置 Server 端:查询数据 getData 地址 + */ + public String getDataUrl = "/sso/getData"; + + /** + * 配置当前 Client 端的登录地址(为空时自动获取) + */ + public String currSsoLogin; + + /** + * 配置当前 Client 端的单点注销回调URL (为空时自动获取) + */ + public String currSsoLogoutCall; + + /** + * 是否打开模式三(此值为 true 时将使用 http 请求校验 ticket 值) + */ + public Boolean isHttp = false; + + /** + * 是否打开单点注销功能 (为 true 时,开放 /sso/logout 接口,以及接收单点注销回调消息推送) + */ + public Boolean isSlo = true; + + /** + * 是否注册单点登录注销回调 (为 true 时,登录时附带单点登录回调地址,并且开放 /sso/logoutCall 地址) + */ + public Boolean regLogoutCall = false; + + /** + * API 调用签名秘钥 + */ + public String secretKey; + + /** + * 是否校验参数签名(为 false 时暂时关闭参数签名校验,此为方便本地调试用的一个配置项,生产环境请务必为true) + */ + public Boolean isCheckSign = true; + + + // 额外添加的一些函数 + + /** + * @return 获取拼接 url:Server 端单点登录授权地址 + */ + public String splicingAuthUrl() { + return SaFoxUtil.spliceTwoUrl(getServerUrl(), getAuthUrl()); + } + + /** + * @return 获取拼接 url:Server 端查询数据 getData 地址 + */ + public String splicingGetDataUrl() { + return SaFoxUtil.spliceTwoUrl(getServerUrl(), getGetDataUrl()); + } + + /** + * @return 获取拼接 url:Server 端单点注销地址 + */ + public String splicingSignoutUrl() { + return SaFoxUtil.spliceTwoUrl(getServerUrl(), getSignoutUrl()); + } + + /** + * @return 获取拼接 url:单独配置 Server 端推送消息地址 + */ + public String splicingPushUrl() { + return SaFoxUtil.spliceTwoUrl(getServerUrl(), getPushUrl()); + } + + + // get set + + /** + * 获取 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) + * + * @return / + */ + public String getMode() { + return this.mode; + } + + /** + * 设置 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) + * + * @param mode / + */ + public void setMode(String mode) { + this.mode = mode; + } + + /** + * @return 是否打开单点注销功能 (为 true 时,开放 /sso/logout 接口,以及接收单点注销回调消息推送) + */ + public Boolean getIsSlo() { + return isSlo; + } + + /** + * @param isSlo 是否打开单点注销功能 (为 true 时,开放 /sso/logout 接口,以及接收单点注销回调消息推送) + * @return 对象自身 + */ + public SaSsoClientConfig setIsSlo(Boolean isSlo) { + this.isSlo = isSlo; + return this; + } + + /** + * @return isHttp 是否打开模式三(此值为 true 时将使用 http 请求校验 ticket 值) + */ + public Boolean getIsHttp() { + return isHttp; + } + + /** + * @param isHttp 是否打开模式三(此值为 true 时将使用 http 请求校验 ticket 值) + * @return 对象自身 + */ + public SaSsoClientConfig setIsHttp(Boolean isHttp) { + this.isHttp = isHttp; + return this; + } + + /** + * 当前 Client 标识(非必填,不填时代表当前应用是一个匿名应用) + * + * @return / + */ + public String getClient() { + return client; + } + + /** + * 当前 Client 标识(非必填,不填时代表当前应用是一个匿名应用) + * + * @param client / + */ + public SaSsoClientConfig setClient(String client) { + this.client = client; + return this; + } + + /** + * @return 单独配置 Server 端:单点登录授权地址 + */ + public String getAuthUrl() { + return authUrl; + } + + /** + * @param authUrl 单独配置 Server 端:单点登录授权地址 + * @return 对象自身 + */ + public SaSsoClientConfig setAuthUrl(String authUrl) { + this.authUrl = authUrl; + return this; + } + + /** + * @return 单独配置 Server 端:查询数据 getData 地址 + */ + public String getGetDataUrl() { + return getDataUrl; + } + + /** + * @param getDataUrl 单独配置 Server 端:查询数据 getData 地址 + * @return 对象自身 + */ + public SaSsoClientConfig setGetDataUrl(String getDataUrl) { + this.getDataUrl = getDataUrl; + return this; + } + + /** + * @return 单独配置 Server 端:单点注销地址 + */ + public String getSignoutUrl() { + return signoutUrl; + } + + /** + * @param signoutUrl 单独配置 Server 端:单点注销地址 + * @return 对象自身 + */ + public SaSsoClientConfig setSignoutUrl(String signoutUrl) { + this.signoutUrl = signoutUrl; + return this; + } + + /** + * 获取 单独配置 Server 端:推送消息地址 + * + * @return / + */ + public String getPushUrl() { + return this.pushUrl; + } + + /** + * 设置 单独配置 Server 端:推送消息地址 + * + * @param pushUrl / + * @return 对象自身 + */ + public SaSsoClientConfig setPushUrl(String pushUrl) { + this.pushUrl = pushUrl; + return this; + } + + /** + * @return 配置当前 Client 端的登录地址(为空时自动获取) + */ + public String getCurrSsoLogin() { + return currSsoLogin; + } + + /** + * @param currSsoLogin 配置当前 Client 端的登录地址(为空时自动获取) + * @return 对象自身 + */ + public SaSsoClientConfig setCurrSsoLogin(String currSsoLogin) { + this.currSsoLogin = currSsoLogin; + return this; + } + + /** + * @return 配置当前 Client 端的单点注销回调URL (为空时自动获取) + */ + public String getCurrSsoLogoutCall() { + return currSsoLogoutCall; + } + + /** + * @param currSsoLogoutCall 配置当前 Client 端的单点注销回调URL (为空时自动获取) + * @return 对象自身 + */ + public SaSsoClientConfig setCurrSsoLogoutCall(String currSsoLogoutCall) { + this.currSsoLogoutCall = currSsoLogoutCall; + return this; + } + + /** + * 配置 SSO Server 端主机总地址 + * + * @return / + */ + public String getServerUrl() { + return serverUrl; + } + + /** + * 配置 SSO Server 端主机总地址 + * + * @param serverUrl / + * @return 对象自身 + */ + public SaSsoClientConfig setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + return this; + } + + /** + * 获取 API 调用签名秘钥 + * + * @return / + */ + public String getSecretKey() { + return this.secretKey; + } + + /** + * 设置 API 调用签名秘钥 + * + * @param secretKey / + * @return 对象自身 + */ + public SaSsoClientConfig setSecretKey(String secretKey) { + this.secretKey = secretKey; + return this; + } + + /** + * 获取 是否校验参数签名(为 false 时暂时关闭参数签名校验,此为方便本地调试用的一个配置项,生产环境请务必为true) + * + * @return isCheckSign 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) + */ + public Boolean getIsCheckSign() { + return this.isCheckSign; + } + + /** + * 设置 是否校验参数签名(为 false 时暂时关闭参数签名校验,此为方便本地调试用的一个配置项,生产环境请务必为true) + * + * @param isCheckSign 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) + */ + public SaSsoClientConfig setIsCheckSign(Boolean isCheckSign) { + this.isCheckSign = isCheckSign; + return this; + } + + /** + * 获取 是否注册单点登录注销回调 (为 true 时,登录时附带单点登录回调地址,并且开放 /sso/logoutCall 地址) + * + * @return / + */ + public Boolean getRegLogoutCall() { + return this.regLogoutCall; + } + + /** + * 设置 是否注册单点登录注销回调 (为 true 时,登录时附带单点登录回调地址,并且开放 /sso/logoutCall 地址) + * + * @param regLogoutCall / + * @return / + */ + public SaSsoClientConfig setRegLogoutCall(Boolean regLogoutCall) { + this.regLogoutCall = regLogoutCall; + return this; + } + + @Override + public String toString() { + return "SaSsoClientConfig [" + + "mode=" + mode + + ", client=" + client + + ", serverUrl=" + serverUrl + + ", authUrl=" + authUrl + + ", signoutUrl=" + signoutUrl + + ", pushUrl=" + pushUrl + + ", getDataUrl=" + getDataUrl + + ", currSsoLogin=" + currSsoLogin + + ", currSsoLogoutCall=" + currSsoLogoutCall + + ", isHttp=" + isHttp + + ", isSlo=" + isSlo + + ", regLogoutCall=" + regLogoutCall + + ", secretKey=" + secretKey + + ", isCheckSign=" + isCheckSign + + "]"; + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientModel.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientModel.java new file mode 100644 index 000000000..d74a73482 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientModel.java @@ -0,0 +1,242 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.config; + + +import cn.dev33.satoken.sso.error.SaSsoErrorCode; +import cn.dev33.satoken.sso.exception.SaSsoException; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.io.Serializable; +import java.util.List; + +/** + * Sa-Token SSO 客户端信息配置 (在 Server 端配置允许接入的 Client 信息) + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoClientModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * Client 名称标识 + */ + public String client; + + /** + * 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket ) + */ + public String allowUrl = ""; + + /** + * 是否接收推送消息 + */ + public Boolean isPush = false; + + /** + * 是否打开单点注销功能 + */ + public Boolean isSlo = true; + + /** + * API 调用签名秘钥 + */ + public String secretKey; + + /** + * 此 Client 端主机总地址 + */ + public String serverUrl; + + /** + * 此 Client 端推送消息的地址 (如不配置,默认根据 serverUrl + '/sso/pushC' 进行拼接) + */ + public String pushUrl = "/sso/pushC"; + + + // 额外添加的一些函数 + + /** + * 以数组形式写入允许的授权回调地址 + * @param url 所有集合 + * @return 对象自身 + */ + public SaSsoClientModel setAllow(String ...url) { + this.setAllowUrl(SaFoxUtil.arrayJoin(url)); + return this; + } + + /** + * 获取拼接 url:此 Client 端推送消息的地址 + * + * @return / + */ + public String splicingPushUrl() { + String _pushUrl = SaFoxUtil.spliceTwoUrl(getServerUrl(), getPushUrl()); + if ( ! SaFoxUtil.isUrl(_pushUrl)) { + throw new SaSsoException("应用 [" + getClient() + "] 推送地址无效:" + _pushUrl).setCode(SaSsoErrorCode.CODE_30023); + } + return _pushUrl; + } + + + // get set + + /** + * @return Client 名称标识 + */ + public String getClient() { + return client; + } + + /** + * @param client Client 名称标识 + */ + public SaSsoClientModel setClient(String client) { + this.client = client; + return this; + } + + /** + * @return 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket ) + */ + public String getAllowUrl() { + return allowUrl; + } + + /** + * @param allowUrl 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket ) + * @return 对象自身 + */ + public SaSsoClientModel setAllowUrl(String allowUrl) { + // 提前校验一下配置的 allowUrl 是否合法,让开发者尽早发现错误 + if(SaFoxUtil.isNotEmpty(allowUrl)) { + List allowUrlList = SaFoxUtil.convertStringToList(allowUrl); + SaSsoServerTemplate.checkAllowUrlListStaticMethod(allowUrlList); + } + this.allowUrl = allowUrl; + return this; + } + + /** + * @return isHttp 是否打开模式三 + */ + public Boolean getIsPush() { + return isPush; + } + + /** + * @param isPush 是否打开模式三 + * @return 对象自身 + */ + public SaSsoClientModel setIsPush(Boolean isPush) { + this.isPush = isPush; + return this; + } + + /** + * @return 是否打开单点注销功能 + */ + public Boolean getIsSlo() { + return isSlo; + } + + /** + * @param isSlo 是否打开单点注销功能 + * @return 对象自身 + */ + public SaSsoClientModel setIsSlo(Boolean isSlo) { + this.isSlo = isSlo; + return this; + } + + /** + * 获取 API 调用签名秘钥 + * + * @return / + */ + public String getSecretKey() { + return this.secretKey; + } + + /** + * 设置 API 调用签名秘钥 + * + * @param secretKey / + * @return 对象自身 + */ + public SaSsoClientModel setSecretKey(String secretKey) { + this.secretKey = secretKey; + return this; + } + + /** + * 获取 此 Client 端主机总地址 + * + * @return serverUrl 此 Client 端主机总地址 + */ + public String getServerUrl() { + return this.serverUrl; + } + + /** + * 设置 此 Client 端主机总地址 + * + * @param serverUrl 此 Client 端主机总地址 + * @return 对象自身 + */ + public SaSsoClientModel setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + return this; + } + + /** + * 获取 此 Client 端推送消息的地址 (如不配置,默认根据 serverUrl + '/sso/pushC' 进行拼接) + * + * @return / + */ + public String getPushUrl() { + return this.pushUrl; + } + + /** + * 设置 此 Client 端推送消息的地址 (如不配置,默认根据 serverUrl + '/sso/pushC' 进行拼接) + * + * @param pushUrl 此 Client 端推送消息的地址 + * @return 对象自身 + */ + public SaSsoClientModel setPushUrl(String pushUrl) { + this.pushUrl = pushUrl; + return this; + } + + @Override + public String toString() { + return "SaSsoClientModel [" + + "client=" + client + + ", allowUrl=" + allowUrl + + ", isSlo=" + isSlo + + ", isPush=" + isPush + + ", secretKey=" + secretKey + + ", serverUrl=" + serverUrl + + ", pushUrl=" + pushUrl + + "]"; + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java new file mode 100644 index 000000000..98c814a95 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java @@ -0,0 +1,337 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.config; + + +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Sa-Token SSO Server 端 配置类 + * + * @author click33 + * @since 1.38.0 + */ +public class SaSsoServerConfig implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + + // ----------------- Server端相关配置 + + /** + * 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) + */ + public String mode = ""; + + /** + * ticket 有效期 (单位: 秒) + */ + public long ticketTimeout = 60 * 5; + + /** + * 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 + */ + public String homeRoute; + + /** + * 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息) + */ + public Boolean isSlo = true; + + /** + * 是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) + */ + public Boolean autoRenewTimeout = false; + + /** + * 在 Account-Session 上记录 Client 信息的最高数量(-1=无限),超过此值将进行自动清退处理,先进先出 + */ + public int maxRegClient = 32; + + /** + * 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) + */ + public Boolean isCheckSign = true; + + /** + * Client 信息配置列表 + */ + public Map clients = new LinkedHashMap<>(); + + // 匿名 Client 相关配置 + + /** + * 是否允许匿名 Client 接入 + */ + public Boolean allowAnonClient = false; + + /** + * 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用) + */ + public String allowUrl = ""; + + /** + * API 调用签名秘钥 (全局默认 + 匿名 client 使用) + */ + public String secretKey; + + + // 额外方法 + + /** + * 以数组形式写入允许的授权回调地址 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用) + * @param url 所有集合 + * @return 对象自身 + */ + public SaSsoServerConfig setAllow(String ...url) { + this.setAllowUrl(SaFoxUtil.arrayJoin(url)); + return this; + } + + /** + * 添加一个应用 + * @param client / + * @return 对象自身 + */ + public SaSsoServerConfig addClient(SaSsoClientModel client) { + this.clients.put(client.getClient(), client); + return this; + } + + + // get set + + /** + * 获取 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) + * + * @return / + */ + public String getMode() { + return this.mode; + } + + /** + * 设置 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) + * + * @param mode / + */ + public void setMode(String mode) { + this.mode = mode; + } + + /** + * @return ticket 有效期 (单位: 秒) + */ + public long getTicketTimeout() { + return ticketTimeout; + } + + /** + * @param ticketTimeout ticket 有效期 (单位: 秒) + * @return 对象自身 + */ + public SaSsoServerConfig setTicketTimeout(long ticketTimeout) { + this.ticketTimeout = ticketTimeout; + return this; + } + + /** + * @return 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用) + */ + public String getAllowUrl() { + return allowUrl; + } + + /** + * @param allowUrl 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用) + * @return 对象自身 + */ + public SaSsoServerConfig setAllowUrl(String allowUrl) { + // 提前校验一下配置的 allowUrl 是否合法,让开发者尽早发现错误 + if(SaFoxUtil.isNotEmpty(allowUrl)) { + List allowUrlList = SaFoxUtil.convertStringToList(allowUrl); + SaSsoServerTemplate.checkAllowUrlListStaticMethod(allowUrlList); + } + this.allowUrl = allowUrl; + return this; + } + + /** + * @return 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 + */ + public String getHomeRoute() { + return homeRoute; + } + + /** + * @param homeRoute 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 + * @return 对象自身 + */ + public SaSsoServerConfig setHomeRoute(String homeRoute) { + this.homeRoute = homeRoute; + return this; + } + + /** + * @return 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息) + */ + public Boolean getIsSlo() { + return isSlo; + } + + /** + * @param isSlo 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息) + * @return 对象自身 + */ + public SaSsoServerConfig setIsSlo(Boolean isSlo) { + this.isSlo = isSlo; + return this; + } + + /** + * @return 是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) + */ + public Boolean getAutoRenewTimeout() { + return autoRenewTimeout; + } + + /** + * @param autoRenewTimeout 是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) + * @return 对象自身 + */ + public SaSsoServerConfig setAutoRenewTimeout(Boolean autoRenewTimeout) { + this.autoRenewTimeout = autoRenewTimeout; + return this; + } + + /** + * @return maxLoginClient 在 Account-Session 上记录 Client 信息的最高数量(-1=无限),超过此值将进行自动清退处理,先进先出 + */ + public int getMaxRegClient() { + return maxRegClient; + } + + /** + * @param maxRegClient 在 Account-Session 上记录 Client 信息的最高数量(-1=无限),超过此值将进行自动清退处理,先进先出 + * @return 对象自身 + */ + public SaSsoServerConfig setMaxRegClient(int maxRegClient) { + this.maxRegClient = maxRegClient; + return this; + } + + /** + * 获取 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) + * + * @return isCheckSign 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) + */ + public Boolean getIsCheckSign() { + return this.isCheckSign; + } + + /** + * 设置 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) + * + * @param isCheckSign 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) + */ + public SaSsoServerConfig setIsCheckSign(Boolean isCheckSign) { + this.isCheckSign = isCheckSign; + return this; + } + + /** + * 获取 是否允许匿名 Client 接入 + * + * @return / + */ + public Boolean getAllowAnonClient() { + return this.allowAnonClient; + } + + /** + * 设置 是否允许匿名 Client 接入 + * + * @param allowAnonClient / + */ + public SaSsoServerConfig setAllowAnonClient(Boolean allowAnonClient) { + this.allowAnonClient = allowAnonClient; + return this; + } + + /** + * 获取 API 调用签名秘钥 (全局默认 + 匿名 client 使用) + * + * @return / + */ + public String getSecretKey() { + return this.secretKey; + } + + /** + * 设置 API 调用签名秘钥 (全局默认 + 匿名 client 使用) + * + * @param secretKey / + * @return 对象自身 + */ + public SaSsoServerConfig setSecretKey(String secretKey) { + this.secretKey = secretKey; + return this; + } + + /** + * 获取 Client 信息配置列表 + * + * @return clients Client 信息配置列表 + */ + public Map getClients() { + return this.clients; + } + + /** + * 设置 Client 信息配置列表 + * + * @param clients Client 信息配置列表 + * @return 对象自身 + */ + public SaSsoServerConfig setClients(Map clients) { + this.clients = clients; + return this; + } + + @Override + public String toString() { + return "SaSsoServerConfig [" + + "mode=" + mode + + ", ticketTimeout=" + ticketTimeout + + ", allowUrl=" + allowUrl + + ", homeRoute=" + homeRoute + + ", isSlo=" + isSlo + + ", autoRenewTimeout=" + autoRenewTimeout + + ", maxRegClient=" + maxRegClient + + ", isCheckSign=" + isCheckSign + + ", allowAnonClient=" + allowAnonClient + + ", secretKey=" + secretKey + + ", clients=" + clients + + "]"; + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java new file mode 100644 index 000000000..b52f52f12 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.error; + +/** + * 定义 sa-token-sso 所有异常细分状态码 + * + * @author click33 + * @since 1.33.0 + */ +public interface SaSsoErrorCode { + + /** redirect 重定向 url 是一个无效地址 */ + int CODE_30001 = 30001; + + /** redirect 重定向 url 不在 allowUrl 允许的范围内 */ + int CODE_30002 = 30002; + + /** 接口调用方提供的 secretkey 秘钥无效 */ + int CODE_30003 = 30003; + + /** 提供的 ticket 是无效的 */ + int CODE_30004 = 30004; + + /** 在模式三下,sso-client 调用 sso-server 端 校验ticket接口 时,得到的响应是校验失败 */ + int CODE_30005 = 30005; + + /** 在模式三下,sso-client 调用 sso-server 端 单点注销接口 时,得到的响应是注销失败 */ + int CODE_30006 = 30006; + + /** http 请求调用 提供的 timestamp 与当前时间的差距超出允许的范围 */ + int CODE_30007 = 30007; + + /** http 请求调用 提供的 sign 无效 */ + int CODE_30008 = 30008; + + /** 本地系统没有配置 secretkey 字段 */ + int CODE_30009 = 30009; + + /** 本地系统没有配置 http 请求处理器 */ + int CODE_30010 = 30010; + + /** 该 ticket 不属于当前 client */ + int CODE_30011 = 30011; + + /** 当前缺少配置 server-url 地址 */ + int CODE_30012 = 30012; + + /** 提供的 client 参数值无效 */ + int CODE_30013 = 30013; + + /** 在 /sso/auth 既没有指定 redirect 参数,也没有配置 homeRoute 路由 */ + int CODE_30014 = 30014; + + /** 无效的 allow-url 配置 */ + int CODE_30015 = 30015; + + /** 未能找到指定类型的消息处理器 */ + int CODE_30021 = 30021; + + /** 消息类型不能为空 */ + int CODE_30022 = 30022; + + /** 无效的消息推送地址 */ + int CODE_30023 = 30023; + + /** SSO 消息里缺少指定的参数 */ + int CODE_30024 = 30024; + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/exception/SaSsoException.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/exception/SaSsoException.java new file mode 100644 index 000000000..7d0f68d46 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/exception/SaSsoException.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.exception; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.util.SaFoxUtil; + + +/** + * 一个异常:代表 SSO 认证流程错误 + * + * @author click33 + * @since 1.30.0 + */ +public class SaSsoException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130114L; + + /** + * 一个异常:代表 SSO 认证流程错误 + * @param message 异常描述 + */ + public SaSsoException(String message) { + super(message); + } + + /** + * 一个异常:代表 SSO 认证流程错误 + * @param code 异常细分状态码 + * @param message 异常描述 + */ + public SaSsoException(int code, String message) { + super(code, message); + } + + /** + * 写入异常细分状态码 + * @param code 异常细分状态码 + * @return 对象自身 + */ + public SaSsoException setCode(int code) { + super.setCode(code); + return this; + } + + + /** + * 断言 flag 不为 true,否则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + * @param code 异常细分状态码 + */ + public static void notTrue(boolean flag, String message, int code) { + if(flag) { + throw new SaSsoException(message).setCode(code); + } + } + + /** + * 断言 value 不为空,否则抛出 message 异常 + * @param value 值 + * @param message 异常信息 + * @param code 异常细分状态码 + */ + public static void notEmpty(Object value, String message, int code) { + if(SaFoxUtil.isEmpty(value)) { + throw new SaSsoException(message).setCode(code); + } + } + + /** + * 如果flag==true,则抛出message异常 + * @param flag 标记 + * @param message 异常信息 + */ + @Deprecated + public static void throwBy(boolean flag, String message) { + if(flag) { + throw new SaSsoException(message); + } + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/CheckTicketAppendDataFunction.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/CheckTicketAppendDataFunction.java new file mode 100644 index 000000000..8630f024d --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/CheckTicketAppendDataFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.function; + +import cn.dev33.satoken.util.SaResult; + +import java.util.function.BiFunction; + +/** + * 函数式接口:sso-server 端:在校验 ticket 后,给 sso-client 端追加返回信息的函数 + * + *

    参数:loginId, SaResult 响应参数对象

    + *

    返回:SaResult 响应参数对象

    + * + * @author click33 + * @since 1.38.0 + */ +@FunctionalInterface +public interface CheckTicketAppendDataFunction extends BiFunction { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/DoLoginHandleFunction.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/DoLoginHandleFunction.java new file mode 100644 index 000000000..d10dcbd0e --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/DoLoginHandleFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.function; + +import java.util.function.BiFunction; + +/** + * 函数式接口:sso-server 端:登录处理函数 + * + *

    参数:账号、密码

    + *

    返回:登录结果

    + * + * @author click33 + * @since 1.38.0 + */ +@FunctionalInterface +public interface DoLoginHandleFunction extends BiFunction { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/NotLoginViewFunction.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/NotLoginViewFunction.java new file mode 100644 index 000000000..637a56893 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/NotLoginViewFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.function; + +import java.util.function.Supplier; + +/** + * 函数式接口:sso-server 端:未登录时返回的 View + * + *

    参数:无

    + *

    返回:未登录时的 View 视图

    + * + * @author click33 + * @since 1.38.0 + */ +@FunctionalInterface +public interface NotLoginViewFunction extends Supplier { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SaSsoMessageHandleFunction.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SaSsoMessageHandleFunction.java new file mode 100644 index 000000000..532142440 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SaSsoMessageHandleFunction.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.function; + +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.template.SaSsoTemplate; + +/** + * 函数式接口:处理 SSO 消息的函数式接口 + * + *

    参数:ssoTemplate 模板对象, 要处理的 message 消息

    + *

    返回:任意值

    + * + * @author click33 + * @since 1.38.0 + */ +@FunctionalInterface +public interface SaSsoMessageHandleFunction { + + Object execute(SaSsoTemplate ssoTemplate, SaSsoMessage message); + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SendRequestFunction.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SendRequestFunction.java new file mode 100644 index 000000000..6ec3f5908 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SendRequestFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.function; + +import java.util.function.Function; + +/** + * 函数式接口:发送 Http 请求的处理函数 + * + *

    参数:要请求的url

    + *

    返回:请求结果

    + * + * @author click33 + * @since 1.38.0 + */ +@FunctionalInterface +public interface SendRequestFunction extends Function { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/TicketResultHandleFunction.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/TicketResultHandleFunction.java new file mode 100644 index 000000000..e8f119e80 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/TicketResultHandleFunction.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.function; + +import cn.dev33.satoken.sso.model.SaCheckTicketResult; + +/** + * 函数式接口:sso-client 端:自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) + * + *

    参数:loginId, back

    + *

    返回:返回给前端的值

    + * + * @author click33 + * @since 1.38.0 + */ +@FunctionalInterface +public interface TicketResultHandleFunction { + + Object run(SaCheckTicketResult ctr, String back); + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessage.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessage.java new file mode 100644 index 000000000..4de22f667 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessage.java @@ -0,0 +1,126 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.message; + + +import cn.dev33.satoken.application.SaSetValueInterface; +import cn.dev33.satoken.sso.error.SaSsoErrorCode; +import cn.dev33.satoken.sso.exception.SaSsoException; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * SSO 消息 Model + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoMessage extends LinkedHashMap implements SaSetValueInterface, Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * KEY:TYPE + */ + public static final String MSG_TYPE = "msgType"; + + public SaSsoMessage() { + + } + + /** + * 构造函数 + * @param type 消息类型 + */ + public SaSsoMessage(String type) { + setType(type); + } + + /** + * 构造函数 + * @param map 消息参数 + */ + public SaSsoMessage(Map map) { + this.putAll(map); + } + + /** + * 获取消息类型 + * @return / + */ + public String getType() { + return getString(MSG_TYPE); + } + + /** + * 设置消息类型 + * @param type / + * @return / + */ + public SaSsoMessage setType(String type) { + return set(MSG_TYPE, type); + } + + /** + * 校验消息类型 + */ + public void checkType() { + if(SaFoxUtil.isEmpty(getString(MSG_TYPE))) { + throw new SaSsoException("消息类型不可为空").setCode(SaSsoErrorCode.CODE_30022); + } + } + + // ----------- + + @Override + public Object get(String key) { + return super.get(key); + } + + @Override + public SaSsoMessage set(String key, Object value) { + super.put(key, value); + return this; + } + + @Override + public SaSsoMessage delete(String key) { + super.remove(key); + return this; + } + + // ----------- + + /** + * 获取一个值 (此值必须存在,否则抛出异常 ) + * @param key 键 + * @return 参数值 + */ + public Object getValueNotNull(String key) { + Object value = get(key); + if(SaFoxUtil.isEmpty(value)) { + throw new SaSsoException("缺少参数:" + key).setCode(SaSsoErrorCode.CODE_30024); + } + return value; + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessageHolder.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessageHolder.java new file mode 100644 index 000000000..c2f66aa41 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessageHolder.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.message; + + +import cn.dev33.satoken.sso.error.SaSsoErrorCode; +import cn.dev33.satoken.sso.exception.SaSsoException; +import cn.dev33.satoken.sso.function.SaSsoMessageHandleFunction; +import cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle; +import cn.dev33.satoken.sso.message.handle.SaSsoMessageSimpleHandle; +import cn.dev33.satoken.sso.template.SaSsoTemplate; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * SSO 消息处理器 - 持有器 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoMessageHolder { + + /** + * 所有已注册的消息处理器 + */ + public final Map messageHandleMap = new LinkedHashMap<>(); + + /** + * 判断是否具有指定类型的消息处理器 + * + * @param type 消息类型 + * @return / + */ + public boolean hasHandle(String type) { + return messageHandleMap.containsKey(type); + } + + /** + * 删除指定类型的消息处理器 + * + * @param type 消息类型 + */ + public SaSsoMessageHolder removeHandle(String type) { + messageHandleMap.remove(type); + return this; + } + + /** + * 添加指定类型的消息处理器 + * + * @param handle / + * @return 对象自身 + */ + public SaSsoMessageHolder addHandle(SaSsoMessageHandle handle) { + messageHandleMap.put(handle.getHandlerType(), handle); + return this; + } + + /** + * 添加指定类型的简单消息处理器 + * + * @param type 要处理的消息类型 + * @param handle 要执行的方法 + * @return 对象自身 + */ + public SaSsoMessageHolder addHandle(String type, SaSsoMessageHandleFunction handle) { + messageHandleMap.put(type, new SaSsoMessageSimpleHandle(type, handle)); + return this; + } + + /** + * 获取指定类型的消息处理器 + * + * @param type / + */ + public SaSsoMessageHandle getHandle(String type) { + return messageHandleMap.get(type); + } + + /** + * 处理指定消息 + * + * @param ssoTemplate / + * @param message / + * @return 处理结果 + */ + public Object handleMessage(SaSsoTemplate ssoTemplate, SaSsoMessage message) { + SaSsoMessageHandle handle = messageHandleMap.get(message.getType()); + if(handle == null) { + throw new SaSsoException("未能找到消息处理器: " + message.getType()).setCode(SaSsoErrorCode.CODE_30021); + } + return handle.handle(ssoTemplate, message); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageHandle.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageHandle.java new file mode 100644 index 000000000..deac71dac --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageHandle.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.message.handle; + + +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.template.SaSsoTemplate; + +/** + * SSO 消息处理器 - 父接口 + * + * @author click33 + * @since 1.43.0 + */ +public interface SaSsoMessageHandle { + + /** + * 获取所要处理的消息类型 + * + * @return / + */ + String getHandlerType(); + + /** + * 具体要执行的处理方法 + * + * @param ssoTemplate / + * @param message / + * @return / + */ + Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message); + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageSimpleHandle.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageSimpleHandle.java new file mode 100644 index 000000000..9ddebe32b --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageSimpleHandle.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.message.handle; + + +import cn.dev33.satoken.sso.function.SaSsoMessageHandleFunction; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.template.SaSsoTemplate; + +/** + * SSO 消息处理器 - 简单实现,方便 lambda 表达式编程 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoMessageSimpleHandle implements SaSsoMessageHandle{ + + public String type; + + public SaSsoMessageHandleFunction handle; + + /** + * SSO 消息处理器 - 简单实现,方便 lambda 表达式编程 + * @param type 要处理的消息类型 + * @param handle 要执行的方法 + */ + public SaSsoMessageSimpleHandle(String type, SaSsoMessageHandleFunction handle) { + this.type = type; + this.handle = handle; + } + + /** + * 获取所要处理的消息类型 + * + * @return / + */ + @Override + public String getHandlerType() { + return type; + } + + /** + * 具体要执行的处理方法 + * + * @param ssoTemplate / + * @param message / + * @return / + */ + @Override + public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message){ + return handle.execute(ssoTemplate, message); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/client/SaSsoMessageLogoutCallHandle.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/client/SaSsoMessageLogoutCallHandle.java new file mode 100644 index 000000000..025fc953d --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/client/SaSsoMessageLogoutCallHandle.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.message.handle.client; + + +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle; +import cn.dev33.satoken.sso.name.ParamName; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoTemplate; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.util.SaResult; + +/** + * SSO 消息处理器 - sso-client 端:处理 单点注销回调 的请求 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoMessageLogoutCallHandle implements SaSsoMessageHandle { + + /** + * 获取所要处理的消息类型 + * + * @return / + */ + public String getHandlerType() { + return SaSsoConsts.MESSAGE_LOGOUT_CALL; + } + + /** + * 执行方法 + * + * @param ssoTemplate / + * @param message / + * @return / + */ + public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) { + + // 1、获取对象 + SaSsoClientTemplate ssoClientTemplate = (SaSsoClientTemplate) ssoTemplate; + StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); + ParamName paramName = ssoClientTemplate.paramName; + + // 2、判断当前应用是否开启单点注销功能 + if( ! ssoClientTemplate.getClientConfig().getIsSlo()) { + return SaResult.error("当前 sso-client 端未开启单点注销功能"); + } + + // 3、获取参数 + Object loginId = message.getValueNotNull(paramName.loginId); + loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(loginId); + String deviceId = message.getString(paramName.deviceId); + + // 4、注销当前应用端会话 + stpLogic.logout(loginId, new SaLogoutParameter() + .setDeviceId(deviceId) + ); + + // 5、响应 + return SaResult.ok("单点注销回调成功"); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageCheckTicketHandle.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageCheckTicketHandle.java new file mode 100644 index 000000000..1511b6204 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageCheckTicketHandle.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.message.handle.server; + + +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle; +import cn.dev33.satoken.sso.model.TicketModel; +import cn.dev33.satoken.sso.name.ParamName; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import cn.dev33.satoken.sso.template.SaSsoTemplate; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.util.SaResult; + +/** + * SSO 消息处理器 - sso-server 端:处理校验 ticket 的请求 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoMessageCheckTicketHandle implements SaSsoMessageHandle { + + /** + * 获取所要处理的消息类型 + * + * @return / + */ + public String getHandlerType() { + return SaSsoConsts.MESSAGE_CHECK_TICKET; + } + + /** + * 执行方法 + * + * @param ssoTemplate / + * @param message / + * @return / + */ + public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) { + + // 1、获取对象 + SaSsoServerTemplate ssoServerTemplate = (SaSsoServerTemplate) ssoTemplate; + ParamName paramName = ssoServerTemplate.paramName; + StpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal(); + String client = message.getString(paramName.client); + String ticket = message.getValueNotNull(paramName.ticket).toString(); + String sloCallback = message.getString(paramName.ssoLogoutCall); + + // 2、校验ticket,获取 loginId + TicketModel ticketModel = ssoServerTemplate.checkTicketParamAndDelete(ticket, client); + Object loginId = ticketModel.getLoginId(); + + // 3、注册此客户端的登录信息 + ssoServerTemplate.registerSloCallbackUrl(loginId, client, sloCallback); + + // 4、给 client 端响应结果 + SaResult result = SaResult.ok(); + result.setData(loginId); // 兼容历史版本 + result.set(paramName.loginId, loginId); + result.set(paramName.tokenValue, ticketModel.getTokenValue()); + result.set(paramName.deviceId, stpLogic.getLoginDeviceIdByToken(ticketModel.getTokenValue())); + result.set(paramName.remainTokenTimeout, stpLogic.getTokenTimeout(ticketModel.getTokenValue())); + result.set(paramName.remainSessionTimeout, stpLogic.getSessionTimeoutByLoginId(loginId)); + result = ssoServerTemplate.strategy.checkTicketAppendData.apply(loginId, result); + return result; + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageSignoutHandle.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageSignoutHandle.java new file mode 100644 index 000000000..b11ebdf09 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageSignoutHandle.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.message.handle.server; + + +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle; +import cn.dev33.satoken.sso.name.ParamName; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import cn.dev33.satoken.sso.template.SaSsoTemplate; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.util.SaResult; + +/** + * SSO 消息处理器 - sso-server 端:处理 单点注销 的请求 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoMessageSignoutHandle implements SaSsoMessageHandle { + + /** + * 获取所要处理的消息类型 + * + * @return / + */ + public String getHandlerType() { + return SaSsoConsts.MESSAGE_SIGNOUT; + } + + /** + * 执行方法 + * + * @param ssoTemplate / + * @param message / + * @return / + */ + public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) { + + // 1、获取对象 + SaSsoServerTemplate ssoServerTemplate = (SaSsoServerTemplate) ssoTemplate; + ParamName paramName = ssoServerTemplate.paramName; + + // 2、判断当前是否开启了全局单点注销功能 + if( ! ssoServerTemplate.getServerConfig().getIsSlo()) { + return SaResult.error("当前 sso-server 端未开启单点注销功能"); + } + + // 3、获取参数 + String client = message.getString(paramName.client); + Object loginId = message.get(paramName.loginId); + String deviceId = message.getString(paramName.deviceId); + + // 4、单点注销 + SaLogoutParameter logoutParameter = ssoServerTemplate.getStpLogicOrGlobal().createSaLogoutParameter().setDeviceId(deviceId); + ssoServerTemplate.ssoLogout(loginId, logoutParameter, client); + + // 5、响应 + return SaResult.ok(); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaCheckTicketResult.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaCheckTicketResult.java new file mode 100644 index 000000000..43f82f39c --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaCheckTicketResult.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.model; + +import cn.dev33.satoken.util.SaResult; + +import java.io.Serializable; + +/** + * 校验 ticket 返回 loginId 等结果的参数封装 + * + * @author click33 + * @since 1.38.0 + */ +public class SaCheckTicketResult implements Serializable { + + private static final long serialVersionUID = 1406115065849845073L; + + /** 账号id */ + public Object loginId; + + /** 在 sso-server 端的 token 值 */ + public String tokenValue; + + /** 登录设备 id */ + public String deviceId; + + /** 此账号 token 剩余有效期 */ + public Long remainTokenTimeout; + + /** 此账号会话剩余有效期 */ + public Long remainSessionTimeout; + + /** 此账号在认证中心的 loginId */ + public Object centerId; + + /** 从 sso-server 返回的原生所有参数 */ + public SaResult result; + + @Override + public String toString() { + return "SaCheckTicketResult{" + + "loginId=" + loginId + + ", tokenValue='" + tokenValue + '\'' + + ", deviceId='" + deviceId + '\'' + + ", remainTokenTimeout=" + remainTokenTimeout + + ", remainSessionTimeout=" + remainSessionTimeout + + ", centerId=" + centerId + + ", result=" + result + + '}'; + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientInfo.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientInfo.java new file mode 100644 index 000000000..e203bb961 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientInfo.java @@ -0,0 +1,191 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.model; + + +import cn.dev33.satoken.sso.util.SaSsoConsts; + +import java.io.Serializable; + +/** + * Sa-Token SSO 应用信息(注册在 SaSession 上的已登录应用信息列表) + * + * @author click33 + * @since 1.38.0 + */ +public class SaSsoClientInfo implements Serializable { + + private static final long serialVersionUID = 1406115065849845073L; + + /* + * 只能记录模式三登录的 client 信息,模式一和模式二的信息即使记录上,也无法完成单点注销操作,遂不记录 + * 所以:mode、tokenValue 字段,仅留作扩展,暂时无用 + */ + + /** + * 此 client 登录模式(1=模式一,2=模式二,3=模式三) + */ + public int mode; + + /** + * 客户端标识 + */ + public String client; + + /** + * 单点注销回调 url + */ + public String sloCallbackUrl; + + /** + * 此 client 注册信息的时间,13位时间戳 + */ + public long regTime; + + /** + * 此账号有记录以来为第几次登录,默认从0开始递增 + */ + public int index; + + public SaSsoClientInfo() { + } + + /** + * 模式三构建 + */ + public SaSsoClientInfo(String client, String sloCallbackUrl, int index) { + this.mode = SaSsoConsts.SSO_MODE_3; + this.client = client; + this.sloCallbackUrl = sloCallbackUrl; + this.regTime = System.currentTimeMillis(); + this.index = index; + } + + + // get set + + /** + * 获取 此 client 登录模式(1=模式一,2=模式二,3=模式三) + * + * @return mode 此 client 登录模式(1=模式一,2=模式二,3=模式三) + */ + public int getMode() { + return this.mode; + } + + /** + * 设置 此 client 登录模式(1=模式一,2=模式二,3=模式三) + * + * @param mode 此 client 登录模式(1=模式一,2=模式二,3=模式三) + * @return / + */ + public SaSsoClientInfo setMode(int mode) { + this.mode = mode; + return this; + } + + /** + * 获取 客户端标识 + * + * @return client 客户端标识 + */ + public String getClient() { + return this.client; + } + + /** + * 设置 客户端标识 + * + * @param client 客户端标识 + * @return / + */ + public SaSsoClientInfo setClient(String client) { + this.client = client; + return this; + } + + /** + * 获取 单点注销回调url + * + * @return ssoLogoutCall 单点注销回调url + */ + public String getSloCallbackUrl() { + return this.sloCallbackUrl; + } + + /** + * 设置 单点注销回调url + * + * @param sloCallbackUrl 单点注销回调url + * @return / + */ + public SaSsoClientInfo setSloCallbackUrl(String sloCallbackUrl) { + this.sloCallbackUrl = sloCallbackUrl; + return this; + } + + /** + * 获取 此 client 注册信息的时间,13位时间戳 + * + * @return regTime 此 client 注册信息的时间,13位时间戳 + */ + public long getRegTime() { + return this.regTime; + } + + /** + * 设置 此 client 注册信息的时间,13位时间戳 + * + * @param regTime 此 client 注册信息的时间,13位时间戳 + * @return / + */ + public SaSsoClientInfo setRegTime(long regTime) { + this.regTime = regTime; + return this; + } + + /** + * 获取 此账号有记录以来为第几次登录,默认从0开始递增 + * + * @return regTime 此账号有记录以来为第几次登录,默认从0开始递增 + */ + public long getIndex() { + return this.index; + } + + /** + * 设置 此账号有记录以来为第几次登录,默认从0开始递增 + * + * @param index 此账号有记录以来为第几次登录,默认从0开始递增 + * @return / + */ + public SaSsoClientInfo setIndex(int index) { + this.index = index; + return this; + } + + @Override + public String toString() { + return "SaSsoClientModel{" + + "mode=" + mode + + ", client='" + client + '\'' + + ", sloCallbackUrl='" + sloCallbackUrl + '\'' + + ", regTime=" + regTime + + ", index=" + index + + '}'; + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientModel.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientModel.java new file mode 100644 index 000000000..2c8a22bb2 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientModel.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.model; + + +/** + * Sa-Token SSO Model + * + * @author click33 + * @since 1.38.0 + */ +@Deprecated +public class SaSsoClientModel extends SaSsoClientInfo { + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/TicketModel.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/TicketModel.java new file mode 100644 index 000000000..fcb060d43 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/TicketModel.java @@ -0,0 +1,191 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.model; + +import java.io.Serializable; + +/** + * Model: Ticket 码 + * + * @author click33 + * @since 1.43.0 + */ +public class TicketModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * ticket 码 + */ + public String ticket; + + /** + * 应用标识 + */ + public String client; + + /** + * 对应 loginId + */ + public Object loginId; + + /** + * 会话 token + */ + public String tokenValue; + + /** + * 创建时间,13位时间戳 + */ + public long createTime; + + /** + * 构建一个 + */ + public TicketModel() { + this.createTime = System.currentTimeMillis(); + } + + /** + * 构建一个 + * @param ticket 授权码 + * @param client 应用id + * @param loginId 对应的账号id + * @param tokenValue 会话 token + */ + public TicketModel(String ticket, String client, Object loginId, String tokenValue) { + this(); + this.ticket = ticket; + this.client = client; + this.loginId = loginId; + this.tokenValue = tokenValue; + } + + + // get set + + /** + * 获取 ticket 码 + * + * @return / + */ + public String getTicket() { + return this.ticket; + } + + /** + * 设置 ticket 码 + * + * @param ticket / + * @return 对象自身 + */ + public TicketModel setTicket(String ticket) { + this.ticket = ticket; + return this; + } + + /** + * 获取 应用标识 + * + * @return / + */ + public String getClient() { + return this.client; + } + + /** + * 设置 应用标识 + * + * @param client / + * @return 对象自身 + */ + public TicketModel setClient(String client) { + this.client = client; + return this; + } + + /** + * 获取 对应 loginId + * + * @return / + */ + public Object getLoginId() { + return this.loginId; + } + + /** + * 设置 对应 loginId + * + * @param loginId / + * @return 对象自身 + */ + public TicketModel setLoginId(Object loginId) { + this.loginId = loginId; + return this; + } + + /** + * 获取 会话 token + * + * @return tokenValue 会话 token + */ + public String getTokenValue() { + return this.tokenValue; + } + + /** + * 设置 会话 token + * + * @param tokenValue 会话 token + * @return 对象自身 + */ + public TicketModel setTokenValue(String tokenValue) { + this.tokenValue = tokenValue; + return this; + } + + /** + * 获取 创建时间,13位时间戳 + * + * @return / + */ + public long getCreateTime() { + return this.createTime; + } + + /** + * 设置 创建时间,13位时间戳 + * + * @param createTime / + * @return 对象自身 + */ + public TicketModel setCreateTime(long createTime) { + this.createTime = createTime; + return this; + } + + @Override + public String toString() { + return "TicketModel{" + + "ticket='" + ticket + '\'' + + ", client='" + client + '\'' + + ", loginId=" + loginId + + ", tokenValue=" + tokenValue + + ", createTime=" + createTime + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ApiName.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ApiName.java new file mode 100644 index 000000000..34dff314e --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ApiName.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.name; + +/** + * SSO 模块所有 API 路由名称定义 + * + * @author click33 + * @since 1.32.0 + */ +public class ApiName { + + /** SSO-Server端:授权地址 */ + public String ssoAuth = "/sso/auth"; + + /** SSO-Server端:RestAPI 登录接口 */ + public String ssoDoLogin = "/sso/doLogin"; + + /** SSO-Server端:校验ticket 获取账号id */ + public String ssoCheckTicket = "/sso/checkTicket"; + + /** SSO-Server端:接收推送消息 */ + public String ssoPushS = "/sso/pushS"; + + /** SSO-Server端:获取userinfo */ + public String ssoUserinfo = "/sso/userinfo"; + + /** SSO-Server端:单点注销地址 */ + public String ssoSignout = "/sso/signout"; + + /** SSO-Client端:登录地址 */ + public String ssoLogin = "/sso/login"; + + /** SSO-Client端:单点注销地址 */ + public String ssoLogout = "/sso/logout"; + + /** SSO-Client端:判断当前是否登录地址 */ + public String ssoIsLogin = "/sso/isLogin"; + + /** SSO-Client端:单点注销的回调 */ + public String ssoLogoutCall = "/sso/logoutCall"; + + /** SSO-Client端:接收推送消息 */ + public String ssoPushC = "/sso/pushC"; + + /** + * 批量修改 path,新增固定前缀 + * @param prefix 示例值:/sso-user、/sso-admin + * @return 对象自身 + */ + public ApiName addPrefix(String prefix) { + this.ssoAuth = prefix + this.ssoAuth; + this.ssoDoLogin = prefix + this.ssoDoLogin; + this.ssoCheckTicket = prefix + this.ssoCheckTicket; + this.ssoPushS = prefix + this.ssoPushS; + this.ssoUserinfo = prefix + this.ssoUserinfo; + this.ssoSignout = prefix + this.ssoSignout; + this.ssoLogin = prefix + this.ssoLogin; + this.ssoLogout = prefix + this.ssoLogout; + this.ssoIsLogin = prefix + this.ssoIsLogin; + this.ssoPushC = prefix + this.ssoPushC; + this.ssoLogoutCall = prefix + this.ssoLogoutCall; + return this; + } + + /** + * 批量修改 path,替换掉 /sso 固定前缀 + * @param prefix 示例值:/sso-user、/sso-admin + * @return 对象自身 + */ + public ApiName replacePrefix(String prefix) { + String oldPrefix = "/sso"; + this.ssoAuth = this.ssoAuth.replaceFirst(oldPrefix, prefix); + this.ssoDoLogin = this.ssoDoLogin.replaceFirst(oldPrefix, prefix); + this.ssoCheckTicket = this.ssoCheckTicket.replaceFirst(oldPrefix, prefix); + this.ssoPushS = this.ssoPushS.replaceFirst(oldPrefix, prefix); + this.ssoUserinfo = this.ssoUserinfo.replaceFirst(oldPrefix, prefix); + this.ssoSignout = this.ssoSignout.replaceFirst(oldPrefix, prefix); + this.ssoLogin = this.ssoLogin.replaceFirst(oldPrefix, prefix); + this.ssoLogout = this.ssoLogout.replaceFirst(oldPrefix, prefix); + this.ssoIsLogin = this.ssoIsLogin.replaceFirst(oldPrefix, prefix); + this.ssoPushC = this.ssoPushC.replaceFirst(oldPrefix, prefix); + this.ssoLogoutCall = this.ssoLogoutCall.replaceFirst(oldPrefix, prefix); + return this; + } + + @Override + public String toString() { + return "ApiName{" + + "ssoAuth='" + ssoAuth + '\'' + + ", ssoDoLogin='" + ssoDoLogin + '\'' + + ", ssoCheckTicket='" + ssoCheckTicket + '\'' + + ", ssoPushS='" + ssoPushS + '\'' + + ", ssoUserinfo='" + ssoUserinfo + '\'' + + ", ssoSignout='" + ssoSignout + '\'' + + ", ssoIsLogin='" + ssoIsLogin + '\'' + + ", ssoLogin='" + ssoLogin + '\'' + + ", ssoLogout='" + ssoLogout + '\'' + + ", ssoLogoutCall='" + ssoLogoutCall + '\'' + + ", ssoPushC='" + ssoPushC + '\'' + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ParamName.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ParamName.java new file mode 100644 index 000000000..c3422c87a --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ParamName.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.name; + +/** + * SSO 模块所有参数名称定义 + * + * @author click33 + * @since 1.32.0 + */ +public class ParamName { + + /** redirect 参数名称 */ + public String redirect = "redirect"; + + /** ticket 参数名称 */ + public String ticket = "ticket"; + + /** back 参数名称 */ + public String back = "back"; + + /** mode 参数名称 */ + public String mode = "mode"; + + /** 账号 id */ + public String loginId = "loginId"; + + /** client 应用标识 */ + public String client = "client"; + + /** token 名称 */ + public String tokenName = "tokenName"; + + /** token 值 */ + public String tokenValue = "tokenValue"; + + /** 设备 id */ + public String deviceId = "deviceId"; + + /** 接口参数签名秘钥 */ + public String secretkey = "secretkey"; + + /** Client 端单点注销时 - 回调 URL 参数名称 */ + public String ssoLogoutCall = "ssoLogoutCall"; + + /** 是否为超过 maxRegClient 触发的自动注销 */ + public String autoLogout = "autoLogout"; + + public String name = "name"; + public String pwd = "pwd"; + + public String timestamp = "timestamp"; + public String nonce = "nonce"; + public String sign = "sign"; + + /** Session 剩余有效期 参数名称 */ + public String remainSessionTimeout = "remainSessionTimeout"; + + /** token 剩余有效期 参数名称 */ + public String remainTokenTimeout = "remainTokenTimeout"; + + /** 是否单设备 id 注销 */ + public String singleDeviceIdLogout = "singleDeviceIdLogout"; + + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoClientProcessor.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoClientProcessor.java new file mode 100644 index 000000000..3cb2e7699 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoClientProcessor.java @@ -0,0 +1,417 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.processor; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.error.SaSsoErrorCode; +import cn.dev33.satoken.sso.exception.SaSsoException; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.model.SaCheckTicketResult; +import cn.dev33.satoken.sso.model.TicketModel; +import cn.dev33.satoken.sso.name.ApiName; +import cn.dev33.satoken.sso.name.ParamName; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; + +import java.util.Map; + +/** + * SSO 请求处理器 (Client端) + * + * @author click33 + * @since 1.38.0 + */ +public class SaSsoClientProcessor { + + /** + * 全局默认实例 + */ + public static SaSsoClientProcessor instance = new SaSsoClientProcessor(); + + /** + * 底层 SaSsoClientTemplate 对象 + */ + public SaSsoClientTemplate ssoClientTemplate = new SaSsoClientTemplate(); + + // ----------- SSO-Client 端路由分发 ----------- + + /** + * 分发 Client 端所有请求 + * @return 处理结果 + */ + public Object dister() { + ApiName apiName = ssoClientTemplate.apiName; + + // 获取对象 + SaRequest req = SaHolder.getRequest(); + SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); + + // ------------------ 路由分发 ------------------ + + // sso-client:登录地址 + if(req.isPath(apiName.ssoLogin)) { + return ssoLogin(); + } + + // sso-client:单点注销 + if(req.isPath(apiName.ssoLogout)) { + return ssoLogout(); + } + + // sso-client:接收消息推送 + if(req.isPath(apiName.ssoPushC)) { + return ssoPushC(); + } + + // sso-client:单点注销的回调 + if(req.isPath(apiName.ssoLogoutCall) && cfg.getRegLogoutCall()) { + return ssoLogoutCall(); + } + + // 默认返回 + return SaSsoConsts.NOT_HANDLE; + } + + /** + * SSO-Client端:登录地址 + * @return 处理结果 + */ + public Object ssoLogin() { + // 获取对象 + SaRequest req = SaHolder.getRequest(); + ParamName paramName = ssoClientTemplate.paramName; + String ticket = req.getParam(paramName.ticket); + + /* + * 此时有两种情况: + * 情况1:ticket 无值,说明此请求是 sso-client 端访问,需要重定向至 sso-server 认证中心 + * 情况2:ticket 有值,说明此请求从 sso-server 认证中心重定向而来,需要根据 ticket 进行登录 + */ + if(ticket == null) { + return _goServerAuth(); + } else { + return _loginByTicket(); + } + } + + /** + * SSO-Client端:单点注销 + * @return 处理结果 + */ + public Object ssoLogout() { + // 获取对象 + SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); + + // 无论登录时选择的是模式二还是模式三 + // 在注销时都应该按照模式三的方法,通过 http 请求调用 sso-server 的单点注销接口来做到全端下线 + // 如果按照模式二的方法注销,则会导致按照模式三登录的应用无法参与到单点注销环路中来 + if(cfg.getIsSlo()) { + return _ssoLogoutByMode3(); + } + + // 默认返回 + return SaSsoConsts.NOT_HANDLE; + } + + /** + * SSO-Client端:接收推送消息 + * + * @return 处理结果 + */ + public Object ssoPushC() { + SaSsoClientConfig ssoClientConfig = ssoClientTemplate.getClientConfig(); + + // 1、校验签名 + Map paramMap = SaHolder.getRequest().getParamMap(); + if(ssoClientConfig.getIsCheckSign()) { + ssoClientTemplate.getSignTemplate().checkParamMap(paramMap); + } else { + SaSsoManager.printNoCheckSignWarningByRuntime(); + } + + // 2、处理消息 + SaSsoMessage message = new SaSsoMessage(paramMap); + return ssoClientTemplate.handleMessage(message); + } + + /** + * SSO-Client端:单点注销的回调 [模式三] + * @return 处理结果 + */ + public Object ssoLogoutCall() { + + // 获取对象 + SaRequest req = SaHolder.getRequest(); + StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); + ParamName paramName = ssoClientTemplate.paramName; + SaSsoClientConfig ssoConfig = ssoClientTemplate.getClientConfig(); + + // 获取参数 + Object loginId = req.getParamNotNull(paramName.loginId); + loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(loginId); + String deviceId = req.getParam(paramName.deviceId); + + // 校验参数签名 + if(ssoConfig.getIsCheckSign()) { + ssoClientTemplate.getSignTemplate().checkRequest(req); + } else { + SaSsoManager.printNoCheckSignWarningByRuntime(); + } + + // 注销当前应用端会话 + SaLogoutParameter logoutParameter = ssoClientTemplate.getStpLogicOrGlobal().createSaLogoutParameter(); + stpLogic.logout(loginId, logoutParameter.setDeviceId(deviceId)); + + // 响应 + return SaResult.ok("单点注销回调成功"); + } + + // 次级方法 + + /** + * 跳转去 sso-server 认证中心 + * @return / + */ + public Object _goServerAuth() { + // 获取对象 + SaRequest req = SaHolder.getRequest(); + SaResponse res = SaHolder.getResponse(); + SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); + StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); + ParamName paramName = ssoClientTemplate.paramName; + + // 获取参数 + String back = req.getParam(paramName.back, "/"); + + // 如果当前 sso-client 端已经登录,则无需访问 SSO 认证中心,可以直接返回 + if(stpLogic.isLogin()) { + return res.redirect(back); + } + + // 获取当前项目的 sso 登录中转页地址,形如:http://sso-client.com/sso/login + // 全局配置了就是用全局的,否则使用当前请求的地址 + String currSsoLoginUrl = cfg.getCurrSsoLogin(); + if(SaFoxUtil.isEmpty(currSsoLoginUrl)) { + currSsoLoginUrl = SaHolder.getRequest().getUrl(); + } + // 构建最终授权地址 url,形如:http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com + String serverAuthUrl = ssoClientTemplate.buildServerAuthUrl(currSsoLoginUrl, back); + return res.redirect(serverAuthUrl); + } + + /** + * 根据认证中心回传的 ticket 进行登录 + * @return / + */ + public Object _loginByTicket() { + // 获取对象 + SaRequest req = SaHolder.getRequest(); + SaResponse res = SaHolder.getResponse(); + StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); + ParamName paramName = ssoClientTemplate.paramName; + ApiName apiName = ssoClientTemplate.apiName; + + // 获取参数 + String back = req.getParam(paramName.back, "/"); + String ticket = req.getParam(paramName.ticket); + + // 1、校验 ticket,获取 loginId 等数据 + SaCheckTicketResult ctr = checkTicket(ticket, apiName.ssoLogin); + + // 2、如果开发者自定义了 ticket 结果值处理函数,则使用自定义的函数 + if(ssoClientTemplate.strategy.ticketResultHandle != null) { + return ssoClientTemplate.strategy.ticketResultHandle.run(ctr, back); + } + + // 3、登录并重定向至back地址 + stpLogic.login(ctr.loginId, new SaLoginParameter() + .setTimeout(ctr.remainTokenTimeout) + .setDeviceId(ctr.deviceId) + ); + return res.redirect(back); + } + + /** + * SSO-Client端:单点注销 [模式三] + * @return 处理结果 + */ + public Object _ssoLogoutByMode3() { + // 获取对象 + SaRequest req = SaHolder.getRequest(); + SaResponse res = SaHolder.getResponse(); + StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); + boolean singleDeviceIdLogout = req.isParam(ssoClientTemplate.paramName.singleDeviceIdLogout, "true"); + + // 如果未登录,则无需注销 + if( ! stpLogic.isLogin()) { + return _ssoLogoutBack(req, res); + } + + // 向 sso-server 认证中心推送消息:单点注销 + SaLogoutParameter logoutParameter = stpLogic.createSaLogoutParameter(); + if(singleDeviceIdLogout) { + logoutParameter.setDeviceId(stpLogic.getLoginDeviceId()); + } + Object loginId = stpLogic.getLoginId(); + Object centerId = ssoClientTemplate.strategy.convertLoginIdToCenterId.run(loginId); + SaSsoMessage message = ssoClientTemplate.buildSignoutMessage(centerId, logoutParameter); + SaResult result = ssoClientTemplate.pushMessageAsSaResult(message); + + // 如果 sso-server 响应的状态码非200,代表业务失败,将回应的 msg 字段作为异常抛出 + if(result.getCode() == null || SaResult.CODE_SUCCESS != result.getCode()) { + throw new SaSsoException(result.getMsg()).setCode(SaSsoErrorCode.CODE_30006); + } + + // 极端场景下,sso-server 中心的单点注销可能并不会通知到当前 client 端,所以这里需要再补一刀 + if(stpLogic.isLogin()) { + stpLogic.logout(loginId, logoutParameter); + } + return _ssoLogoutBack(req, res); + } + + /** + * 封装:校验ticket,取出loginId,如果 ticket 无效则抛出异常 (适用于模式二或模式三) + * + * @param ticket ticket码 + * @return SaCheckTicketResult + */ + public SaCheckTicketResult checkTicket(String ticket) { + return checkTicket(ticket, null); + } + + /** + * 封装:校验ticket,取出loginId,如果 ticket 无效则抛出异常 (适用于模式二或模式三) + * + * @param ticket ticket码 + * @param currUri 当前路由的uri,用于计算单点注销回调地址 (如果是使用模式二,可以填写null) + * @return SaCheckTicketResult + */ + public SaCheckTicketResult checkTicket(String ticket, String currUri) { + SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); + + // 两种模式: + // isHttp=true:模式三,使用 http 请求从认证中心校验ticket + // isHttp=false:模式二,直连 redis 中校验 ticket + if(cfg.getIsHttp()) { + return _checkTicketByHttp(ticket, currUri); + } else { + return _checkTicketByRedis(ticket); + } + } + + /** + * 校验 ticket,http 请求方式 + * @param ticket / + * @param currUri / + * @return / + */ + public SaCheckTicketResult _checkTicketByHttp(String ticket, String currUri) { + SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); + ApiName apiName = ssoClientTemplate.apiName; + ParamName paramName = ssoClientTemplate.paramName; + + // 计算当前 sso-client 的单点注销回调地址 + String ssoLogoutCall = null; + if(cfg.getRegLogoutCall()) { + // 如果配置了回调地址,就使用配置的值: + if(SaFoxUtil.isNotEmpty(cfg.getCurrSsoLogoutCall())) { + ssoLogoutCall = cfg.getCurrSsoLogoutCall(); + } + // 如果提供了当前 uri,则根据此值来计算: + else if(SaFoxUtil.isNotEmpty(currUri)) { + ssoLogoutCall = SaHolder.getRequest().getUrl().replace(currUri, apiName.ssoLogoutCall); + } + // 否则视为不注册单点注销回调地址 + else { + } + } + + // 发起请求 + SaSsoMessage message = ssoClientTemplate.buildCheckTicketMessage(ticket, ssoLogoutCall); + SaResult result = ssoClientTemplate.pushMessageAsSaResult(message); + + // 如果 sso-server 响应的状态码非200,代表业务失败,将回应的 msg 字段作为异常抛出 + if(result.getCode() == null || result.getCode() != SaResult.CODE_SUCCESS) { + throw new SaSsoException(result.getMsg()).setCode(SaSsoErrorCode.CODE_30005); + } + + // 构建返回结果 + SaCheckTicketResult ctr = new SaCheckTicketResult(); + ctr.loginId = result.get(paramName.loginId); + ctr.tokenValue = result.get(paramName.tokenValue, String.class); + ctr.deviceId = result.get(paramName.deviceId, String.class); + ctr.remainTokenTimeout = result.get(paramName.remainTokenTimeout, Long.class); + ctr.remainSessionTimeout = result.get(paramName.remainSessionTimeout, Long.class); + ctr.result = result; + + // 转换 loginId 和 centerId + ctr.centerId = ctr.loginId; + ctr.loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(ctr.centerId); + + return ctr; + } + + /** + * 校验 ticket,直连 redis 方式 + * @param ticket / + * @return / + */ + public SaCheckTicketResult _checkTicketByRedis(String ticket) { + // 直连 redis 校验 ticket + // 注意此处调用了 SaSsoServerProcessor 处理器里的方法, + // 这意味着如果你的 sso-server 端重写了 SaSsoServerProcessor 里的部分方法, + // 而在当前 sso-client 没有按照相应格式重写 SaSsoClientProcessor 里的方法, + // 可能会导致调用失败(注意是可能,而非一定,主要取决于你是否改变了数据读写格式), + // 解决方案为:在当前 sso-client 端也按照 sso-server 端的格式重写 SaSsoClientProcessor 里的方法 + + StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); + TicketModel ticketModel = SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, ssoClientTemplate.getClient()); + + SaCheckTicketResult ctr = new SaCheckTicketResult(); + ctr.loginId = ticketModel.getLoginId(); + ctr.tokenValue = ticketModel.getTokenValue(); + ctr.deviceId = stpLogic.getLoginDeviceIdByToken(ticketModel.getTokenValue()); + ctr.remainTokenTimeout = stpLogic.getTokenTimeout(ticketModel.getTokenValue()); + ctr.remainSessionTimeout = stpLogic.getSessionTimeoutByLoginId(ticketModel.getLoginId()); + ctr.result = null; + + // 转换 loginId 和 centerId + ctr.centerId = ctr.loginId; + ctr.loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(ctr.centerId); + + return ctr; + } + + /** + * 封装:单点注销成功后返回结果 + * @param req SaRequest对象 + * @param res SaResponse对象 + * @return 返回结果 + */ + public Object _ssoLogoutBack(SaRequest req, SaResponse res) { + return SaSsoProcessorHelper.ssoLogoutBack(req, res, ssoClientTemplate.paramName); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoProcessorHelper.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoProcessorHelper.java new file mode 100644 index 000000000..37084a94b --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoProcessorHelper.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.processor; + +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.sso.name.ParamName; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * SSO 请求处理器,辅助方法 + * + * @author click33 + * @since 1.38.0 + */ +public class SaSsoProcessorHelper { + + /** + * 封装:单点注销成功后返回结果 + * @param req SaRequest对象 + * @param res SaResponse对象 + * @return 返回结果 + */ + public static Object ssoLogoutBack(SaRequest req, SaResponse res, ParamName paramName) { + + /* + * 三种情况: + * 1. 有back参数,值为SELF -> 回退一级并刷新 + * 2. 有back参数,值为url -> 跳转到此url地址 + * 3. 无back参数 -> 返回json数据 + */ + String back = req.getParam(paramName.back); + if(SaFoxUtil.isNotEmpty(back)) { + if(back.equals(SaSsoConsts.SELF)) { + res.setHeader("Content-Type", "text/html; charset=utf-8"); + return ""; + } + return res.redirect(back); + } else { + return SaResult.ok("单点注销成功"); + } + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoServerProcessor.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoServerProcessor.java new file mode 100644 index 000000000..262f41982 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoServerProcessor.java @@ -0,0 +1,241 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.processor; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; +import cn.dev33.satoken.sso.error.SaSsoErrorCode; +import cn.dev33.satoken.sso.exception.SaSsoException; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.name.ApiName; +import cn.dev33.satoken.sso.name.ParamName; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; +import cn.dev33.satoken.util.SaSugar; + +import java.util.Map; + +/** + * SSO 请求处理器 (Server端) + * + * @author click33 + * @since 1.38.0 + */ +public class SaSsoServerProcessor { + + /** + * 全局默认实例 + */ + public static SaSsoServerProcessor instance = new SaSsoServerProcessor(); + + /** + * 底层 SaSsoServerTemplate 对象 + */ + public SaSsoServerTemplate ssoServerTemplate = new SaSsoServerTemplate(); + + // ----------- SSO-Server 端路由分发 ----------- + + /** + * 分发 Server 端所有请求 + * + * @return 处理结果 + */ + public Object dister() { + + // 获取对象 + SaRequest req = SaHolder.getRequest(); + ApiName apiName = ssoServerTemplate.apiName; + + // ------------------ 路由分发 ------------------ + + // sso-server:授权地址 + if(req.isPath(apiName.ssoAuth)) { + return ssoAuth(); + } + + // sso-server:RestAPI 登录接口 + if(req.isPath(apiName.ssoDoLogin)) { + return ssoDoLogin(); + } + + // sso-server:单点注销 + if(req.isPath(apiName.ssoSignout)) { + return ssoSignout(); + } + + // sso-server:接收推送消息 + if(req.isPath(apiName.ssoPushS)) { + return ssoPushS(); + } + + // 默认返回 + return SaSsoConsts.NOT_HANDLE; + } + + /** + * SSO-Server端:授权地址 + * @return 处理结果 + */ + public Object ssoAuth() { + // 获取对象 + SaRequest req = SaHolder.getRequest(); + SaResponse res = SaHolder.getResponse(); + SaSsoServerConfig cfg = ssoServerTemplate.getServerConfig(); + StpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal(); + ParamName paramName = ssoServerTemplate.paramName; + + // 两种情况: + // 情况1:在 SSO 认证中心尚未登录,需要显示登录视图,去登录 + // 情况2:在 SSO 认证中心已经登录,需要重定向回 Client 端 + + // 情况1,显示登录视图 + if( ! stpLogic.isLogin()) { + return ssoServerTemplate.strategy.notLoginView.get(); + } + + // 情况2,开始跳转 + String mode = req.getParam(paramName.mode, SaSsoConsts.MODE_TICKET); + String redirect = req.getParam(paramName.redirect); + String client = req.getParam(paramName.client); + + // 构建最终重定向地址 + String redirectUrl = SaSugar.get(() -> { + + // 若 redirect 参数为空,说明用户并不是从 client 重定向来的,而是直接访问的 http://sso-server.com/sso/auth 地址 + // 此时需要跳转到配置的 homeRoute 路由上, + // 若 homeRoute 也为空,则没有明确的跳转地址了,需要抛出异常 + if(SaFoxUtil.isEmpty(redirect)) { + if(SaFoxUtil.isEmpty(cfg.getHomeRoute())) { + throw new SaSsoException("未指定 redirect 参数,也未配置 homeRoute 路由,无法完成重定向操作").setCode(SaSsoErrorCode.CODE_30014); + } + return cfg.getHomeRoute(); + } + + // 方式1:直接重定向回Client端 (mode=simple,一般是模式一) + if(mode.equals(SaSsoConsts.MODE_SIMPLE)) { + ssoServerTemplate.checkRedirectUrl(client, redirect); + return redirect; + } else { + // 方式2:带着 ticket 参数重定向回Client端 (mode=ticket,一般是模式二、三) + + // 构建并跳转 + String _redirectUrl = ssoServerTemplate.buildRedirectUrl(client, redirect, stpLogic.getLoginId(), stpLogic.getTokenValue()); + + // 构建成功,说明 redirect 地址合法,此时需要更新一下当前 token 有效期 + if(cfg.getAutoRenewTimeout()) { + stpLogic.renewTimeout(stpLogic.getConfigOrGlobal().getTimeout()); + } + return _redirectUrl; + } + }); + + // 跳转 + ssoServerTemplate.strategy.jumpToRedirectUrlNotice.run(redirectUrl); + return res.redirect(redirectUrl); + } + + /** + * SSO-Server端:RestAPI 登录接口 + * @return 处理结果 + */ + public Object ssoDoLogin() { + // 获取参数 + SaRequest req = SaHolder.getRequest(); + ParamName paramName = ssoServerTemplate.paramName; + String name = req.getParam(paramName.name); + String pwd = req.getParam(paramName.pwd); + + // 处理 + return ssoServerTemplate.strategy.doLoginHandle.apply(name, pwd); + } + + /** + * SSO-Server端:单点注销 + * @return 处理结果 + */ + public Object ssoSignout() { + // 获取对象 + SaRequest req = SaHolder.getRequest(); + SaResponse res = SaHolder.getResponse(); + StpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal(); + Object loginId = stpLogic.getLoginIdDefaultNull(); + boolean singleDeviceIdLogout = req.isParam(ssoServerTemplate.paramName.singleDeviceIdLogout, "true"); + + // 单点注销 + if(SaFoxUtil.isNotEmpty(loginId)) { + SaLogoutParameter logoutParameter = stpLogic.createSaLogoutParameter(); + if(singleDeviceIdLogout) { + logoutParameter.setDeviceId(stpLogic.getLoginDeviceId()); + } + ssoServerTemplate.ssoLogout(loginId, logoutParameter, null); + } + + // 完成 + return _ssoLogoutBack(req, res); + } + + /** + * SSO-Server端:接收推送消息 + * + * @return 处理结果 + */ + public Object ssoPushS() { + ParamName paramName = ssoServerTemplate.paramName; + SaSsoServerConfig ssoServerConfig = ssoServerTemplate.getServerConfig(); + + // 1、获取参数 + SaRequest req = SaHolder.getRequest(); + String client = req.getParam(paramName.client); + + // 2、校验提供的client是否为非法字符 + if(SaSsoConsts.CLIENT_WILDCARD.equals(client)) { + return SaResult.error("无效 client 标识:" + client); + } + + // 3、校验参数签名 + Map paramMap = req.getParamMap(); + if(ssoServerConfig.getIsCheckSign()) { + ssoServerTemplate.getSignTemplate(client).checkParamMap(paramMap); + } else { + SaSsoManager.printNoCheckSignWarningByRuntime(); + } + + // 4、处理消息 + SaSsoMessage message = new SaSsoMessage(paramMap); + if( ! ssoServerTemplate.messageHolder.hasHandle(message.getType())) { + return SaResult.error("未能找到消息处理器:" + message.getType()); + } + return ssoServerTemplate.handleMessage(message); + } + + /** + * 封装:单点注销成功后返回结果 + * @param req SaRequest对象 + * @param res SaResponse对象 + * @return 返回结果 + */ + public Object _ssoLogoutBack(SaRequest req, SaResponse res) { + return SaSsoProcessorHelper.ssoLogoutBack(req, res, ssoServerTemplate.paramName); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoClientStrategy.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoClientStrategy.java new file mode 100644 index 000000000..755c0a0c4 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoClientStrategy.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.strategy; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.fun.SaParamRetFunction; +import cn.dev33.satoken.sso.function.SendRequestFunction; +import cn.dev33.satoken.sso.function.TicketResultHandleFunction; +import cn.dev33.satoken.util.SaResult; + +import java.util.Map; + +/** + * Sa-Token SSO Client 相关策略 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoClientStrategy { + + /** + * 发送 Http 请求的处理函数 + */ + public SendRequestFunction sendRequest = url -> { + return SaManager.getSaHttpTemplate().get(url); + }; + + /** + * 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) + *

    参数:loginId, back + *

    返回值:返回给前端的值 + */ + public TicketResultHandleFunction ticketResultHandle = null; + + /** + * 转换:认证中心 centerId > 本地 loginId + * + *

    参数:认证中心 centerId + *

    返回值:本地 loginId + */ + public SaParamRetFunction convertCenterIdToLoginId = (centerId) -> { + return centerId; + }; + + /** + * 转换:本地 loginId > 认证中心 centerId + * + *

    参数:本地 loginId + *

    返回值:认证中心 centerId + */ + public SaParamRetFunction convertLoginIdToCenterId = (loginId) -> { + return loginId; + }; + + /** + * 发送 Http 请求,并将响应结果转换为 SaResult + * + * @param url 请求地址 + * @return 返回的结果 + */ + public SaResult requestAsSaResult(String url) { + String body = sendRequest.apply(url); + Map map = SaManager.getSaJsonTemplate().jsonToMap(body); + return new SaResult(map); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoServerStrategy.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoServerStrategy.java new file mode 100644 index 000000000..31377e5ca --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoServerStrategy.java @@ -0,0 +1,93 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.strategy; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.sso.function.CheckTicketAppendDataFunction; +import cn.dev33.satoken.sso.function.DoLoginHandleFunction; +import cn.dev33.satoken.sso.function.NotLoginViewFunction; +import cn.dev33.satoken.sso.function.SendRequestFunction; +import cn.dev33.satoken.util.SaResult; + +import java.util.Map; + +/** + * Sa-Token SSO Server 相关策略 + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoServerStrategy { + + /** + * 发送 Http 请求的处理函数 + */ + public SendRequestFunction sendRequest = url -> { + return SaManager.getSaHttpTemplate().get(url); + }; + + /** + * 使用异步模式执行一个任务 + */ + public SaParamFunction asyncRun = fun -> { + new Thread(() -> { + fun.run(); + }).start(); + }; + + /** + * 未登录时返回的 View + */ + public NotLoginViewFunction notLoginView = () -> { + return "当前会话在 SSO-Server 认证中心尚未登录(当前未配置登录视图)"; + }; + + /** + * SSO-Server端:登录函数 + */ + public DoLoginHandleFunction doLoginHandle = (name, pwd) -> { + return SaResult.error(); + }; + + /** + * SSO-Server端:在授权重定向之前的通知 + */ + public SaParamFunction jumpToRedirectUrlNotice = (redirectUrl) -> { + + }; + + /** + * SSO-Server端:在校验 ticket 后,给 sso-client 端追加返回信息的函数 + */ + public CheckTicketAppendDataFunction checkTicketAppendData = (loginId, result) -> { + return result; + }; + + /** + * 发送 Http 请求,并将响应结果转换为 SaResult + * + * @param url 请求地址 + * @return 返回的结果 + */ + public SaResult requestAsSaResult(String url) { + String body = sendRequest.apply(url); + Map map = SaManager.getSaJsonTemplate().jsonToMap(body); + return new SaResult(map); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientTemplate.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientTemplate.java new file mode 100644 index 000000000..97dae7797 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientTemplate.java @@ -0,0 +1,262 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.template; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.template.SaSignTemplate; +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.error.SaSsoErrorCode; +import cn.dev33.satoken.sso.exception.SaSsoException; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.message.handle.client.SaSsoMessageLogoutCallHandle; +import cn.dev33.satoken.sso.strategy.SaSsoClientStrategy; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; + +import java.util.Map; + +/** + * SSO 模板方法类 (Client端) + * + * @author click33 + * @since 1.38.0 + */ +public class SaSsoClientTemplate extends SaSsoTemplate { + + /** + * Client 相关策略 + */ + public SaSsoClientStrategy strategy = new SaSsoClientStrategy(); + + public SaSsoClientTemplate() { + super.messageHolder.addHandle(new SaSsoMessageLogoutCallHandle()); + } + + + // ------------------- getData 相关 ------------------- + + /** + * 根据配置的 getData 地址,查询数据 + * + * @param paramMap 查询参数 + * @return 查询结果 + */ + public Object getData(Map paramMap) { + String getDataUrl = getClientConfig().splicingGetDataUrl(); + return getData(getDataUrl, paramMap); + } + + /** + * 根据自定义 path 地址,查询数据 (此方法需要配置 sa-token.sso.server-url 地址) + * + * @param path 自定义 path + * @param paramMap 查询参数 + * @return 查询结果 + */ + public Object getData(String path, Map paramMap) { + String url = buildCustomPathUrl(path, paramMap); + return strategy.sendRequest.apply(url); + } + + /** + * 构建URL:Server端 getData 地址,带签名等参数 + * @param paramMap 查询参数 + * @return / + */ + public String buildGetDataUrl(Map paramMap) { + String getDataUrl = getClientConfig().getGetDataUrl(); + return buildCustomPathUrl(getDataUrl, paramMap); + } + + /** + * 构建URL:Server 端自定义 path 地址,带签名等参数 (此方法需要配置 sa-token.sso.server-url 地址) + * @param paramMap 请求参数 + * @return / + */ + public String buildCustomPathUrl(String path, Map paramMap) { + SaSsoClientConfig ssoConfig = getClientConfig(); + + // 构建 url + // 如果 path 不是以 http 开头,那么就拼接上 serverUrl + String url = path; + if( ! url.startsWith("http") ) { + String serverUrl = ssoConfig.getServerUrl(); + SaSsoException.notEmpty(serverUrl, "请先配置 sa-token.sso-client.server-url 地址", SaSsoErrorCode.CODE_30012); + url = SaFoxUtil.spliceTwoUrl(serverUrl, path); + } + + // 构建参数字符串 + paramMap.put(paramName.client, getClient()); + String signParamsStr = getSignTemplate().addSignParamsAndJoin(paramMap); + + // 拼接 + return SaFoxUtil.joinParam(url, signParamsStr); + } + + + // ---------------------- 构建交互地址 ---------------------- + + /** + * 构建URL:Server端 单点登录授权地址, + *
    形如:http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com + * @param clientLoginUrl Client端登录地址 + * @param back 回调路径 + * @return [SSO-Server端-认证地址 ] + */ + public String buildServerAuthUrl(String clientLoginUrl, String back) { + SaSsoClientConfig ssoConfig = getClientConfig(); + + // 服务端认证地址 + String serverUrl = ssoConfig.splicingAuthUrl(); + + // 拼接客户端标识 + String client = getClient(); + if(SaFoxUtil.isNotEmpty(client)) { + serverUrl = SaFoxUtil.joinParam(serverUrl, paramName.client, client); + } + + // 对back地址编码 + back = (back == null ? "" : back); + back = SaFoxUtil.encodeUrl(back); + + // 开始拼接 sso 统一认证地址,形如:serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com + + /* + * 部分 Servlet 版本 request.getRequestURL() 返回的 url 带有 query 参数,形如:http://domain.com?id=1, + * 如果不加判断会造成最终生成的 serverAuthUrl 带有双 back 参数 ,这个 if 判断正是为了解决此问题 + */ + if( ! clientLoginUrl.contains(paramName.back + "=") ) { + clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, paramName.back, back); + } + + // 返回 + return SaFoxUtil.joinParam(serverUrl, paramName.redirect, clientLoginUrl); + } + + + // ------------------- 消息推送 ------------------- + + /** + * 向 sso-server 推送消息 + * + * @param message / + * @return / + */ + public String pushMessage(SaSsoMessage message) { + SaSsoClientConfig ssoConfig = getClientConfig(); + + // 拼接 push-url 地址 + String pushUrl = ssoConfig.splicingPushUrl(); + SaSsoException.notTrue(! SaFoxUtil.isUrl(pushUrl), "无效 push-url 地址:" + pushUrl, SaSsoErrorCode.CODE_30023); + + // 组织参数 + message.set(paramName.client, getClient()); + message.checkType(); + String paramsStr = getSignTemplate().addSignParamsAndJoin(message); + + // 发起请求 + String finalUrl = SaFoxUtil.joinParam(pushUrl, paramsStr); + return strategy.sendRequest.apply(finalUrl); + } + + /** + * 向 sso-server 推送消息,并将返回值转为 SaResult + * + * @param message / + * @return / + */ + public SaResult pushMessageAsSaResult(SaSsoMessage message) { + String res = pushMessage(message); + Map map = SaManager.getSaJsonTemplate().jsonToMap(res); + return new SaResult(map); + } + + /** + * 构建消息:校验 ticket + * + * @param ticket ticket码 + * @param ssoLogoutCallUrl 单点注销时的回调URL + * @return 构建完毕的URL + */ + public SaSsoMessage buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl) { + SaSsoClientConfig ssoConfig = getClientConfig(); + SaSsoMessage message = new SaSsoMessage(); + message.setType(SaSsoConsts.MESSAGE_CHECK_TICKET); + message.set(paramName.client, getClient()); + message.set(paramName.ticket, ticket); + message.set(paramName.ssoLogoutCall, ssoLogoutCallUrl); + return message; + } + + /** + * 构建消息:单点注销 + * + * @param loginId 要注销的账号 id + * @param logoutParameter 单点注销 + * @return 单点注销URL + */ + public SaSsoMessage buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter) { + SaSsoMessage message = new SaSsoMessage(); + message.setType(SaSsoConsts.MESSAGE_SIGNOUT); + message.set(paramName.client, getClient()); + message.set(paramName.loginId, loginId); + message.set(paramName.deviceId, logoutParameter.getDeviceId()); + return message; + } + + + // ------------------- Bean 对象获取 ------------------- + + /** + * 获取底层使用的SsoClient配置对象 + * @return / + */ + public SaSsoClientConfig getClientConfig() { + return SaSsoManager.getClientConfig(); + } + + /** + * 获取当前项目 client 标识 + * @return / + */ + public String getClient() { + return getClientConfig().getClient(); + } + + /** + * 获取底层使用的 API 签名对象 + * + * @return / + */ + public SaSignTemplate getSignTemplate() { + SaSignConfig signConfig = SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().copy(); + + // 使用 secretKey 的优先级:SSO 模块全局配置 > sign 模块默认配置 + String secretKey = getClientConfig().getSecretKey(); + if(SaFoxUtil.isEmpty(secretKey)) { + secretKey = signConfig.getSecretKey(); + } + signConfig.setSecretKey(secretKey); + + return new SaSignTemplate(signConfig); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java new file mode 100644 index 000000000..1364f3de1 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.template; + +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.util.SaResult; + +import java.util.Map; + +/** + * SSO 模板方法类 (Client端) + * + * @author click33 + * @since 1.38.0 + */ +public class SaSsoClientUtil { + + private SaSsoClientUtil() { + } + + /** + * 返回底层使用的 SaSsoClientTemplate 对象 + * @return / + */ + public static SaSsoClientTemplate getSsoTemplate() { + return SaSsoClientProcessor.instance.ssoClientTemplate; + } + + + // ------------------- getData 相关 ------------------- + + /** + * 根据配置的 getData 地址,查询数据 + * + * @param paramMap 查询参数 + * @return 查询结果 + */ + public static Object getData(Map paramMap) { + return SaSsoClientProcessor.instance.ssoClientTemplate.getData(paramMap); + } + + /** + * 根据自定义 path 地址,查询数据 (此方法需要配置 sa-token.sso.server-url 地址) + * + * @param path 自定义 path + * @param paramMap 查询参数 + * @return 查询结果 + */ + public static Object getData(String path, Map paramMap) { + return SaSsoClientProcessor.instance.ssoClientTemplate.getData(path, paramMap); + } + + + // ---------------------- 构建交互地址 ---------------------- + + /** + * 构建URL:Server端 单点登录授权地址, + *
    形如:http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com + * @param clientLoginUrl Client端登录地址 + * @param back 回调路径 + * @return [SSO-Server端-认证地址 ] + */ + public static String buildServerAuthUrl(String clientLoginUrl, String back) { + return SaSsoClientProcessor.instance.ssoClientTemplate.buildServerAuthUrl(clientLoginUrl, back); + } + + + // ------------------- 消息推送 ------------------- + + /** + * 向 sso-server 推送消息 + * + * @param message / + * @return / + */ + public static String pushMessage(SaSsoMessage message) { + return SaSsoClientProcessor.instance.ssoClientTemplate.pushMessage(message); + } + + /** + * 向 sso-server 推送消息,并将返回值转为 SaResult + * + * @param message / + * @return / + */ + public static SaResult pushMessageAsSaResult(SaSsoMessage message) { + return SaSsoClientProcessor.instance.ssoClientTemplate.pushMessageAsSaResult(message); + } + + /** + * 构建消息:校验 ticket + * + * @param ticket ticket码 + * @param ssoLogoutCallUrl 单点注销时的回调URL + * @return 构建完毕的URL + */ + public static SaSsoMessage buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl) { + return SaSsoClientProcessor.instance.ssoClientTemplate.buildCheckTicketMessage(ticket, ssoLogoutCallUrl); + } + + /** + * 构建消息:单点注销 + * + * @param loginId 要注销的账号 id + * @param logoutParameter 单点注销 + * @return 单点注销URL + */ + public static SaSsoMessage buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter) { + return SaSsoClientProcessor.instance.ssoClientTemplate.buildSignoutMessage(loginId, logoutParameter); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java new file mode 100644 index 000000000..99a484fc0 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java @@ -0,0 +1,827 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.template; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.template.SaSignTemplate; +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientModel; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; +import cn.dev33.satoken.sso.error.SaSsoErrorCode; +import cn.dev33.satoken.sso.exception.SaSsoException; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.message.handle.server.SaSsoMessageCheckTicketHandle; +import cn.dev33.satoken.sso.message.handle.server.SaSsoMessageSignoutHandle; +import cn.dev33.satoken.sso.model.SaSsoClientInfo; +import cn.dev33.satoken.sso.model.TicketModel; +import cn.dev33.satoken.sso.strategy.SaSsoServerStrategy; +import cn.dev33.satoken.sso.util.SaSsoConsts; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; + +import java.util.*; + +/** + * SSO 模板方法类 (Server端) + * + * @author click33 + * @since 1.38.0 + */ +public class SaSsoServerTemplate extends SaSsoTemplate { + + /** + * Server 相关策略 + */ + public SaSsoServerStrategy strategy = new SaSsoServerStrategy(); + + public SaSsoServerTemplate() { + super.messageHolder.addHandle(new SaSsoMessageCheckTicketHandle()); + super.messageHolder.addHandle(new SaSsoMessageSignoutHandle()); + } + + // ---------------------- Ticket 操作 ---------------------- + + // 增删改 + + /** + * 保存 Ticket + * @param ticketModel / + */ + public void saveTicket(TicketModel ticketModel) { + long ticketTimeout = getServerConfig().getTicketTimeout(); + SaManager.getSaTokenDao().setObject(splicingTicketModelSaveKey(ticketModel.getTicket()), ticketModel, ticketTimeout); + } + + /** + * 删除 Ticket + * @param ticket Ticket码 + */ + public void deleteTicket(String ticket) { + if(ticket == null) { + return; + } + SaManager.getSaTokenDao().deleteObject(splicingTicketModelSaveKey(ticket)); + } + + /** + * 根据参数创建一个 ticket 码 + * + * @param client 客户端标识 + * @param loginId 账号 id + * @param tokenValue 会话 Token + * @return Ticket码 + */ + public TicketModel createTicket(String client, Object loginId, String tokenValue) { + TicketModel ticketModel = new TicketModel(); + ticketModel.setTicket(randomTicket(loginId)); + ticketModel.setClient(client); + ticketModel.setLoginId(loginId); + ticketModel.setTokenValue(tokenValue); + return ticketModel; + } + + /** + * 根据参数创建一个 ticket 码,并保存 + * + * @param client 客户端标识 + * @param loginId 账号 id + * @param tokenValue 会话 Token + * @return Ticket码 + */ + public String createTicketAndSave(String client, Object loginId, String tokenValue) { + // 创建 + TicketModel ticketModel = createTicket(client, loginId, tokenValue); + + // 保存 + saveTicket(ticketModel); + saveTicketIndex(client, loginId, ticketModel.getTicket()); + + // 返回 + return ticketModel.getTicket(); + } + + /** + * 随机一个 Ticket 码 + * @param loginId 账号id + * @return Ticket 码 + */ + public String randomTicket(Object loginId) { + return SaFoxUtil.getRandomString(64); + } + + // 查 + + /** + * 查询 ticket ,如果 ticket 无效则返回 null + * + * @param ticket Ticket码 + * @return 账号id + */ + public TicketModel getTicket(String ticket) { + if(SaFoxUtil.isEmpty(ticket)) { + return null; + } + return SaManager.getSaTokenDao().getObject(splicingTicketModelSaveKey(ticket), TicketModel.class); + } + + /** + * 查询 ticket 指向的 loginId,如果 ticket 码无效则返回 null + * @param ticket Ticket码 + * @return 账号id + */ + public Object getLoginId(String ticket) { + TicketModel ticketModel = getTicket(ticket); + if(ticketModel == null) { + return null; + } + return ticketModel.getLoginId(); + } + + /** + * 查询 ticket 指向的 loginId,并转换为指定类型 + * @param 要转换的类型 + * @param ticket Ticket码 + * @param cs 要转换的类型 + * @return 账号id + */ + public T getLoginId(String ticket, Class cs) { + return SaFoxUtil.getValueByType(getLoginId(ticket), cs); + } + + // 校验 + + /** + * 校验 Ticket,无效 ticket 会抛出异常 + * + * @param ticket Ticket码 + * @return / + */ + public TicketModel checkTicket(String ticket) { + TicketModel ticketModel = getTicket(ticket); + if(ticketModel == null) { + throw new SaSsoException("无效 ticket : " + ticket).setCode(SaSsoErrorCode.CODE_30004); + } + return ticketModel; + } + + /** + * 校验 Ticket 码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 + * @param ticket Ticket码 + * @return 账号id + */ + public TicketModel checkTicketParamAndDelete(String ticket) { + return checkTicketParamAndDelete(ticket, SaSsoConsts.CLIENT_WILDCARD); + } + + /** + * 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 + * + * @param ticket Ticket码 + * @param client client 标识 + * @return / + */ + public TicketModel checkTicketParamAndDelete(String ticket, String client) { + TicketModel ticketModel = checkTicket(ticket); + + // 校验 client 参数是否正确,即:创建 ticket 的 client 和当前校验 ticket 的 client 是否一致 + String ticketClient = ticketModel.getClient(); + if(SaSsoConsts.CLIENT_WILDCARD.equals(client)) { + // 如果提供的是通配符,直接越过 client 校验 + } else if (SaFoxUtil.isEmpty(client) && SaFoxUtil.isEmpty(ticketClient)) { + // 如果提供的和期望的两者均为空,则通过校验 + } else { + // 开始详细比对 + if(SaFoxUtil.notEquals(client, ticketClient)) { + throw new SaSsoException("该 ticket 不属于 client=" + client + ", ticket 值: " + ticket).setCode(SaSsoErrorCode.CODE_30011); + } + } + + // 删除 ticket 信息,使其只有一次性有效 + deleteTicket(ticket); + deleteTicketIndex(client, ticketModel.getLoginId()); + + // + return ticketModel; + } + + // ticket 索引 + + /** + * 保存 Ticket 索引 (id 反查 ticket) + * + * @param client 应用端 + * @param ticket ticket码 + * @param loginId 账号id + */ + public void saveTicketIndex(String client, Object loginId, String ticket) { + long ticketTimeout = getServerConfig().getTicketTimeout(); + SaManager.getSaTokenDao().set(splicingTicketIndexKey(client, loginId), String.valueOf(ticket), ticketTimeout); + } + + /** + * 删除 Ticket 索引 + * + * @param client 应用标识 + * @param loginId 账号id + */ + public void deleteTicketIndex(String client, Object loginId) { + if(loginId == null) { + return; + } + SaManager.getSaTokenDao().delete(splicingTicketIndexKey(client, loginId)); + } + + /** + * 查询 指定 client、loginId 其所属的 ticket 值 + * + * @param client 应用 + * @param loginId 账号id + * @return Ticket值 + */ + public String getTicketValue(String client, Object loginId) { + if(loginId == null) { + return null; + } + return SaManager.getSaTokenDao().get(splicingTicketIndexKey(client, loginId)); + } + + + // ---------------------- Client 信息获取 ---------------------- + + /** + * 获取所有 Client + * + * @return / + */ + public List getClients() { + return new ArrayList<>(getServerConfig().getClients().values()); + } + + /** + * 获取应用信息,无效 client 返回 null + * + * @param client / + * @return / + */ + public SaSsoClientModel getClient(String client) { + return getServerConfig().getClients().get(client); + } + + /** + * 获取应用信息,无效 client 则抛出异常 + * + * @param client / + * @return / + */ + public SaSsoClientModel getClientNotNull(String client) { + if(SaFoxUtil.isEmpty(client)) { + if(getConfigOfAllowAnonClient()) { + return getAnonClient(); + } else { + throw new SaSsoException("client 标识不可为空"); + } + } else { + SaSsoClientModel scm = getClient(client); + if(scm == null) { + throw new SaSsoException("未能获取应用信息,client=" + client).setCode(SaSsoErrorCode.CODE_30013); + } + return scm; + } + } + + /** + * 获取配置项:是否允许匿名 client 接入 + * + * @return / + */ + public boolean getConfigOfAllowAnonClient() { + return getServerConfig().getAllowAnonClient(); + } + + /** + * 获取匿名 client 配置信息 + * + * @return / + */ + public SaSsoClientModel getAnonClient() { + SaSsoServerConfig serverConfig = getServerConfig(); + SaSsoClientModel scm = new SaSsoClientModel(); + scm.setAllowUrl(serverConfig.getAllowUrl()); + scm.setIsSlo(serverConfig.getIsSlo()); + scm.setSecretKey(serverConfig.getSecretKey()); + if(SaFoxUtil.isEmpty(scm.getSecretKey())) { + scm.setSecretKey(SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().getSecretKey()); + } + return scm; + } + + /** + * 获取所有需要接收消息推送的 Client + * + * @return / + */ + public List getNeedPushClients() { + List list = new ArrayList<>(); + List clients = getClients(); + for(SaSsoClientModel scm : clients) { + if (scm.getIsPush()) { + list.add(scm); + } + } + return list; + } + + + // ------------------- 重定向 URL 构建与校验 ------------------- + + /** + * 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 + * + * @param client 客户端标识 + * @param redirect sso-client 端的重定向地址 + * @param loginId 账号 id + * @param tokenValue 会话 token + * @return / + */ + public String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) { + + // 校验 重定向地址 是否合法 + checkRedirectUrl(client, redirect); + + // 删掉 旧Ticket + deleteTicket(getTicketValue(client, loginId)); + + // 创建 新Ticket + String ticket = createTicketAndSave(client, loginId, tokenValue); + + // 构建 授权重定向地址 (Server端 根据此地址向 Client端 下放 Ticket) + return SaFoxUtil.joinParam(encodeBackParam(redirect), paramName.ticket, ticket); + } + + /** + * 对 url 中的 back 参数进行 URL 编码, 解决超链接重定向后参数丢失的 bug + * + * @param url url + * @return 编码过后的url + */ + public String encodeBackParam(String url) { + + // 获取back参数所在位置 + int index = url.indexOf("?" + paramName.back + "="); + if(index == -1) { + index = url.indexOf("&" + paramName.back + "="); + if(index == -1) { + return url; + } + } + + // 开始编码 + int length = paramName.back.length() + 2; + String back = url.substring(index + length); + back = SaFoxUtil.encodeUrl(back); + + // 放回url中 + url = url.substring(0, index + length) + back; + return url; + } + + /** + * 校验重定向 url 合法性 + * + * @param client 应用标识 + * @param url 下放ticket的url地址 + */ + public void checkRedirectUrl(String client, String url) { + + // 1、是否是一个有效的url + if( ! SaFoxUtil.isUrl(url) ) { + throw new SaSsoException("无效redirect:" + url).setCode(SaSsoErrorCode.CODE_30001); + } + + // 2、截取掉?后面的部分 + int qIndex = url.indexOf("?"); + if(qIndex != -1) { + url = url.substring(0, qIndex); + } + + // 3、不允许出现@字符 + if(url.contains("@")) { + // 为什么不允许出现 @ 字符呢,因为这有可能导致 redirect 参数绕过 AllowUrl 列表的校验 + // + // 举个例子 配置文件: + // sa-token.sso-server.allow-url=http://sa-sso-client1.com* + // + // 开发者原意是为了允许 sa-sso-client1.com 下的所有地址都可以下放ticket + // + // 但是如果攻击者精心构建一个url: + // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com@sa-token.com + // + // 那么这个url就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: + // http://sa-token.com/?ticket=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul + // + // 造成了ticket 参数劫持 + // 所以此处需要禁止在 url 中出现 @ 字符 + // + // 这么一刀切的做法,可能会导致一些特殊的正常url也无法通过校验,例如: + // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com:9003/@getInfo + // + // 但是为了安全起见,这么做还是有必要的 + throw new SaSsoException("无效redirect(不允许出现@字符):" + url).setCode(SaSsoErrorCode.CODE_30001); + } + + // 4、判断是否在 [ 允许的地址列表 ] 之中 + String allowUrlString = getClientNotNull(client).getAllowUrl(); + List allowUrlList = Arrays.asList(allowUrlString.replaceAll(" ", "").split(",")); + checkAllowUrlList(allowUrlList); + if( ! SaStrategy.instance.hasElement.apply(allowUrlList, url) ) { + throw new SaSsoException("非法redirect:" + url).setCode(SaSsoErrorCode.CODE_30002); + } + + // 校验通过 √ + } + + /** + * 校验配置的 AllowUrl 是否合规,如果不合规则抛出异常 + * @param allowUrlList 待校验的 allow-url 地址列表 + */ + public void checkAllowUrlList(List allowUrlList){ + checkAllowUrlListStaticMethod(allowUrlList); + } + + /** + * 校验配置的 AllowUrl 是否合规,如果不合规则抛出异常 + * @param allowUrlList 待校验的 allow-url 地址列表 + */ + public static void checkAllowUrlListStaticMethod(List allowUrlList){ + for (String url : allowUrlList) { + int index = url.indexOf("*"); + // 如果配置了 * 字符,则必须出现在最后一位,否则属于无效配置项 + if(index != -1 && index != url.length() - 1) { + // 为什么不允许 * 字符出现在中间位置呢,因为这有可能导致 redirect 参数绕过 allow-url 列表的校验 + // + // 举个例子 配置文件: + // sa-token.sso-server.allow-url=http://*.sa-sso-client1.com + // + // 开发者原意是为了允许 sa-sso-client1.com 下的所有子域名都可以下放ticket + // 例如:http://shop.sa-sso-client1.com + // + // 但是如果攻击者精心构建一个url: + // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-token.com/a.sa-sso-client1.com/sso/login + // + // 那么这个 url 就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: + // http://sa-token.com/a.sa-sso-client1.com/sso/login?ticket=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx + // + // 造成了 ticket 参数劫持 + // 所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符(出现在末尾是没有问题的) + // + // 这么一刀切的做法,可能会导致正常场景下的子域名url也无法通过校验,例如: + // http://sa-sso-server.com:9000/sso/auth?redirect=http://shop.sa-sso-client1.com/sso/login + // + // 但是为了安全起见,这么做还是有必要的 + throw new SaSsoException("无效的 allow-url 配置(*通配符只允许出现在最后一位):" + url).setCode(SaSsoErrorCode.CODE_30015); + } + } + } + + + // ------------------- 单点注销 ------------------- + + /** + * 为指定账号 id 注册应用接入信息(模式三) + * + * @param loginId 账号id + * @param client 指定客户端标识,可为null + * @param sloCallbackUrl 单点注销时的回调URL + */ + public void registerSloCallbackUrl(Object loginId, String client, String sloCallbackUrl) { + // 如果提供的参数是空值,则直接返回,不进行任何操作 + if(SaFoxUtil.isEmpty(loginId)) { + return; + } + + SaSession session = getStpLogicOrGlobal().getSessionByLoginId(loginId); + + // 取出原来的 + List scmList = session.get(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, ArrayList::new); + + // 将 新登录client 加入到集合中 + SaSsoClientInfo scm = new SaSsoClientInfo(client, sloCallbackUrl, calcNextIndex(scmList)); + scmList.add(scm); + + // 如果登录的client数量超过了限制,则从最早的一个登录开始清退 + int maxRegClient = getServerConfig().maxRegClient; + if(maxRegClient != -1) { + for (;;) { + if(scmList.size() > maxRegClient) { + SaSsoClientInfo removeScm = scmList.remove(0); + strategy.asyncRun.run(() -> { + notifyClientLogout(loginId, null, removeScm, true, true); + }); + } else { + break; + } + } + } + + // 存入持久库 + session.set(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, scmList); + } + + /** + * 计算下一个 index 值 + * @param scmList / + * @return / + */ + public int calcNextIndex(List scmList) { + // 如果目前还没有任何登录记录,则直接返回0 + if(scmList == null || scmList.isEmpty()) { + return 0; + } + // 获取目前最大的index值 + int maxIndex = scmList.get(scmList.size() - 1).index; + + // 如果已经是 int 最大值了,则直接返回0 + if(maxIndex == Integer.MAX_VALUE) { + return 0; + } + + // 否则返回最大值+1 + maxIndex++; + return maxIndex; + } + + /** + * 指定账号单点注销 + * + * @param loginId 指定账号 + */ + public void ssoLogout(Object loginId) { + ssoLogout(loginId, getStpLogicOrGlobal().createSaLogoutParameter(), null); + } + + /** + * 指定账号单点注销 + * + * @param loginId 指定账号 + * @param logoutParameter 注销参数 + * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 + */ + public void ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) { + + // 1、消息推送:单点注销 + pushToAllClientByLogoutCall(loginId, logoutParameter, ignoreClient); + + // 2、SaSession 挂载的 Client 端注销会话 + SaSession session = getStpLogicOrGlobal().getSessionByLoginId(loginId, false); + if(session == null) { + return; + } + List scmList = session.get(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, ArrayList::new); + scmList.forEach(scm -> { + strategy.asyncRun.run(() -> { + notifyClientLogout(loginId, logoutParameter.getDeviceId(), scm, false, false); + }); + }); + + // 3、Server 端本身注销 + getStpLogicOrGlobal().logout(loginId, logoutParameter); + } + + /** + * 通知指定账号的指定客户端注销 + * + * @param loginId 指定账号 + * @param deviceId 指定设备 id + * @param scm 客户端信息对象 + * @param autoLogout 是否为超过 maxRegClient 的自动注销 + * @param isPushWork 如果该 client 没有注册注销回调地址,是否使用 push 消息的方式进行注销回调通知 + * + * @return / + */ + public String notifyClientLogout(Object loginId, String deviceId, SaSsoClientInfo scm, boolean autoLogout, boolean isPushWork) { + + // 如果给个null值,不进行任何操作 + if(scm == null || scm.mode != SaSsoConsts.SSO_MODE_3) { + return null; + } + + // 如果此 Client 并没有注册 单点注销 回调地址 + String sloCallUrl = scm.getSloCallbackUrl(); + if(SaFoxUtil.isEmpty(sloCallUrl)) { + if(isPushWork && SaFoxUtil.isNotEmpty(scm.getClient())) { + SaSsoClientModel client = getClient(scm.getClient()); + return pushToClientByLogoutCall(client, loginId, true, getStpLogicOrGlobal().createSaLogoutParameter()); + } + return null; + } + + // 参数 + Map paramsMap = new TreeMap<>(); + paramsMap.put(paramName.client, scm.getClient()); + paramsMap.put(paramName.loginId, loginId); + paramsMap.put(paramName.deviceId, deviceId); + paramsMap.put(paramName.autoLogout, autoLogout); + String signParamsStr = getSignTemplate(scm.getClient()).addSignParamsAndJoin(paramsMap); + + // 拼接 + String finalUrl = SaFoxUtil.joinParam(sloCallUrl, signParamsStr); + + // 发起请求 + return strategy.sendRequest.apply(finalUrl); + } + + + // ------------------- 消息推送 ------------------- + + /** + * 向指定 Client 推送消息 + * @param clientModel / + * @param message / + * @return / + */ + public String pushMessage(SaSsoClientModel clientModel, SaSsoMessage message) { + message.checkType(); + String noticeUrl = clientModel.splicingPushUrl(); + String paramsStr = getSignTemplate(clientModel.getClient()).addSignParamsAndJoin(message); + String finalUrl = SaFoxUtil.joinParam(noticeUrl, paramsStr); + return strategy.sendRequest.apply(finalUrl); + } + + /** + * 向指定 client 推送消息,并将返回值转为 SaResult + * + * @param clientModel / + * @param message / + * @return / + */ + public SaResult pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message) { + String res = pushMessage(clientModel, message); + Map map = SaManager.getSaJsonTemplate().jsonToMap(res); + return new SaResult(map); + } + + /** + * 向指定 Client 推送消息 + * @param client / + * @param message / + * @return / + */ + public String pushMessage(String client, SaSsoMessage message) { + return pushMessage(getClientNotNull(client), message); + } + + /** + * 向指定 client 推送消息,并将返回值转为 SaResult + * + * @param client / + * @param message / + * @return / + */ + public SaResult pushMessageAsSaResult(String client, SaSsoMessage message) { + String res = pushMessage(client, message); + Map map = SaManager.getSaJsonTemplate().jsonToMap(res); + return new SaResult(map); + } + + /** + * 向所有 Client 推送消息 + * + * @param message / + */ + public void pushToAllClient(SaSsoMessage message) { + pushToAllClient(message, null); + } + + /** + * 向所有 Client 推送消息,并忽略掉某个 client + * + * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 + * @param message / + */ + public void pushToAllClient(SaSsoMessage message, String ignoreClient) { + List needPushClients = getNeedPushClients(); + for (SaSsoClientModel client : needPushClients) { + if(SaFoxUtil.isNotEmpty(ignoreClient) && ignoreClient.equals(client.getClient())) { + continue; + } + strategy.asyncRun.run(() -> pushMessage(client, message)); + } + } + + /** + * 向所有 Client 推送消息:单点注销回调 + * + * @param loginId / + * @param logoutParameter 注销参数 + * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 + */ + public void pushToAllClientByLogoutCall(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) { + List npClients = getNeedPushClients(); + for (SaSsoClientModel client : npClients) { + if(SaFoxUtil.isNotEmpty(ignoreClient) && ignoreClient.equals(client.getClient())) { + continue; + } + if(client.getIsSlo()) { + strategy.asyncRun.run(() -> { + pushToClientByLogoutCall(client, loginId, false, logoutParameter); + }); + } + } + } + + /** + * 向指定 Client 推送消息:单点注销回调 + * + * @param client 应用 + * @param loginId / + * @param autoLogout 是否为超过 maxRegClient 的自动注销 + * @param logoutParameter 注销参数 + * @return / + */ + public String pushToClientByLogoutCall(SaSsoClientModel client, Object loginId, boolean autoLogout, SaLogoutParameter logoutParameter) { + SaSsoMessage message = new SaSsoMessage(); + message.setType(SaSsoConsts.MESSAGE_LOGOUT_CALL); + message.set(paramName.loginId, loginId); + message.set(paramName.autoLogout, autoLogout); + message.set(paramName.deviceId, logoutParameter.getDeviceId()); + return pushMessage(client, message); + } + + + // ------------------- Bean 获取 ------------------- + + /** + * 获取底层使用的SsoServer配置对象 + * @return / + */ + public SaSsoServerConfig getServerConfig() { + return SaSsoManager.getServerConfig(); + } + + /** + * 获取底层使用的 API 签名对象 + * @param client 指定客户端标识,填 null 代表获取默认的 + * @return / + */ + public SaSignTemplate getSignTemplate(String client) { + SaSignConfig signConfig = SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().copy(); + SaSsoClientModel clientModel = getClientNotNull(client); + + // 使用 secretKey 的优先级:client 单独配置 > SSO 模块全局配置 > sign 模块默认配置 + String secretKey = clientModel.getSecretKey(); + if (SaFoxUtil.isEmpty(secretKey) && SaFoxUtil.isNotEmpty(client)) { + secretKey = getServerConfig().getSecretKey(); + } + if(SaFoxUtil.isEmpty(secretKey)) { + secretKey = signConfig.getSecretKey(); + } + signConfig.setSecretKey(secretKey); + + return new SaSignTemplate(signConfig); + } + + + // ------------------- 返回相应key ------------------- + + /** + * 拼接key:TicketModel + * @param ticket ticket值 + * @return key + */ + public String splicingTicketModelSaveKey(String ticket) { + return getStpLogicOrGlobal().getConfigOrGlobal().getTokenName() + ":ticket:" + ticket; + } + + /** + * 拼接key:Ticket 索引 + * + * @param client 应用标识 + * @param id 账号id + * @return key + */ + public String splicingTicketIndexKey(String client, Object id) { + if(SaFoxUtil.isEmpty(client) || SaSsoConsts.CLIENT_WILDCARD.equals(client)) { + client = SaSsoConsts.CLIENT_ANON; + } + return getStpLogicOrGlobal().getConfigOrGlobal().getTokenName() + ":ticket-index:" + client + ":" + id; + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java new file mode 100644 index 000000000..10e2dda48 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java @@ -0,0 +1,311 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.template; + +import cn.dev33.satoken.sso.config.SaSsoClientModel; +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.model.TicketModel; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.stp.parameter.SaLogoutParameter; +import cn.dev33.satoken.util.SaResult; + +import java.util.List; + +/** + * SSO 工具类 (Server端) + * + * @author click33 + * @since 1.43.0 + */ +public class SaSsoServerUtil { + + private SaSsoServerUtil() { + } + + /** + * 返回底层使用的 SaSsoServerTemplate 对象 + * @return / + */ + public static SaSsoServerTemplate getSsoTemplate() { + return SaSsoServerProcessor.instance.ssoServerTemplate; + } + + + // ---------------------- Ticket 操作 ---------------------- + + // 增删改 + + /** + * 删除 Ticket + * @param ticket Ticket码 + */ + public static void deleteTicket(String ticket) { + SaSsoServerProcessor.instance.ssoServerTemplate.deleteTicket(ticket); + } + + /** + * 根据参数创建一个 ticket 码,并保存 + * + * @param client 客户端标识 + * @param loginId 账号 id + * @param tokenValue 会话 Token + * @return Ticket码 + */ + public static String createTicketAndSave(String client, Object loginId, String tokenValue) { + return SaSsoServerProcessor.instance.ssoServerTemplate.createTicketAndSave(client, loginId, tokenValue); + } + + // 查 + + /** + * 查询 ticket ,如果 ticket 无效则返回 null + * + * @param ticket Ticket码 + * @return 账号id + */ + public static TicketModel getTicket(String ticket) { + return SaSsoServerProcessor.instance.ssoServerTemplate.getTicket(ticket); + } + + /** + * 查询 ticket 指向的 loginId,如果 ticket 码无效则返回 null + * @param ticket Ticket码 + * @return 账号id + */ + public static Object getLoginId(String ticket) { + return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket); + } + + /** + * 查询 ticket 指向的 loginId,并转换为指定类型 + * @param 要转换的类型 + * @param ticket Ticket码 + * @param cs 要转换的类型 + * @return 账号id + */ + public static T getLoginId(String ticket, Class cs) { + return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket, cs); + } + + // 校验 + + /** + * 校验 Ticket,无效 ticket 会抛出异常 + * + * @param ticket Ticket码 + * @return / + */ + public static TicketModel checkTicket(String ticket) { + return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicket(ticket); + } + + /** + * 校验 Ticket 码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 + * @param ticket Ticket码 + * @return 账号id + */ + public static TicketModel checkTicketParamAndDelete(String ticket) { + return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket); + } + + /** + * 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 + * + * @param ticket Ticket码 + * @param client client 标识 + * @return / + */ + public static TicketModel checkTicketParamAndDelete(String ticket, String client) { + return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, client); + } + + // ticket 索引 + + /** + * 查询 指定 client、loginId 其所属的 ticket 值 + * + * @param client 应用 + * @param loginId 账号id + * @return Ticket值 + */ + public static String getTicketValue(String client, Object loginId) { + return SaSsoServerProcessor.instance.ssoServerTemplate.getTicketValue(client, loginId); + } + + + // ---------------------- Client 信息获取 ---------------------- + + /** + * 获取所有 Client + * + * @return / + */ + public static List getClients() { + return SaSsoServerProcessor.instance.ssoServerTemplate.getClients(); + } + + /** + * 获取应用信息,无效 client 返回 null + * + * @param client / + * @return / + */ + public static SaSsoClientModel getClient(String client) { + return SaSsoServerProcessor.instance.ssoServerTemplate.getClient(client); + } + + /** + * 获取应用信息,无效 client 则抛出异常 + * + * @param client / + * @return / + */ + public static SaSsoClientModel getClientNotNull(String client) { + return SaSsoServerProcessor.instance.ssoServerTemplate.getClientNotNull(client); + } + + /** + * 获取匿名 client 信息 + * + * @return / + */ + public static SaSsoClientModel getAnonClient() { + return SaSsoServerProcessor.instance.ssoServerTemplate.getAnonClient(); + } + + /** + * 获取所有需要接收消息推送的 Client + * + * @return / + */ + public static List getNeedPushClients() { + return SaSsoServerProcessor.instance.ssoServerTemplate.getNeedPushClients(); + } + + + // ------------------- 重定向 URL 构建与校验 ------------------- + + /** + * 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 + * + * @param client 客户端标识 + * @param redirect sso-client 端的重定向地址 + * @param loginId 账号 id + * @param tokenValue 会话 token + * @return / + */ + public static String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) { + return SaSsoServerProcessor.instance.ssoServerTemplate.buildRedirectUrl(client, redirect, loginId, tokenValue); + } + + /** + * 校验重定向 url 合法性 + * + * @param client 应用标识 + * @param url 下放ticket的url地址 + */ + public static void checkRedirectUrl(String client, String url) { + SaSsoServerProcessor.instance.ssoServerTemplate.checkRedirectUrl(client, url); + } + + + // ------------------- 单点注销 ------------------- + + /** + * 指定账号单点注销 + * + * @param loginId 指定账号 + */ + public static void ssoLogout(Object loginId) { + SaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId); + } + + /** + * 指定账号单点注销 + * + * @param loginId 指定账号 + * @param logoutParameter 注销参数 + * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 + */ + public static void ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) { + SaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId, logoutParameter, ignoreClient); + } + + + // ------------------- 消息推送 ------------------- + + /** + * 向指定 Client 推送消息 + * @param clientModel / + * @param message / + * @return / + */ + public static String pushMessage(SaSsoClientModel clientModel, SaSsoMessage message) { + return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessage(clientModel, message); + } + + /** + * 向指定 client 推送消息,并将返回值转为 SaResult + * + * @param clientModel / + * @param message / + * @return / + */ + public static SaResult pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message) { + return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessageAsSaResult(clientModel, message); + } + + /** + * 向指定 Client 推送消息 + * @param client / + * @param message / + * @return / + */ + public static String pushMessage(String client, SaSsoMessage message) { + return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessage(client, message); + } + + /** + * 向指定 client 推送消息,并将返回值转为 SaResult + * + * @param client / + * @param message / + * @return / + */ + public static SaResult pushMessageAsSaResult(String client, SaSsoMessage message) { + return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessageAsSaResult(client, message); + } + + /** + * 向所有 Client 推送消息 + * + * @param message / + */ + public static void pushToAllClient(SaSsoMessage message) { + SaSsoServerProcessor.instance.ssoServerTemplate.pushToAllClient(message); + } + + /** + * 向所有 Client 推送消息,并忽略掉某个 client + * + * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 + * @param message / + */ + public static void pushToAllClient(SaSsoMessage message, String ignoreClient) { + SaSsoServerProcessor.instance.ssoServerTemplate.pushToAllClient(message, ignoreClient); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoTemplate.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoTemplate.java new file mode 100644 index 000000000..0ea977656 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoTemplate.java @@ -0,0 +1,115 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.template; + +import cn.dev33.satoken.sso.message.SaSsoMessage; +import cn.dev33.satoken.sso.message.SaSsoMessageHolder; +import cn.dev33.satoken.sso.name.ApiName; +import cn.dev33.satoken.sso.name.ParamName; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; + +/** + * SSO 模板方法类 (公共端) + * + * @author click33 + * @since 1.30.0 + */ +public class SaSsoTemplate { + + // ---------------------- 全局配置 ---------------------- + + /** + * 所有 API 名称 + */ + public ApiName apiName = new ApiName(); + + /** + * 所有参数名称 + */ + public ParamName paramName = new ParamName(); + + /** + * @param paramName 替换 paramName 对象 + * @return 对象自身 + */ + public SaSsoTemplate setParamName(ParamName paramName) { + this.paramName = paramName; + return this; + } + + /** + * @param apiName 替换 apiName 对象 + * @return 对象自身 + */ + public SaSsoTemplate setApiName(ApiName apiName) { + this.apiName = apiName; + return this; + } + + /** + * 底层使用的 StpLogic 对象 + */ + StpLogic stpLogic; + + /** + * 写入底层使用的会话对象 + * + * @param stpLogic / + * @return / + */ + public SaSsoTemplate setStpLogic(StpLogic stpLogic) { + this.stpLogic = stpLogic; + return this; + } + + /** + * 获取底层使用的会话对象 + * @return / + */ + public StpLogic getStpLogic() { + return this.stpLogic; + } + + /** + * 获取底层使用的会话对象,如果没有配置则返回全局默认 StpLogic + * @return / + */ + public StpLogic getStpLogicOrGlobal() { + StpLogic stpLogic = getStpLogic(); + if (stpLogic == null) { + return StpUtil.stpLogic; + } + return stpLogic; + } + + // ----------- 消息处理 + + /** + * SSO 消息处理器 - 持有器 + */ + public SaSsoMessageHolder messageHolder = new SaSsoMessageHolder(); + + /** + * 处理指定消息 + * + * @param message / + */ + public Object handleMessage(SaSsoMessage message) { + return messageHolder.handleMessage(this, message); + } + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoUtil.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoUtil.java new file mode 100644 index 000000000..88365f507 --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoUtil.java @@ -0,0 +1,200 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.template; + +import cn.dev33.satoken.sso.model.TicketModel; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; + +import java.util.Map; + +/** + * Sa-Token-SSO 单点登录模块 工具类 + * + *

    请更换为 SaSsoServerUtil 或 SaSsoClientUtil

    + * + * @author click33 + * @since 1.30.0 + */ +@Deprecated +public class SaSsoUtil { + + // ---------------------- Ticket 操作 ---------------------- + + /** + * 根据参数创建一个 ticket 码 + * + * @param client 客户端标识 + * @param loginId 账号 id + * @param deviceId 设备 id + * @return Ticket码 + */ + public static String createTicket(String client, Object loginId, String deviceId) { + return SaSsoServerProcessor.instance.ssoServerTemplate.createTicketAndSave(client, loginId, deviceId); + } + + /** + * 删除 Ticket + * @param ticket Ticket码 + */ + public static void deleteTicket(String ticket) { + SaSsoServerProcessor.instance.ssoServerTemplate.deleteTicket(ticket); + } + + /** + * 删除 Ticket索引 + * @param client 应用 id + * @param loginId 账号id + */ + public static void deleteTicketIndex(String client, Object loginId) { + SaSsoServerProcessor.instance.ssoServerTemplate.deleteTicketIndex(client, loginId); + } + + /** + * 根据 Ticket码 获取账号id,如果Ticket码无效则返回null + * @param ticket Ticket码 + * @return 账号id + */ + public static Object getLoginId(String ticket) { + return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket); + } + + /** + * 根据 Ticket码 获取账号id,并转换为指定类型 + * @param 要转换的类型 + * @param ticket Ticket码 + * @param cs 要转换的类型 + * @return 账号id + */ + public static T getLoginId(String ticket, Class cs) { + return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket, cs); + } + + /** + * 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 + * @param ticket Ticket码 + * @return 账号id + */ + public static TicketModel checkTicket(String ticket) { + return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket); + } + + /** + * 校验ticket码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 + * @param ticket Ticket码 + * @param client client 标识 + * @return 账号id + */ + public static TicketModel checkTicket(String ticket, String client) { + return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, client); + } + + /** + * 校验重定向url合法性 + * + * @param client 应用标识 + * @param url 下放ticket的url地址 + */ + public static void checkRedirectUrl(String client, String url) { + SaSsoServerProcessor.instance.ssoServerTemplate.checkRedirectUrl(client, url); + } + + + // ------------------- SSO 模式三 ------------------- + + /** + * 为指定账号id注册单点注销回调URL + * @param loginId 账号id + * @param client 指定客户端标识,可为null + * @param sloCallbackUrl 单点注销时的回调URL + */ + public static void registerSloCallbackUrl(Object loginId, String client, String sloCallbackUrl) { + SaSsoServerProcessor.instance.ssoServerTemplate.registerSloCallbackUrl(loginId, client, sloCallbackUrl); + } + + /** + * 指定账号单点注销 (以Server方发起) + * @param loginId 指定账号 + */ + public static void ssoLogout(Object loginId) { + SaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId); + } + + /** + * 获取:查询数据 + * @param paramMap 查询参数 + * @return 查询结果 + */ + public static Object getData(Map paramMap) { + return SaSsoClientProcessor.instance.ssoClientTemplate.getData(paramMap); + } + + /** + * 根据自定义 path 查询数据 (此方法需要配置 sa-token.sso.server-url 地址) + * @param path 自定义 path + * @param paramMap 查询参数 + * @return 查询结果 + */ + public static Object getData(String path, Map paramMap) { + return SaSsoClientProcessor.instance.ssoClientTemplate.getData(path, paramMap); + } + + + // ---------------------- 构建URL ---------------------- + + /** + * 构建URL:Server端 单点登录地址 + * @param clientLoginUrl Client端登录地址 + * @param back 回调路径 + * @return [SSO-Server端-认证地址 ] + */ + public static String buildServerAuthUrl(String clientLoginUrl, String back) { + return SaSsoClientProcessor.instance.ssoClientTemplate.buildServerAuthUrl(clientLoginUrl, back); + } + + /** + * 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 + * + * @param client 客户端标识 + * @param redirect sso-client 端的重定向地址 + * @param loginId 账号 id + * @param tokenValue 会话 token + * @return / + */ + public static String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) { + return SaSsoServerProcessor.instance.ssoServerTemplate.buildRedirectUrl(client, redirect, loginId, tokenValue); + } + + /** + * 构建URL:Server端 getData 地址,带签名等参数 + * @param paramMap 查询参数 + * @return / + */ + public static String buildGetDataUrl(Map paramMap) { + return SaSsoClientProcessor.instance.ssoClientTemplate.buildGetDataUrl(paramMap); + } + + /** + * 构建URL:Server 端自定义 path 地址,带签名等参数 (此方法需要配置 sa-token.sso.server-url 地址) + * @param paramMap 请求参数 + * @return / + */ + public static String buildCustomPathUrl(String path, Map paramMap) { + return SaSsoClientProcessor.instance.ssoClientTemplate.buildCustomPathUrl(path, paramMap); + } + + +} diff --git a/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/util/SaSsoConsts.java b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/util/SaSsoConsts.java new file mode 100644 index 000000000..5cbcf1c8f --- /dev/null +++ b/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/util/SaSsoConsts.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.sso.util; + +/** + * Sa-Token-SSO模块相关常量 + * + * @author click33 + * @since 1.30.0 + */ +public class SaSsoConsts { + + /** Client端单点注销回调URL的Set集合,存储在Session中使用的key */ + @Deprecated + public static final String SLO_CALLBACK_SET_KEY = "SLO_CALLBACK_SET_KEY_"; + + /** Client 端 Model 信息的 List 集合,存储在 SaSession 中使用的key */ + public static final String SSO_CLIENT_MODEL_LIST_KEY_ = "SSO_CLIENT_MODEL_LIST_KEY_"; + + /** 表示OK的返回结果 */ + public static final String OK = "ok"; + + /** 表示自己 */ + public static final String SELF = "self"; + + /** 表示简单模式(SSO模式一) */ + public static final String MODE_SIMPLE = "simple"; + + /** 表示ticket模式(SSO模式二和模式三) */ + public static final String MODE_TICKET = "ticket"; + + /** 表示请求没有得到任何有效处理 {msg: "not handle"} */ + public static final String NOT_HANDLE = "{\"msg\": \"not handle\"}"; + + /** client 身份,* 代表通配,可以解析出所有 client 的 ticket */ + public static final String CLIENT_WILDCARD = "*"; + + /** client 身份,代表匿名 client */ + public static final String CLIENT_ANON = "anon"; + + /** SSO 模式1 */ + public static final int SSO_MODE_1 = 1; + /** SSO 模式2 */ + public static final int SSO_MODE_2 = 2; + /** SSO 模式3 */ + public static final int SSO_MODE_3 = 3; + + /** 消息类型:校验 ticket */ + public static final String MESSAGE_CHECK_TICKET = "checkTicket"; + + /** 消息类型:单点注销 */ + public static final String MESSAGE_SIGNOUT = "signout"; + + /** 消息类型:单点注销回调 */ + public static final String MESSAGE_LOGOUT_CALL = "logoutCall"; + + + +} diff --git a/sa-token-plugin/sa-token-temp-jwt/.gitignore b/sa-token-plugin/sa-token-temp-jwt/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-plugin/sa-token-temp-jwt/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-plugin/sa-token-temp-jwt/pom.xml b/sa-token-plugin/sa-token-temp-jwt/pom.xml index 4185d4d02..80ed45323 100644 --- a/sa-token-plugin/sa-token-temp-jwt/pom.xml +++ b/sa-token-plugin/sa-token-temp-jwt/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-plugin - 1.24.0 + ${revision} + ../pom.xml jar @@ -20,21 +21,18 @@ cn.dev33 sa-token-core - ${sa-token-version} - + - io.jsonwebtoken - jjwt - 0.9.1 + io.jsonwebtoken + jjwt + + + + javax.xml.bind + jaxb-api + 2.3.1 - - - org.springframework.boot - spring-boot-configuration-processor - 2.0.0.RELEASE - true - diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java new file mode 100644 index 000000000..432be5d4e --- /dev/null +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.plugin; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.temp.jwt.SaTempTemplateForJwt; + +/** + * SaToken 插件安装:临时 token 生成器 - Jwt 版 + * + * @author click33 + * @since 1.41.0 + */ +public class SaTokenPluginForTempForJwt implements SaTokenPlugin { + + @Override + public void install() { + SaManager.setSaTempTemplate(new SaTempTemplateForJwt()); + } + +} \ No newline at end of file diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java index 3c4c02975..0d0780797 100644 --- a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java @@ -1,23 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.temp.jwt; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.secure.SaSecureUtil; +import cn.dev33.satoken.temp.jwt.error.SaTempJwtErrorCode; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +import javax.crypto.SecretKey; /** - * jwt操作工具类 - * @author kong + * jwt 相关操作工具类,封装一下 * + * @author click33 + * @since 1.20.0 */ public class SaJwtUtil { /** - * key: value + * key: value 前缀 */ - public static final String KEY_VALUE = "value"; + public static final String KEY_VALUE = "value_"; /** * key: 有效期 (时间戳) @@ -28,24 +48,30 @@ public class SaJwtUtil { public static final long NEVER_EXPIRE = SaTokenDao.NEVER_EXPIRE; /** - * 根据指定值创建 jwt-token + * 根据指定值创建 jwt-token + * * @param value 要保存的值 * @param timeout token有效期 (单位 秒) * @param keyt 秘钥 * @return jwt-token */ public static String createToken(Object value, long timeout, String keyt) { - // 计算eff有效期 + // 计算eff有效期: + // 如果 timeout 指定为 -1,那么 eff 也为 -1,代表永不过期 + // 如果 timeout 指定为一个具体的值,那么 eff 为 13 位时间戳,代表此数据到期的时间 long eff = timeout; if(timeout != NEVER_EXPIRE) { eff = timeout * 1000 + System.currentTimeMillis(); } - // 在这里你可以使用官方提供的claim方法构建载荷,也可以使用setPayload自定义载荷,但是两者不可一起使用 - JwtBuilder builder = Jwts.builder() - // .setHeaderParam("typ", "JWT") + + // 在这里你可以使用官方提供的claim方法构建载荷,也可以使用setPayload自定义载荷,但是两者不可一起使用 + SecretKey key = Keys.hmacShaKeyFor(SaSecureUtil.md5(keyt).getBytes()); + JwtBuilder builder = Jwts.builder() + .header().add("typ", "JWT").and() .claim(KEY_VALUE, value) .claim(KEY_EFF, eff) - .signWith(SignatureAlgorithm.HS256, keyt.getBytes()); + .signWith(key); + // 生成jwt-token return builder.compact(); } @@ -57,16 +83,16 @@ public static String createToken(Object value, long timeout, String keyt) { * @return Claims对象 */ public static Claims parseToken(String jwtToken, String keyt) { - // 解析出载荷 - Claims claims = Jwts.parser() - .setSigningKey(keyt.getBytes()) - .parseClaimsJws(jwtToken).getBody(); - // 返回 - return claims; + // 解析出载荷 + SecretKey key = Keys.hmacShaKeyFor(SaSecureUtil.md5(keyt).getBytes()); + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(jwtToken).getPayload(); } /** - * 从一个 jwt-token 解析出载荷, 并取出数据 + * 从一个 jwt-token 解析出载荷, 并取出数据 * @param jwtToken JwtToken值 * @param keyt 秘钥 * @return 值 @@ -77,8 +103,8 @@ public static Object getValue(String jwtToken, String keyt) { // 验证是否超时 Long eff = claims.get(KEY_EFF, Long.class); - if((eff == null || eff < System.currentTimeMillis()) && eff != NEVER_EXPIRE) { - throw new SaTokenException("Token已超时"); + if(eff == null || (eff < System.currentTimeMillis() && eff != NEVER_EXPIRE)) { + throw new SaTokenException("token 已超时,无法解析:" + jwtToken).setCode(SaTempJwtErrorCode.CODE_30303); } // 获取数据 @@ -86,7 +112,7 @@ public static Object getValue(String jwtToken, String keyt) { } /** - * 从一个 jwt-token 解析出载荷, 并取出其剩余有效期 + * 从一个 jwt-token 解析出载荷, 并取出其剩余有效期 * @param jwtToken JwtToken值 * @param keyt 秘钥 * @return 值 @@ -94,7 +120,7 @@ public static Object getValue(String jwtToken, String keyt) { public static long getTimeout(String jwtToken, String keyt) { // 取出数据 Claims claims = parseToken(jwtToken, keyt); - + // 验证是否超时 Long eff = claims.get(KEY_EFF, Long.class); @@ -103,7 +129,7 @@ public static long getTimeout(String jwtToken, String keyt) { return NEVER_EXPIRE; } // 已经超时 - if(eff == null || eff < System.currentTimeMillis()) { + if(eff < System.currentTimeMillis()) { return SaTokenDao.NOT_VALUE_EXPIRE; } diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempForJwt.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempForJwt.java deleted file mode 100644 index 60c157fc1..000000000 --- a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempForJwt.java +++ /dev/null @@ -1,51 +0,0 @@ -package cn.dev33.satoken.temp.jwt; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.temp.SaTempInterface; -import cn.dev33.satoken.util.SaFoxUtil; - -/** - * Sa-Token 临时令牌验证模块接口 JWT实现类 - * @author kong - * - */ -public class SaTempForJwt implements SaTempInterface { - - /** - * 根据value创建一个token - */ - public String createToken(Object value, long timeout) { - String token = SaJwtUtil.createToken(value, timeout, getJwtSecretKey()); - return token; - } - - /** - * 解析token获取value - */ - public Object parseToken(String token) { - Object value = SaJwtUtil.getValue(token, getJwtSecretKey()); - return value; - } - - /** - * 返回指定token的剩余有效期,单位:秒 - */ - public long getTimeout(String token) { - long timeout = SaJwtUtil.getTimeout(token, getJwtSecretKey()); - return timeout; - } - - /** - * 获取jwt秘钥 - * @return jwt秘钥 - */ - public String getJwtSecretKey() { - String jwtSecretKey = SaManager.getConfig().getJwtSecretKey(); - if(SaFoxUtil.isEmpty(jwtSecretKey)) { - throw new SaTokenException("请配置:jwtSecretKey"); - } - return jwtSecretKey; - } - -} diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempTemplateForJwt.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempTemplateForJwt.java new file mode 100644 index 000000000..dd729189b --- /dev/null +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempTemplateForJwt.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.temp.jwt; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.exception.ApiDisabledException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.temp.SaTempTemplate; +import cn.dev33.satoken.temp.jwt.error.SaTempJwtErrorCode; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.List; + +/** + * Sa-Token 临时令牌验证模块接口 JWT实现类,提供以 JWT 为逻辑内核的临时 token 验证功能 + * + * @author click33 + * @since 1.20.0 + */ +public class SaTempTemplateForJwt extends SaTempTemplate { + + /** + * 根据value创建一个token + */ + @Override + public String createToken(Object value, long timeout, boolean isRecordIndex) { + return SaJwtUtil.createToken(value, timeout, getJwtSecretKey()); + } + + /** + * 解析token获取value + */ + @Override + public Object parseToken(String token) { + return SaJwtUtil.getValue(token, getJwtSecretKey()); + } + + /** + * 返回指定token的剩余有效期,单位:秒 + */ + @Override + public long getTimeout(String token) { + return SaJwtUtil.getTimeout(token, getJwtSecretKey()); + } + + /** + * 删除一个token + */ + @Override + public void deleteToken(String token) { + throw new ApiDisabledException("jwt cannot delete token").setCode(SaTempJwtErrorCode.CODE_30302); + } + + /** + * 获取指定 value 的 temp-token 列表记录 + * @param value / + * @return / + */ + public List getTempTokenList(Object value) { + throw new ApiDisabledException("jwt cannot get token list").setCode(SaTempJwtErrorCode.CODE_30304); + } + + /** + * 获取jwt秘钥 + * @return jwt秘钥 + */ + @Override + public String getJwtSecretKey() { + String jwtSecretKey = SaManager.getConfig().getJwtSecretKey(); + if(SaFoxUtil.isEmpty(jwtSecretKey)) { + throw new SaTokenException("请配置:jwtSecretKey").setCode(SaTempJwtErrorCode.CODE_30301); + } + return jwtSecretKey; + } + +} diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java new file mode 100644 index 000000000..4512e79d7 --- /dev/null +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.temp.jwt.error; + +/** + * 定义 sa-token-temp-jwt 所有异常细分状态码 + * + * @author click33 + * @since 1.33.0 + */ +public interface SaTempJwtErrorCode { + + /** jwt 模式没有提供秘钥 */ + int CODE_30301 = 30301; + + /** jwt 模式不可以删除 Token */ + int CODE_30302 = 30302; + + /** Token已超时 */ + int CODE_30303 = 30303; + + /** jwt 模式不可以查询旧 Token 列表 */ + int CODE_30304 = 30304; + +} diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin b/sa-token-plugin/sa-token-temp-jwt/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin new file mode 100644 index 000000000..195a264ea --- /dev/null +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin @@ -0,0 +1 @@ +cn.dev33.satoken.plugin.SaTokenPluginForTempForJwt \ No newline at end of file diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/resources/META-INF/spring.factories b/sa-token-plugin/sa-token-temp-jwt/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 0c16a1f25..000000000 --- a/sa-token-plugin/sa-token-temp-jwt/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.temp.jwt.SaTempForJwt \ No newline at end of file diff --git a/sa-token-plugin/sa-token-thymeleaf/pom.xml b/sa-token-plugin/sa-token-thymeleaf/pom.xml new file mode 100644 index 000000000..e8db224d2 --- /dev/null +++ b/sa-token-plugin/sa-token-thymeleaf/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-plugin + ${revision} + ../pom.xml + + jar + + sa-token-thymeleaf + sa-token-thymeleaf + sa-token-thymeleaf + + + + + cn.dev33 + sa-token-core + + + + org.thymeleaf + thymeleaf + true + + + + org.springframework.boot + spring-boot-configuration-processor + 2.5.14 + true + + + + diff --git a/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/Sa-Token-Dialect.xml b/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/Sa-Token-Dialect.xml new file mode 100644 index 000000000..92b05bb0c --- /dev/null +++ b/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/Sa-Token-Dialect.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenDialect.java b/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenDialect.java new file mode 100644 index 000000000..f0da7f6cf --- /dev/null +++ b/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenDialect.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.thymeleaf.dialect; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.thymeleaf.dialect.AbstractProcessorDialect; +import org.thymeleaf.processor.IProcessor; + +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import org.thymeleaf.standard.processor.StandardXmlNsTagProcessor; +import org.thymeleaf.templatemode.TemplateMode; + +/** + * Sa-Token 集成 Thymeleaf 标签方言 + * + * @author click33 + * @since 1.27.0 + */ +public class SaTokenDialect extends AbstractProcessorDialect { + + /** + * 底层使用的 StpLogic + */ + public StpLogic stpLogic; + + /** + * 使用默认参数注册方言 + */ + public SaTokenDialect() { + this("sa", 1000, StpUtil.stpLogic); + } + + /** + * 构造方言对象,使用自定义参数 + * + * @param name 方言名称 + * @param precedence 优先级 + * @param stpLogic 使用的 StpLogic 对象 + */ + public SaTokenDialect(String name, int precedence, StpLogic stpLogic) { + // 名称、前缀、优先级 + super(name, name, precedence); + this.stpLogic = stpLogic; + } + + /** + * 返回所有方言处理器 + */ + @Override + public Set getProcessors(String prefix) { + return new HashSet<>(Arrays.asList( + // 登录判断 + new SaTokenTagProcessor(prefix, "login", value -> stpLogic.isLogin()), + new SaTokenTagProcessor(prefix, "notLogin", value -> ! stpLogic.isLogin()), + + // 角色判断 + new SaTokenTagProcessor(prefix, "hasRole", value -> stpLogic.hasRole(value)), + new SaTokenTagProcessor(prefix, "hasRoleAnd", value -> stpLogic.hasRoleAnd(toArray(value))), + new SaTokenTagProcessor(prefix, "hasRoleOr", value -> stpLogic.hasRoleOr(toArray(value))), + new SaTokenTagProcessor(prefix, "notRole", value -> ! stpLogic.hasRole(value)), + new SaTokenTagProcessor(prefix, "lackRole", value -> ! stpLogic.hasRole(value)), + + // 权限判断 + new SaTokenTagProcessor(prefix, "hasPermission", value -> stpLogic.hasPermission(value)), + new SaTokenTagProcessor(prefix, "hasPermissionAnd", value -> stpLogic.hasPermissionAnd(toArray(value))), + new SaTokenTagProcessor(prefix, "hasPermissionOr", value -> stpLogic.hasPermissionOr(toArray(value))), + new SaTokenTagProcessor(prefix, "notPermission", value -> ! stpLogic.hasPermission(value)), + new SaTokenTagProcessor(prefix, "lackPermission", value -> ! stpLogic.hasPermission(value)), + + // 移除标签命名空间 + new StandardXmlNsTagProcessor(TemplateMode.HTML,prefix) + + )); + } + + /** + * String 转 Array + * @param str 字符串 + * @return 数组 + */ + public String[] toArray(String str) { + List list = SaFoxUtil.convertStringToList(str); + return list.toArray(new String[0]); + } + +} diff --git a/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenTagProcessor.java b/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenTagProcessor.java new file mode 100644 index 000000000..768e74312 --- /dev/null +++ b/sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenTagProcessor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.thymeleaf.dialect; + +import java.util.function.Function; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.templatemode.TemplateMode; + +/** + * 封装 Sa-Token 标签方言处理器 + * + * @author click33 + * @since 1.27.0 + */ +public class SaTokenTagProcessor extends AbstractAttributeTagProcessor { + + Function fun; + + public SaTokenTagProcessor(final String dialectPrefix, String attrName, Function fun) { + super( + TemplateMode.HTML, // This processor will apply only to HTML mode + dialectPrefix, // Prefix to be applied to name for matching + null, // No tag name: match any tag name + false, // No prefix to be applied to tag name + attrName, // Name of the attribute that will be matched + true, // Apply dialect prefix to attribute name + 10000, // Precedence (inside dialect's own precedence) + true); // Remove the matched attribute afterwards + this.fun = fun; + } + + @Override + protected void doProcess( + final ITemplateContext context, final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IElementTagStructureHandler structureHandler) { + // 执行表达式返回值为false,则删除这个标签 + if( ! this.fun.apply(attributeValue)) { + structureHandler.removeElement(); + } + } + +} \ No newline at end of file diff --git a/sa-token-special-dependencies/pom.xml b/sa-token-special-dependencies/pom.xml new file mode 100644 index 000000000..66561f14e --- /dev/null +++ b/sa-token-special-dependencies/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-parent + ${revision} + ../pom.xml + + + pom + sa-token-special-dependencies + sa-token-special-dependencies + Sa-Token Special Dependencies + + + sa-token-spring-boot2-dependencies + sa-token-spring-boot3-dependencies + sa-token-spring-boot4-dependencies + + + diff --git a/sa-token-special-dependencies/sa-token-spring-boot2-dependencies/pom.xml b/sa-token-special-dependencies/sa-token-spring-boot2-dependencies/pom.xml new file mode 100644 index 000000000..c58869f5e --- /dev/null +++ b/sa-token-special-dependencies/sa-token-spring-boot2-dependencies/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + + + cn.dev33 + sa-token-special-dependencies + ${revision} + ../pom.xml + + pom + + sa-token-spring-boot2-dependencies + sa-token-spring-boot2-dependencies + Sa-Token SpringBoot2 Dependencies + + + 2.7.18 + 5.3.39 + 3.7.4 + + + + + + + + org.springframework.boot + spring-boot-starter + ${springboot2.version} + + + + + org.springframework.boot + spring-boot-starter-web + ${springboot2.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + ${springboot2.version} + + + + org.springframework + spring-web + ${springboot2-spring.version} + + + + org.springframework + spring-webmvc + ${springboot2-spring.version} + + + + + io.projectreactor + reactor-core + ${reactor-core.version} + + + + + + + + org.springframework.boot + spring-boot-starter-data-redis + ${springboot2.version} + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + ${springboot2.version} + + + + + org.springframework.boot + spring-boot-starter-aop + ${springboot2.version} + + + + + org.springframework.boot + spring-boot-starter-actuator + ${springboot2.version} + + + + + org.springframework.boot + spring-boot-starter-test + ${springboot2.version} + + + + + + \ No newline at end of file diff --git a/sa-token-special-dependencies/sa-token-spring-boot3-dependencies/pom.xml b/sa-token-special-dependencies/sa-token-spring-boot3-dependencies/pom.xml new file mode 100644 index 000000000..c6dbebbf3 --- /dev/null +++ b/sa-token-special-dependencies/sa-token-spring-boot3-dependencies/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + + + cn.dev33 + sa-token-special-dependencies + ${revision} + ../pom.xml + + pom + + sa-token-spring-boot3-dependencies + sa-token-spring-boot3-dependencies + Sa-Token SpringBoot3 Dependencies + + + 3.5.11 + 6.2.16 + 3.7.4 + + + + + + + + org.springframework.boot + spring-boot-starter + ${springboot3.version} + + + + + org.springframework.boot + spring-boot-starter-web + ${springboot3.version} + + + + + org.springframework + spring-web + ${springboot3-spring.version} + + + + + io.projectreactor + reactor-core + ${reactor-core.version} + + + + + org.springframework.boot + spring-boot-configuration-processor + ${springboot3.version} + + + + + + \ No newline at end of file diff --git a/sa-token-special-dependencies/sa-token-spring-boot4-dependencies/pom.xml b/sa-token-special-dependencies/sa-token-spring-boot4-dependencies/pom.xml new file mode 100644 index 000000000..de9e494a4 --- /dev/null +++ b/sa-token-special-dependencies/sa-token-spring-boot4-dependencies/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + + cn.dev33 + sa-token-special-dependencies + ${revision} + ../pom.xml + + pom + + sa-token-spring-boot4-dependencies + sa-token-spring-boot4-dependencies + Sa-Token SpringBoot4 Dependencies + + + 4.0.3 + 7.0.3 + + + + + + + + org.springframework.boot + spring-boot-starter + ${springboot4.version} + + + + org.springframework.boot + spring-boot-starter-webmvc + ${springboot4.version} + + + + org.springframework + spring-web + ${springboot4-spring.version} + + + + org.springframework.boot + spring-boot-starter-webflux + ${springboot4.version} + + + + org.springframework.boot + spring-boot-configuration-processor + ${springboot4.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + ${springboot4.version} + + + + + org.apache.commons + commons-pool2 + 2.12.1 + true + + + + + + \ No newline at end of file diff --git a/sa-token-starter/.gitignore b/sa-token-starter/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-starter/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-starter/pom.xml b/sa-token-starter/pom.xml index d24c598c5..56e49dea4 100644 --- a/sa-token-starter/pom.xml +++ b/sa-token-starter/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-parent - 1.24.0 + ${revision} + ../pom.xml pom @@ -18,9 +19,20 @@ sa-token-servlet + sa-token-jakarta-servlet + sa-token-spring-boot-webmvc-reactor-v2v3v4-common + sa-token-spring-boot-reactor-v3v4-common sa-token-spring-boot-starter + sa-token-spring-boot-webmvc-v3v4-common + sa-token-spring-boot3-starter + sa-token-spring-boot4-starter sa-token-reactor-spring-boot-starter + sa-token-reactor-spring-boot3-starter + sa-token-reactor-spring-boot4-starter sa-token-solon-plugin + sa-token-jboot-plugin + sa-token-jfinal-plugin + sa-token-loveqq-boot-starter \ No newline at end of file diff --git a/sa-token-starter/sa-token-jakarta-servlet/pom.xml b/sa-token-starter/sa-token-jakarta-servlet/pom.xml new file mode 100644 index 000000000..8950b889c --- /dev/null +++ b/sa-token-starter/sa-token-jakarta-servlet/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + jar + + sa-token-jakarta-servlet + sa-token-jakarta-servlet + sa-token authentication by Jakarta Servlet API + + + + + cn.dev33 + sa-token-core + + + + + jakarta.servlet + jakarta.servlet-api + + + + + + diff --git a/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java new file mode 100644 index 000000000..7cc17ee88 --- /dev/null +++ b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.error; + +/** + * 定义 sa-token-servlet 所有异常细分状态码 + * + * @author click33 + * @since 1.34.0 + */ +public interface SaServletErrorCode { + + /** 转发失败 */ + int CODE_20001 = 20001; + + /** 重定向失败 */ + int CODE_20002 = 20002; + +} diff --git a/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java new file mode 100644 index 000000000..5addd61d6 --- /dev/null +++ b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java @@ -0,0 +1,199 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.model; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.servlet.error.SaServletErrorCode; +import cn.dev33.satoken.util.SaFoxUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 对 SaRequest 包装类的实现(Jakarta-Servlet 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaRequestForServlet implements SaRequest { + + /** + * 底层Request对象 + */ + protected HttpServletRequest request; + + /** + * 实例化 + * @param request request对象 + */ + public SaRequestForServlet(HttpServletRequest request) { + this.request = request; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return request; + } + + /** + * 在 [请求体] 里获取一个值 + */ + @Override + public String getParam(String name) { + return request.getParameter(name); + } + + /** + * 获取 [请求体] 里提交的所有参数名称 + * @return 参数名称列表 + */ + @Override + public Collection getParamNames(){ + return Collections.list(request.getParameterNames()); + } + + /** + * 获取 [请求体] 里提交的所有参数 + * @return 参数列表 + */ + @Override + public Map getParamMap(){ + // 获取所有参数 + Map parameterMap = request.getParameterMap(); + Map map = new LinkedHashMap<>(parameterMap.size()); + for (String key : parameterMap.keySet()) { + String[] values = parameterMap.get(key); + map.put(key, values[0]); + } + return map; + } + + /** + * 在 [请求头] 里获取一个值 + */ + @Override + public String getHeader(String name) { + return request.getHeader(name); + } + + /** + * 在 [Cookie作用域] 里获取一个值 + */ + @Override + public String getCookieValue(String name) { + return getCookieLastValue(name); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name){ + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie != null && name.equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name){ + String value = null; + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie != null && name.equals(cookie.getName())) { + value = cookie.getValue(); + } + } + } + return value; + } + + /** + * 返回当前请求path (不包括上下文名称) + */ + @Override + public String getRequestPath() { + return ApplicationInfo.cutPathPrefix(request.getRequestURI()); + } + + /** + * 返回当前请求的url,例:http://xxx.com/test + * @return see note + */ + @Override + public String getUrl() { + String currDomain = SaManager.getConfig().getCurrDomain(); + if( ! SaFoxUtil.isEmpty(currDomain)) { + return currDomain + this.getRequestPath(); + } + return request.getRequestURL().toString(); + } + + /** + * 返回当前请求的类型 + */ + @Override + public String getMethod() { + return request.getMethod(); + } + + /** + * 查询请求 host + */ + @Override + public String getHost() { + return request.getServerName(); + } + + /** + * 转发请求 + */ + @Override + public Object forward(String path) { + try { + HttpServletResponse response = (HttpServletResponse)SaManager.getSaTokenContext().getResponse().getSource(); + request.getRequestDispatcher(path).forward(request, response); + return null; + } catch (ServletException | IOException e) { + throw new SaTokenException(e).setCode(SaServletErrorCode.CODE_20001); + } + } + +} diff --git a/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java new file mode 100644 index 000000000..39e661a65 --- /dev/null +++ b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.model; + +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.servlet.error.SaServletErrorCode; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 对 SaResponse 包装类的实现(Jakarta-Servlet 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaResponseForServlet implements SaResponse { + + /** + * 底层Request对象 + */ + protected HttpServletResponse response; + + /** + * 实例化 + * @param response response对象 + */ + public SaResponseForServlet(HttpServletResponse response) { + this.response = response; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return response; + } + + /** + * 设置响应状态码 + */ + @Override + public SaResponse setStatus(int sc) { + response.setStatus(sc); + return this; + } + + /** + * 在响应头里写入一个值 + */ + @Override + public SaResponse setHeader(String name, String value) { + response.setHeader(name, value); + return this; + } + + /** + * 在响应头里添加一个值 + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + @Override + public SaResponse addHeader(String name, String value) { + response.addHeader(name, value); + return this; + } + + /** + * 重定向 + */ + @Override + public Object redirect(String url) { + try { + response.sendRedirect(url); + } catch (Exception e) { + throw new SaTokenException(e).setCode(SaServletErrorCode.CODE_20002); + } + return null; + } + + +} diff --git a/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java new file mode 100644 index 000000000..9419d222d --- /dev/null +++ b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.model; + +import cn.dev33.satoken.context.model.SaStorage; +import jakarta.servlet.http.HttpServletRequest; + +/** + * 对 SaStorage 包装类的实现(Jakarta-Servlet 版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaStorageForServlet implements SaStorage { + + /** + * 底层Request对象 + */ + protected HttpServletRequest request; + + /** + * 实例化 + * @param request request对象 + */ + public SaStorageForServlet(HttpServletRequest request) { + this.request = request; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return request; + } + + /** + * 在 [Request作用域] 里写入一个值 + */ + @Override + public SaStorageForServlet set(String key, Object value) { + request.setAttribute(key, value); + return this; + } + + /** + * 在 [Request作用域] 里获取一个值 + */ + @Override + public Object get(String key) { + return request.getAttribute(key); + } + + /** + * 在 [Request作用域] 里删除一个值 + */ + @Override + public SaStorageForServlet delete(String key) { + request.removeAttribute(key); + return this; + } + +} diff --git a/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java new file mode 100644 index 000000000..618326e80 --- /dev/null +++ b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * Sa-Token对接 Jakarta-Servlet API 容器所需要的实现类接口包 + */ +package cn.dev33.satoken.servlet; \ No newline at end of file diff --git a/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaJakartaServletOperateUtil.java b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaJakartaServletOperateUtil.java new file mode 100644 index 000000000..9bd0b969e --- /dev/null +++ b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaJakartaServletOperateUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.util; + +import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + +/** + * Jakarta Servlet 操作工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaJakartaServletOperateUtil { + + /** + * 写入结果到输出流 + * @param response / + * @param result / + */ + public static void writeResult(ServletResponse response, String result) throws IOException { + // 写入输出流 + // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json + // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); + if(response.getContentType() == null) { + response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); + } + response.getWriter().print(result); + response.getWriter().flush(); + } + + +} diff --git a/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextJakartaServletUtil.java b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextJakartaServletUtil.java new file mode 100644 index 000000000..fc90307f9 --- /dev/null +++ b/sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextJakartaServletUtil.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.util; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaRetGenericFunction; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.servlet.model.SaResponseForServlet; +import cn.dev33.satoken.servlet.model.SaStorageForServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + + +/** + * SaTokenContext 上下文读写工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenContextJakartaServletUtil { + + /** + * 写入当前上下文 + * @param request / + * @param response / + */ + public static void setContext(HttpServletRequest request, HttpServletResponse response) { + SaRequest req = new SaRequestForServlet(request); + SaResponse res = new SaResponseForServlet(response); + SaStorage stg = new SaStorageForServlet(request); + SaManager.getSaTokenContext().setContext(req, res, stg); + } + + /** + * 写入上下文对象, 并在执行函数后将其清除 + * @param request / + * @param response / + * @param fun / + */ + public static void setContext(HttpServletRequest request, HttpServletResponse response, SaFunction fun) { + try { + setContext(request, response); + fun.run(); + } finally { + clearContext(); + } + } + + /** + * 写入上下文对象, 并在执行函数后将其清除 + * + * @param request / + * @param response / + * @param fun / + * @return / + * @param / + */ + public static T setContext(HttpServletRequest request, HttpServletResponse response, SaRetGenericFunction fun) { + try { + setContext(request, response); + return fun.run(); + } finally { + clearContext(); + } + } + + /** + * 清除当前上下文 + */ + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); + } + + /** + * 获取当前 ModelBox + * @return / + */ + public static SaTokenContextModelBox getModelBox() { + return SaManager.getSaTokenContext().getModelBox(); + } + + /** + * 获取当前 Request + * @return / + */ + public static HttpServletRequest getRequest() { + return (HttpServletRequest) getModelBox().getRequest().getSource(); + } + + /** + * 获取当前 Response + * @return / + */ + public static HttpServletResponse getResponse() { + return (HttpServletResponse) getModelBox().getResponse().getSource(); + } + +} diff --git a/sa-token-starter/sa-token-jboot-plugin/pom.xml b/sa-token-starter/sa-token-jboot-plugin/pom.xml new file mode 100644 index 000000000..e50a33ef4 --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/pom.xml @@ -0,0 +1,61 @@ + + + + sa-token-starter + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + jar + + sa-token-jboot-plugin + sa-token-jboot-plugin + jboot integrate sa-token + + + 8 + 8 + 3.8.0 + + + + + io.jboot + jboot + provided + + + cn.dev33 + sa-token-core + + + cn.dev33 + sa-token-servlet + + + redis.clients + jedis + ${jedis.version} + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + 1.8 + 1.8 + UTF-8 + -parameters + + + + + \ No newline at end of file diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/PathAnalyzer.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/PathAnalyzer.java new file mode 100644 index 000000000..1df2ab412 --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/PathAnalyzer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PathAnalyzer { + + private static final Map cached = new LinkedHashMap<>(); + private final Pattern pattern; + + public static PathAnalyzer get(String expr) { + PathAnalyzer pa = cached.get(expr); + if (pa == null) { + synchronized(expr.intern()) { + pa = cached.get(expr); + if (pa == null) { + pa = new PathAnalyzer(expr); + cached.put(expr, pa); + } + } + } + + return pa; + } + + private PathAnalyzer(String expr) { + this.pattern = Pattern.compile(exprCompile(expr), Pattern.CASE_INSENSITIVE); + } + + public Matcher matcher(String uri) { + return this.pattern.matcher(uri); + } + + public boolean matches(String uri) { + return this.pattern.matcher(uri).find(); + } + + private static String exprCompile(String expr) { + String p = expr.replace(".", "\\."); + p = p.replace("$", "\\$"); + p = p.replace("**", ".[]"); + p = p.replace("*", "[^/]*"); + if (p.contains("{")) { + if (p.indexOf("_}") > 0) { + p = p.replaceAll("\\{[^\\}]+?\\_\\}", "(.+?)"); + } + + p = p.replaceAll("\\{[^\\}]+?\\}", "([^/]+?)"); + } + + if (!p.startsWith("/")) { + p = "/" + p; + } + + p = p.replace(".[]", ".*"); + return "^" + p + "$"; + } +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaAnnotationInterceptor.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaAnnotationInterceptor.java new file mode 100644 index 000000000..011264b18 --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaAnnotationInterceptor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot; + +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; + +/** + * 注解式鉴权 - 拦截器 + */ +public class SaAnnotationInterceptor implements Interceptor { + @Override + public void intercept(Invocation invocation) { + SaAnnotationStrategy.instance.checkMethodAnnotation.accept((invocation.getMethod())); + invocation.invoke(); + } +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJdkSerializer.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJdkSerializer.java new file mode 100644 index 000000000..8c19c725c --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJdkSerializer.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot; + +import com.jfinal.log.Log; +import io.jboot.components.serializer.JbootSerializer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +public class SaJdkSerializer implements JbootSerializer { + + private static final Log LOG = Log.getLog(SaJdkSerializer.class); + + @Override + public byte[] serialize(Object value) { + if (value == null) { + return null; + } + ObjectOutputStream objectOut = null; + try { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(1024); + objectOut = new ObjectOutputStream(bytesOut); + objectOut.writeObject(value); + objectOut.flush(); + return bytesOut.toByteArray(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + finally { + if(objectOut != null) + try {objectOut.close();} catch (Exception e) { + LOG.error(e.getMessage(), e);} + } + } + + @Override + public Object deserialize(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return null; + } + ObjectInputStream objectInput = null; + try { + ByteArrayInputStream bytesInput = new ByteArrayInputStream(bytes); + objectInput = new ObjectInputStream(bytesInput); + return objectInput.readObject(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + finally { + if (objectInput != null) + try {objectInput.close();} catch (Exception e) {LOG.error(e.getMessage(), e);} + } + } +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisCache.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisCache.java new file mode 100644 index 000000000..943bd0655 --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisCache.java @@ -0,0 +1,271 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot; + +import com.jfinal.plugin.ehcache.IDataLoader; +import io.jboot.components.cache.JbootCache; +import io.jboot.components.cache.JbootCacheConfig; +import io.jboot.core.spi.JbootSpi; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.support.redis.JbootRedisConfig; +import io.jboot.support.redis.RedisScanResult; +import io.jboot.utils.StrUtil; +import redis.clients.jedis.*; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.util.ArrayList; +import java.util.List; + +/** + * sa 缓存处理 + */ +@JbootSpi("sacache") +@SuppressWarnings({"unchecked", "rawtypes"}) +public class SaRedisCache implements JbootCache { + protected JbootRedisConfig config; + protected JedisPool jedisPool; + private final ThreadLocal CACHE_NAME_PREFIX_TL = new ThreadLocal<>(); + + public SaRedisCache(JbootRedisConfig config) { + this.config = config; + + String host = config.getHost(); + Integer port = config.getPort(); + Integer timeout = config.getTimeout(); + String password = config.getPassword(); + Integer database = config.getDatabase(); + String clientName = config.getClientName(); + + if (host.contains(":")) { + port = Integer.valueOf(host.split(":")[1]); + } + + + JedisPoolConfig poolConfig = new JedisPoolConfig(); + + if (StrUtil.isNotBlank(config.getTestWhileIdle())) { + poolConfig.setTestWhileIdle(config.getTestWhileIdle()); + } + + if (StrUtil.isNotBlank(config.getTestOnBorrow())) { + poolConfig.setTestOnBorrow(config.getTestOnBorrow()); + } + + if (StrUtil.isNotBlank(config.getTestOnCreate())) { + poolConfig.setTestOnCreate(config.getTestOnCreate()); + } + + if (StrUtil.isNotBlank(config.getTestOnReturn())) { + poolConfig.setTestOnReturn(config.getTestOnReturn()); + } + + if (StrUtil.isNotBlank(config.getMinEvictableIdleTimeMillis())) { + poolConfig.setMinEvictableIdleTimeMillis(config.getMinEvictableIdleTimeMillis()); + } + + if (StrUtil.isNotBlank(config.getTimeBetweenEvictionRunsMillis())) { + poolConfig.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis()); + } + + if (StrUtil.isNotBlank(config.getNumTestsPerEvictionRun())) { + poolConfig.setNumTestsPerEvictionRun(config.getNumTestsPerEvictionRun()); + } + + if (StrUtil.isNotBlank(config.getMaxTotal())) { + poolConfig.setMaxTotal(config.getMaxTotal()); + } + + if (StrUtil.isNotBlank(config.getMaxIdle())) { + poolConfig.setMaxIdle(config.getMaxIdle()); + } + + if (StrUtil.isNotBlank(config.getMinIdle())) { + poolConfig.setMinIdle(config.getMinIdle()); + } + + if (StrUtil.isNotBlank(config.getMaxWaitMillis())) { + poolConfig.setMaxWaitMillis(config.getMaxWaitMillis()); + } + + this.jedisPool = new JedisPool(poolConfig, host, port, timeout, timeout, password, database, clientName); + } + + public SaRedisCache(JedisPool jedisPool) { + this.jedisPool = jedisPool; + } + + @Override + public JbootCache setCurrentCacheNamePrefix(String cacheNamePrefix) { + if (StrUtil.isNotBlank(cacheNamePrefix)) { + CACHE_NAME_PREFIX_TL.set(cacheNamePrefix); + } else { + CACHE_NAME_PREFIX_TL.remove(); + } + return this; + } + + @Override + public void removeCurrentCacheNamePrefix() { + CACHE_NAME_PREFIX_TL.remove(); + } + + @Override + public JbootCacheConfig getConfig() { + return null; + } + + @Override + public T get(String cacheName, Object key) { + Jedis jedis = getJedis(); + try { + return (T) (jedis.get(key.toString())); + } finally { + returnResource(jedis); + } + } + + @Override + public void put(String cacheName, Object key, Object value) { + Jedis jedis = getJedis(); + try { + jedis.set(key.toString(), value.toString()); + } finally { + returnResource(jedis); + } + } + + @Override + public void put(String cacheName, Object key, Object value, int liveSeconds) { + Jedis jedis = getJedis(); + try { + jedis.setex(key.toString(), Long.parseLong(liveSeconds + ""), value.toString()); + } finally { + returnResource(jedis); + } + } + + @Override + public void remove(String cacheName, Object key) { + Jedis jedis = getJedis(); + try { + jedis.del(key.toString()); + } finally { + returnResource(jedis); + } + } + + @Override + public void removeAll(String cacheName) { + + } + + @Override + public T get(String cacheName, Object key, IDataLoader dataLoader) { + return null; + } + + @Override + public T get(String cacheName, Object key, IDataLoader dataLoader, int liveSeconds) { + return null; + } + + @Override + public Integer getTtl(String cacheName, Object key) { + Jedis jedis = getJedis(); + try { + return jedis.ttl(key.toString()).intValue(); + } finally { + returnResource(jedis); + } + } + + @Override + public void setTtl(String cacheName, Object key, int seconds) { + Jedis jedis = getJedis(); + try { + jedis.expire(key.toString(), Long.parseLong(seconds + "")); + } finally { + returnResource(jedis); + } + } + + @Override + public void refresh(String cacheName, Object key) { + + } + + @Override + public void refresh(String cacheName) { + + } + + @Override + public List getNames() { + return null; + } + + @Override + public List getKeys(String cacheName) { + List keys = new ArrayList<>(); + String cursor = "0"; + int scanCount = 1000; + boolean continueState = true; + do { + RedisScanResult redisScanResult = this.scan("*", cursor, scanCount); + List scanKeys = redisScanResult.getResults(); + cursor = redisScanResult.getCursor(); + + if (scanKeys != null && scanKeys.size() > 0) { + for (String key : scanKeys) { + keys.add(key.substring(3)); + } + } + + if (redisScanResult.isCompleteIteration()) { + continueState = false; + } + } while (continueState); + + return keys; + } + + public Jedis getJedis() { + try { + return jedisPool.getResource(); + } catch (JedisConnectionException e) { + throw new JbootIllegalConfigException("can not connect to redis host " + config.getHost() + ":" + config.getPort() + " ," + + " cause : " + e, e); + } + } + + + public void returnResource(Jedis jedis) { + if (jedis != null) { + jedis.close(); + } + } + + public RedisScanResult scan(String pattern, String cursor, int scanCount) { + ScanParams params = new ScanParams(); + params.match(pattern).count(scanCount); + try (Jedis jedis = getJedis()) { + ScanResult scanResult = jedis.scan(cursor, params); + return new RedisScanResult<>(scanResult.getCursor(), scanResult.getResult()); + } + } + + +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenCacheDao.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenCacheDao.java new file mode 100644 index 000000000..99d794dcc --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenCacheDao.java @@ -0,0 +1,300 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.auto.SaTokenDaoBySessionFollowObject; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.util.SaFoxUtil; +import io.jboot.Jboot; +import io.jboot.components.serializer.JbootSerializer; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.support.redis.JbootRedisConfig; +import io.jboot.utils.ConfigUtil; +import redis.clients.jedis.Jedis; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 使用Jboot的缓存方法存取Token数据 + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class SaTokenCacheDao implements SaTokenDaoBySessionFollowObject { + + protected SaRedisCache saRedisCache; + protected JbootSerializer serializer; + + private final Map saRedisMap = new ConcurrentHashMap(); + + /** + * 使用默认redis配置 + */ + public SaTokenCacheDao() { + JbootRedisConfig config = Jboot.config(JbootRedisConfig.class); + this.saRedisCache = new SaRedisCache(config); + this.serializer = new SaJdkSerializer(); + } + + /** + * 调用的Cache名称 + * + * @param cacheName 使用的缓存配置名,默认为 default + */ + public SaTokenCacheDao(String cacheName) { + SaRedisCache saCache = this.saRedisMap.get(cacheName); + if (saCache == null) { + synchronized (this) { + saCache = this.saRedisMap.get(cacheName); + if (saCache == null) { + Map configModels = ConfigUtil.getConfigModels(JbootRedisConfig.class); + if (!configModels.containsKey(cacheName)) { + throw new JbootIllegalConfigException("Please config \"jboot.redis." + cacheName + ".host\" in your jboot.properties."); + } + + JbootRedisConfig jbootRedisConfig = configModels.get(cacheName); + saCache = new SaRedisCache(jbootRedisConfig); + this.saRedisMap.put(cacheName, saCache); + } + } + } + this.saRedisCache = saCache; + this.serializer = new SaJdkSerializer(); + } + + + @Override + public String get(String key) { + Jedis jedis = saRedisCache.getJedis(); + try { + return jedis.get(key); + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public void set(String key, String value, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + Jedis jedis = saRedisCache.getJedis(); + try { + if (timeout == SaTokenDao.NEVER_EXPIRE) { + jedis.set(key, value); + } else { + jedis.setex(key, timeout, value); + } + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public void update(String key, String value) { + long expire = getTimeout(key); + // -2 = 无此键 + if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.set(key, value, expire); + } + + @Override + public void delete(String key) { + Jedis jedis = saRedisCache.getJedis(); + try { + jedis.del(key); + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public long getTimeout(String key) { + Jedis jedis = saRedisCache.getJedis(); + try { + return jedis.ttl(key); + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public void updateTimeout(String key, long timeout) { + //判断是否想要设置为永久 + if (timeout == SaTokenDao.NEVER_EXPIRE) { + long expire = getTimeout(key); + if (expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.set(key, this.get(key), timeout); + } + return; + } + Jedis jedis = saRedisCache.getJedis(); + try { + jedis.expire(key, timeout); + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public Object getObject(String key) { + Jedis jedis = saRedisCache.getJedis(); + try { + return valueFromBytes(jedis.get(keyToBytes(key))); + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public T getObject(String key, Class classType) { + return (T) getObject(key); + } + + @Override + public void setObject(String key, Object object, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + Jedis jedis = saRedisCache.getJedis(); + try { + if (timeout == SaTokenDao.NEVER_EXPIRE) { + jedis.set(keyToBytes(key), valueToBytes(object)); + } else { + jedis.setex(keyToBytes(key), timeout, valueToBytes(object)); + } + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public void updateObject(String key, Object object) { + long expire = getObjectTimeout(key); + // -2 = 无此键 + if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.setObject(key, object, expire); + } + + @Override + public void deleteObject(String key) { + Jedis jedis = saRedisCache.getJedis(); + try { + jedis.del(keyToBytes(key)); + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public long getObjectTimeout(String key) { + Jedis jedis = saRedisCache.getJedis(); + try { + return jedis.ttl(keyToBytes(key)); + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public void updateObjectTimeout(String key, long timeout) { + //判断是否想要设置为永久 + if (timeout == SaTokenDao.NEVER_EXPIRE) { + long expire = getObjectTimeout(key); + if (expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.setObject(key, this.getObject(key), timeout); + } + return; + } + Jedis jedis = saRedisCache.getJedis(); + try { + jedis.expire(keyToBytes(key), timeout); + } finally { + saRedisCache.returnResource(jedis); + } + } + + @Override + public SaSession getSession(String sessionId) { + return SaTokenDaoBySessionFollowObject.super.getSession(sessionId); + } + + @Override + public void setSession(SaSession session, long timeout) { + SaTokenDaoBySessionFollowObject.super.setSession(session, timeout); + } + + @Override + public void updateSession(SaSession session) { + SaTokenDaoBySessionFollowObject.super.updateSession(session); + } + + @Override + public void deleteSession(String sessionId) { + SaTokenDaoBySessionFollowObject.super.deleteSession(sessionId); + } + + @Override + public long getSessionTimeout(String sessionId) { + return SaTokenDaoBySessionFollowObject.super.getSessionTimeout(sessionId); + } + + @Override + public void updateSessionTimeout(String sessionId, long timeout) { + SaTokenDaoBySessionFollowObject.super.updateSessionTimeout(sessionId, timeout); + } + + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + Jedis jedis = saRedisCache.getJedis(); + try { + Set keys = jedis.keys(prefix + "*" + keyword + "*"); + List list = new ArrayList<>(keys); + return SaFoxUtil.searchList(list, start, size, sortType); + } finally { + saRedisCache.returnResource(jedis); + } + } + + + protected byte[] keyToBytes(Object key) { + return key.toString().getBytes(); + } + + protected byte[] valueToBytes(Object value) { + return serializer.serialize(value); + } + + protected Object valueFromBytes(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return null; + } + return serializer.deserialize(bytes); + } +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenContextForJboot.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenContextForJboot.java new file mode 100644 index 000000000..32ea03a7f --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenContextForJboot.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot; + +import cn.dev33.satoken.context.SaTokenContextForReadOnly; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.servlet.model.SaResponseForServlet; +import cn.dev33.satoken.servlet.model.SaStorageForServlet; +import cn.dev33.satoken.strategy.SaStrategy; +import io.jboot.web.controller.JbootControllerContext; + +/** + * Sa-Token 上线文处理器 [Jboot 版本实现] + */ +public class SaTokenContextForJboot implements SaTokenContextForReadOnly { + + public SaTokenContextForJboot() { + // 重写路由匹配算法 + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return PathAnalyzer.get(pattern).matches(path); + }; + } + + /** + * 获取当前请求的Request对象 + */ + @Override + public SaRequest getRequest() { + return new SaRequestForServlet(JbootControllerContext.get().getRequest()); + } + + /** + * 获取当前请求的Response对象 + */ + @Override + public SaResponse getResponse() { + return new SaResponseForServlet(JbootControllerContext.get().getResponse()); + } + + /** + * 获取当前请求的 [存储器] 对象 + */ + @Override + public SaStorage getStorage() { + return new SaStorageForServlet(JbootControllerContext.get().getRequest()); + } + + @Override + public boolean isValid() { + return JbootControllerContext.get() != null; + } +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenPathFilter.java b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenPathFilter.java new file mode 100644 index 000000000..31149ba25 --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenPathFilter.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.filter.SaFilterAuthStrategy; +import cn.dev33.satoken.filter.SaFilterErrorStrategy; +import cn.dev33.satoken.filter.SaFilter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SaTokenPathFilter implements SaFilter { + + // ------------------------ 设置此过滤器 拦截 & 放行 的路由 + + /** + * 拦截路由 + */ + public List includeList = new ArrayList<>(); + + /** + * 放行路由 + */ + public List excludeList = new ArrayList<>(); + + @Override + public SaTokenPathFilter addInclude(String... paths) { + includeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaTokenPathFilter addExclude(String... paths) { + excludeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaTokenPathFilter setIncludeList(List pathList) { + includeList = pathList; + return this; + } + + @Override + public SaTokenPathFilter setExcludeList(List pathList) { + excludeList = pathList; + return this; + } + + + // ------------------------ 钩子函数 + + /** + * 认证函数:每次请求执行 + */ + public SaFilterAuthStrategy auth = r -> {}; + + /** + * 异常处理函数:每次[认证函数]发生异常时执行此函数 + */ + public SaFilterErrorStrategy error = e -> { + throw new SaTokenException(e); + }; + + /** + * 前置函数:在每次[认证函数]之前执行 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth + */ + public SaFilterAuthStrategy beforeAuth = r -> {}; + + @Override + public SaTokenPathFilter setAuth(SaFilterAuthStrategy auth) { + this.auth = auth; + return this; + } + + @Override + public SaTokenPathFilter setError(SaFilterErrorStrategy error) { + this.error = error; + return this; + } + + @Override + public SaTokenPathFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + + /*@Override + public void doFilter(Controller ctx, FilterChain chain) throws Throwable { + try { + // 执行全局过滤器 + beforeAuth.run(null); + SaRouter.match(includeList).notMatch(excludeList).check(r -> { + auth.run(null); + }); + + } catch (StopMatchException e) { + + } catch (Throwable e) { + // 1. 获取异常处理策略结果 + String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e)); + // 2. 写入输出流 + ctx.renderText(result); + return; + } + + // 执行 + chain.doFilter(ctx); + }*/ +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AppRun.java b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AppRun.java new file mode 100644 index 000000000..57f2ff5b6 --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AppRun.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot.test; + +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/") +public class AppRun extends JbootController { + public static void main(String[] args) { + JbootApplication.run(args); + } + + public void index() { + renderText("index"); + } + + public void doLogin() { + StpUtil.login(10001); + //赋值角色 + renderText("登录成功"); + } + + public void getLoginInfo() { + System.out.println("是否登录:" + StpUtil.isLogin()); + System.out.println("登录信息" + StpUtil.getTokenInfo()); + renderJson(StpUtil.getTokenInfo()); + } + + @SaCheckRole("super-admin") + public void add() { + renderText("超级管理员方法!"); + } + + @SuppressWarnings("unused") + public void token(String token) { + Object t = Jboot.getRedis().get("xxxxx"); //默认redis库 + SaSession saSession = StpUtil.getSessionByLoginId(StpUtil.getLoginIdByToken(token), false); //satoken redis库 + renderJson(saSession); + } +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java new file mode 100644 index 000000000..af6bdeaee --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot.test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaCookieConfig; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.jboot.SaAnnotationInterceptor; +import cn.dev33.satoken.jboot.SaTokenCacheDao; +import cn.dev33.satoken.jboot.SaTokenContextForJboot; +import cn.dev33.satoken.util.SaTokenConsts; +import com.jfinal.config.Constants; +import com.jfinal.config.Interceptors; +import com.jfinal.config.Routes; +import com.jfinal.template.Engine; +import io.jboot.aop.jfinal.JfinalHandlers; +import io.jboot.aop.jfinal.JfinalPlugins; +import io.jboot.core.listener.JbootAppListener; + +public class AtteStartListener implements JbootAppListener { + public void onInit() { + SaTokenContext saTokenContext = new SaTokenContextForJboot(); + SaManager.setSaTokenContext(saTokenContext); + SaManager.setStpInterface(new StpInterfaceImpl()); + SaTokenConfig saTokenConfig = new SaTokenConfig(); + saTokenConfig.setTokenStyle(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID); + saTokenConfig.setTimeout(60*60*4); //登录有效时间4小时 + saTokenConfig.setActiveTimeout(30*60); //半小时无操作就冻结 token + saTokenConfig.setIsShare(false); + saTokenConfig.setTokenName("token"); //更换satoken的名称 + saTokenConfig.setCookie(new SaCookieConfig().setHttpOnly(true)); //开启cookies的httponly属性 + SaManager.setConfig(saTokenConfig); + } + + @Override + public void onConstantConfig(Constants constants) { + + } + + @Override + public void onRouteConfig(Routes routes) { + + } + + @Override + public void onEngineConfig(Engine engine) { + + } + + @Override + public void onPluginConfig(JfinalPlugins plugins) { + + } + + @Override + public void onInterceptorConfig(Interceptors interceptors) { + //开启注解方式权限验证 + interceptors.add(new SaAnnotationInterceptor()); + } + + @Override + public void onHandlerConfig(JfinalHandlers handlers) { + + } + + @Override + public void onStartBefore() { + + } + + @Override + public void onStart() { + SaManager.setSaTokenDao(new SaTokenCacheDao("sa")); + } + + @Override + public void onStartFinish() { + + } + + @Override + public void onStop() { + + } +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/StpInterfaceImpl.java b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/StpInterfaceImpl.java new file mode 100644 index 000000000..bb1f48f1d --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/StpInterfaceImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jboot.test; + +import cn.dev33.satoken.stp.StpInterface; +import io.jboot.aop.annotation.Bean; + +import java.util.ArrayList; +import java.util.List; + +@Bean +public class StpInterfaceImpl implements StpInterface { + @Override + public List getPermissionList(Object o, String s) { + return null; + } + + @Override + public List getRoleList(Object o, String s) { + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } +} diff --git a/sa-token-starter/sa-token-jboot-plugin/src/test/resources/jboot.properties b/sa-token-starter/sa-token-jboot-plugin/src/test/resources/jboot.properties new file mode 100644 index 000000000..c5069fe8f --- /dev/null +++ b/sa-token-starter/sa-token-jboot-plugin/src/test/resources/jboot.properties @@ -0,0 +1,15 @@ +undertow.devMode=true +undertow.port=9980 +undertow.host=0.0.0.0 +#other redis config +jboot.cache.type=redis +jboot.redis.host=127.0.0.1 +jboot.redis.port=6379 +jboot.redis.password=123456 +jboot.redis.database=3 +#satoken redis config +jboot.cache.sa.type=sacache +jboot.redis.sa.host=127.0.0.1 +jboot.redis.sa.port=6379 +jboot.redis.sa.password=123456 +jboot.redis.sa.database=1 \ No newline at end of file diff --git a/sa-token-starter/sa-token-jfinal-plugin/pom.xml b/sa-token-starter/sa-token-jfinal-plugin/pom.xml new file mode 100644 index 000000000..7ce6197cb --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/pom.xml @@ -0,0 +1,85 @@ + + + + sa-token-starter + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + jar + + sa-token-jfinal-plugin + sa-token-jfinal-plugin + jfinal integrate sa-token + + + 8 + 8 + + + + org.slf4j + slf4j-api + 1.7.24 + + + com.jfinal + jfinal-undertow + 2.8 + + + com.jfinal + jfinal + provided + + + cn.dev33 + sa-token-core + + + cn.dev33 + sa-token-servlet + + + org.apache.commons + commons-pool2 + test + + + redis.clients + jedis + 3.7.0 + + + slf4j-api + org.slf4j + + + + + de.ruedigermoeller + fst + 2.29 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + 1.8 + 1.8 + UTF-8 + -parameters + + + + + \ No newline at end of file diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/PathAnalyzer.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/PathAnalyzer.java new file mode 100644 index 000000000..788673c35 --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/PathAnalyzer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PathAnalyzer { + + private static final Map cached = new LinkedHashMap<>(); + private final Pattern pattern; + + public static PathAnalyzer get(String expr) { + PathAnalyzer pa = cached.get(expr); + if (pa == null) { + synchronized(expr.intern()) { + pa = cached.get(expr); + if (pa == null) { + pa = new PathAnalyzer(expr); + cached.put(expr, pa); + } + } + } + + return pa; + } + + private PathAnalyzer(String expr) { + this.pattern = Pattern.compile(exprCompile(expr), Pattern.CASE_INSENSITIVE); + } + + public Matcher matcher(String uri) { + return this.pattern.matcher(uri); + } + + public boolean matches(String uri) { + return this.pattern.matcher(uri).find(); + } + + private static String exprCompile(String expr) { + String p = expr.replace(".", "\\."); + p = p.replace("$", "\\$"); + p = p.replace("**", ".[]"); + p = p.replace("*", "[^/]*"); + if (p.contains("{")) { + if (p.indexOf("_}") > 0) { + p = p.replaceAll("\\{[^\\}]+?\\_\\}", "(.+?)"); + } + + p = p.replaceAll("\\{[^\\}]+?\\}", "([^/]+?)"); + } + + if (!p.startsWith("/")) { + p = "/" + p; + } + + p = p.replace(".[]", ".*"); + return "^" + p + "$"; + } +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaAnnotationInterceptor.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaAnnotationInterceptor.java new file mode 100644 index 000000000..5f80f5c28 --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaAnnotationInterceptor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal; + +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; + +/** + * 注解式鉴权 - 拦截器 + */ +public class SaAnnotationInterceptor implements Interceptor { + @Override + public void intercept(Invocation invocation) { + SaAnnotationStrategy.instance.checkMethodAnnotation.accept((invocation.getMethod())); + invocation.invoke(); + } +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaControllerContext.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaControllerContext.java new file mode 100644 index 000000000..0ca1d14fa --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaControllerContext.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal; + +import com.jfinal.core.Controller; + +public class SaControllerContext { + private static ThreadLocal controllers = new ThreadLocal<>(); + + + public static void hold(Controller controller) { + controllers.set(controller); + } + + public static Controller get() { + return controllers.get(); + } + + public static void release() { + controllers.remove(); + } +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaJdkSerializer.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaJdkSerializer.java new file mode 100644 index 000000000..fd052fc48 --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaJdkSerializer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal; + +import com.jfinal.kit.LogKit; +import com.jfinal.plugin.redis.serializer.ISerializer; +import com.jfinal.plugin.redis.serializer.JdkSerializer; +import redis.clients.jedis.util.SafeEncoder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +public class SaJdkSerializer implements ISerializer { + + public static final ISerializer me = new JdkSerializer(); + + public byte[] keyToBytes(String key) { + return SafeEncoder.encode(key); + } + + public String keyFromBytes(byte[] bytes) { + return SafeEncoder.encode(bytes); + } + + public byte[] fieldToBytes(Object field) { + return valueToBytes(field); + } + + public Object fieldFromBytes(byte[] bytes) { + return valueFromBytes(bytes); + } + + public byte[] valueToBytes(Object value) { + ObjectOutputStream objectOut = null; + try { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(1024); + objectOut = new ObjectOutputStream(bytesOut); + objectOut.writeObject(value); + objectOut.flush(); + return bytesOut.toByteArray(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + finally { + if(objectOut != null) + try {objectOut.close();} catch (Exception e) { + LogKit.error(e.getMessage(), e);} + } + } + + public Object valueFromBytes(byte[] bytes) { + if(bytes == null || bytes.length == 0) + return null; + + ObjectInputStream objectInput = null; + try { + ByteArrayInputStream bytesInput = new ByteArrayInputStream(bytes); + objectInput = new ObjectInputStream(bytesInput); + return objectInput.readObject(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + finally { + if (objectInput != null) + try {objectInput.close();} catch (Exception e) {LogKit.error(e.getMessage(), e);} + } + } +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java new file mode 100644 index 000000000..a882d3818 --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java @@ -0,0 +1,174 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal; + +import com.jfinal.aop.Invocation; +import com.jfinal.config.Constants; +import com.jfinal.core.*; +import com.jfinal.kit.ReflectKit; +import com.jfinal.log.Log; +import com.jfinal.render.Render; +import com.jfinal.render.RenderException; +import com.jfinal.render.RenderManager; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +public class SaTokenActionHandler extends ActionHandler { + protected boolean devMode; + protected ActionMapping actionMapping; + protected ControllerFactory controllerFactory; + protected ActionReporter actionReporter; + protected static final RenderManager renderManager = RenderManager.me(); + private static final Log log = Log.getLog(ActionHandler.class); + + protected void init(ActionMapping actionMapping, Constants constants) { + this.actionMapping = actionMapping; + this.devMode = constants.getDevMode(); + this.controllerFactory = constants.getControllerFactory(); + this.actionReporter = constants.getActionReporter(); + } + + /** + * 子类覆盖 getAction 方法可以定制路由功能 + */ + protected Action getAction(String target, String[] urlPara) { + return actionMapping.getAction(target, urlPara); + } + + @Override + public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { + if (target.indexOf('.') != -1) { + return ; + } + + isHandled[0] = true; + String[] urlPara = {null}; + Action action = getAction(target, urlPara); + + if (action == null) { + if (log.isWarnEnabled()) { + String qs = request.getQueryString(); + log.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs)); + } + renderManager.getRenderFactory().getErrorRender(404).setContext(request, response).render(); + return ; + } + + Controller controller = null; + try { + // Controller controller = action.getControllerClass().newInstance(); + controller = controllerFactory.getController(action.getControllerClass()); + CPI._init_(controller, action, request, response, urlPara[0]); +// if (resolveJson && controller.isJsonRequest()) { +// // 注入 JsonRequest 包装对象接管 request +// controller.setHttpServletRequest(jsonRequestFactory.apply(controller.getRawData(), controller.getRequest())); +// } + //加入SaToken上下文处理 + SaControllerContext.hold(controller); + if (devMode) { + if (actionReporter.isReportAfterInvocation(request)) { + new Invocation(action, controller).invoke(); + actionReporter.report(target, controller, action); + } else { + actionReporter.report(target, controller, action); + new Invocation(action, controller).invoke(); + } + } + else { + new Invocation(action, controller).invoke(); + } + + Render render = controller.getRender(); + if (render instanceof ForwardActionRender) { + String actionUrl = ((ForwardActionRender)render).getActionUrl(); + if (target.equals(actionUrl)) { + throw new RuntimeException("The forward action url is the same as before."); + } else { + handle(actionUrl, request, response, isHandled); + } + return ; + } + + if (render == null) { + render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName()); + } + render.setContext(request, response, action.getViewPath()).render(); + } + catch (RenderException e) { + if (log.isErrorEnabled()) { + String qs = request.getQueryString(); + log.error(qs == null ? target : target + "?" + qs, e); + } + } + catch (ActionException e) { + handleActionException(target, request, response, action, e); + } + catch (Exception e) { + if (log.isErrorEnabled()) { + String qs = request.getQueryString(); + String targetInfo = (qs == null ? target : target + "?" + qs); + String sign = ReflectKit.getMethodSignature(action.getMethod()); + log.error(sign + " : " + targetInfo, e); + } + renderManager.getRenderFactory().getErrorRender(500).setContext(request, response, action.getViewPath()).render(); + } finally { + SaControllerContext.release(); + controllerFactory.recycle(controller); + } + } + + /** + * 抽取出该方法是为了缩短 handle 方法中的代码量,确保获得 JIT 优化, + * 方法长度超过 8000 个字节码时,将不会被 JIT 编译成二进制码 + *

    + * 通过开启 java 的 -XX:+PrintCompilation 启动参数得知,handle(...) + * 方法(73 行代码)已被 JIT 优化,优化后的字节码长度为 593 个字节,相当于 + * 每行代码产生 8.123 个字节 + */ + private void handleActionException(String target, HttpServletRequest request, HttpServletResponse response, Action action, ActionException e) { + int errorCode = e.getErrorCode(); + String msg = null; + if (errorCode == 404) { + msg = "404 Not Found: "; + } else if (errorCode == 400) { + msg = "400 Bad Request: "; + } else if (errorCode == 401) { + msg = "401 Unauthorized: "; + } else if (errorCode == 403) { + msg = "403 Forbidden: "; + } + + if (msg != null) { + if (log.isWarnEnabled()) { + String qs = request.getQueryString(); + msg = msg + (qs == null ? target : target + "?" + qs); + if (e.getMessage() != null) { + msg = msg + "\n" + e.getMessage(); + } + log.warn(msg); + } + } else { + if (log.isErrorEnabled()) { + String qs = request.getQueryString(); + log.error(errorCode + " Error: " + (qs == null ? target : target + "?" + qs), e); + } + } + + e.getErrorRender().setContext(request, response, action.getViewPath()).render(); + } +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenContextForJfinal.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenContextForJfinal.java new file mode 100644 index 000000000..240e6332a --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenContextForJfinal.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal; + +import cn.dev33.satoken.context.SaTokenContextForReadOnly; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.servlet.model.SaResponseForServlet; +import cn.dev33.satoken.servlet.model.SaStorageForServlet; +import cn.dev33.satoken.strategy.SaStrategy; + +/** + * Sa-Token 上线文处理器 [Jfinal 版本实现] + */ +public class SaTokenContextForJfinal implements SaTokenContextForReadOnly { + + public SaTokenContextForJfinal() { + // 重写路由匹配算法 + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return PathAnalyzer.get(pattern).matches(path); + }; + } + + /** + * 获取当前请求的Request对象 + */ + @Override + public SaRequest getRequest() { + return new SaRequestForServlet(SaControllerContext.get().getRequest()); + } + + /** + * 获取当前请求的Response对象 + */ + @Override + public SaResponse getResponse() { + return new SaResponseForServlet(SaControllerContext.get().getResponse()); + } + + /** + * 获取当前请求的 [存储器] 对象 + */ + @Override + public SaStorage getStorage() { + return new SaStorageForServlet(SaControllerContext.get().getRequest()); + } + + @Override + public boolean isValid() { + return SaControllerContext.get() != null; + } + +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java new file mode 100644 index 000000000..2828f8031 --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java @@ -0,0 +1,268 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.auto.SaTokenDaoBySessionFollowObject; +import cn.dev33.satoken.util.SaFoxUtil; +import com.jfinal.plugin.redis.Cache; +import com.jfinal.plugin.redis.Redis; +import com.jfinal.plugin.redis.serializer.ISerializer; +import redis.clients.jedis.Jedis; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class SaTokenDaoRedis implements SaTokenDaoBySessionFollowObject { + + protected Cache redis; + protected ISerializer serializer; + /** + * 标记:是否已初始化成功 + */ + public boolean isInit; + + public SaTokenDaoRedis(String confName) { + redis = Redis.use(confName); + serializer = new SaJdkSerializer(); + } + + /** + * 获取Value,如无返空 + */ + @Override + public String get(String key) { + Jedis jedis = getJedis(); + try { + return jedis.get(key); + } finally { + close(jedis); + } + } + + /** + * 写入Value,并设定存活时间 (单位: 秒) + */ + @Override + public void set(String key, String value, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + Jedis jedis = getJedis(); + try { + if (timeout == SaTokenDao.NEVER_EXPIRE) { + jedis.set(key, value); + } else { + jedis.setex(key, timeout, value); + } + } finally { + close(jedis); + } + } + + /** + * 修改指定key-value键值对 (过期时间不变) + */ + @Override + public void update(String key, String value) { + long expire = getTimeout(key); + // -2 = 无此键 + if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.set(key, value, expire); + } + + /** + * 删除Value + */ + @Override + public void delete(String key) { + Jedis jedis = getJedis(); + try { + jedis.del(key); + } finally { + close(jedis); + } + } + + /** + * 获取Value的剩余存活时间 (单位: 秒) + */ + @Override + public long getTimeout(String key) { + Jedis jedis = getJedis(); + try { + return jedis.ttl(key); + } finally { + close(jedis); + } + } + + /** + * 修改Value的剩余存活时间 (单位: 秒) + */ + @Override + public void updateTimeout(String key, long timeout) { + //判断是否想要设置为永久 + if (timeout == SaTokenDao.NEVER_EXPIRE) { + long expire = getTimeout(key); + if (expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.set(key, this.get(key), timeout); + } + return; + } + Jedis jedis = getJedis(); + try { + jedis.expire(key, timeout); + } finally { + close(jedis); + } + } + + /** + * 获取Object,如无返空 + */ + @Override + public Object getObject(String key) { + Jedis jedis = getJedis(); + try { + return valueFromBytes(jedis.get(keyToBytes(key))); + } finally { + close(jedis); + } + } + + @Override + public T getObject(String key, Class classType) { + return (T) getObject(key); + } + + /** + * 写入Object,并设定存活时间 (单位: 秒) + */ + @Override + public void setObject(String key, Object object, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + Jedis jedis = getJedis(); + try { + if (timeout == SaTokenDao.NEVER_EXPIRE) { + jedis.set(keyToBytes(key), valueToBytes(object)); + } else { + jedis.setex(keyToBytes(key), timeout, valueToBytes(object)); + } + } finally { + close(jedis); + } + } + + /** + * 更新Object (过期时间不变) + */ + @Override + public void updateObject(String key, Object object) { + long expire = getObjectTimeout(key); + // -2 = 无此键 + if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.setObject(key, object, expire); + } + + /** + * 删除Object + */ + @Override + public void deleteObject(String key) { + Jedis jedis = getJedis(); + try { + jedis.del(keyToBytes(key)); + } finally { + close(jedis); + } + } + + @Override + public long getObjectTimeout(String key) { + Jedis jedis = getJedis(); + try { + return jedis.ttl(keyToBytes(key)); + } finally { + close(jedis); + } + } + + /** + * 修改Object的剩余存活时间 (单位: 秒) + */ + @Override + public void updateObjectTimeout(String key, long timeout) { + //判断是否想要设置为永久 + if (timeout == SaTokenDao.NEVER_EXPIRE) { + long expire = getObjectTimeout(key); + if (expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.setObject(key, this.getObject(key), timeout); + } + return; + } + Jedis jedis = getJedis(); + try { + jedis.expire(keyToBytes(key), timeout); + } finally { + close(jedis); + } + } + + /** + * 搜索数据 + */ + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + Set keys = redis.keys(prefix + "*" + keyword + "*"); + List list = new ArrayList<>(keys); + return SaFoxUtil.searchList(list, start, size, sortType); + } + + public Jedis getJedis() { + return redis.getJedis(); + } + + public void close(Jedis jedis) { + if (jedis != null) + jedis.close(); + } + + protected byte[] keyToBytes(Object key) { + return key.toString().getBytes(); + } + + protected byte[] valueToBytes(Object value) { + return serializer.valueToBytes(value); + } + + protected Object valueFromBytes(byte[] bytes) { + return serializer.valueFromBytes(bytes); + } +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenPathFilter.java b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenPathFilter.java new file mode 100644 index 000000000..2d6e2004a --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenPathFilter.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.filter.SaFilter; +import cn.dev33.satoken.filter.SaFilterAuthStrategy; +import cn.dev33.satoken.filter.SaFilterErrorStrategy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SaTokenPathFilter implements SaFilter { + + // ------------------------ 设置此过滤器 拦截 & 放行 的路由 + + /** + * 拦截路由 + */ + public List includeList = new ArrayList<>(); + + /** + * 放行路由 + */ + public List excludeList = new ArrayList<>(); + + @Override + public SaTokenPathFilter addInclude(String... paths) { + includeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaTokenPathFilter addExclude(String... paths) { + excludeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaTokenPathFilter setIncludeList(List pathList) { + includeList = pathList; + return this; + } + + @Override + public SaTokenPathFilter setExcludeList(List pathList) { + excludeList = pathList; + return this; + } + + + // ------------------------ 钩子函数 + + /** + * 认证函数:每次请求执行 + */ + public SaFilterAuthStrategy auth = r -> {}; + + /** + * 异常处理函数:每次[认证函数]发生异常时执行此函数 + */ + public SaFilterErrorStrategy error = e -> { + throw new SaTokenException(e); + }; + + /** + * 前置函数:在每次[认证函数]之前执行 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth + */ + public SaFilterAuthStrategy beforeAuth = r -> {}; + + @Override + public SaTokenPathFilter setAuth(SaFilterAuthStrategy auth) { + this.auth = auth; + return this; + } + + @Override + public SaTokenPathFilter setError(SaFilterErrorStrategy error) { + this.error = error; + return this; + } + + @Override + public SaTokenPathFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + + /*@Override + public void doFilter(Controller ctx, FilterChain chain) throws Throwable { + try { + // 执行全局过滤器 + beforeAuth.run(null); + SaRouter.match(includeList).notMatch(excludeList).check(r -> { + auth.run(null); + }); + + } catch (StopMatchException e) { + + } catch (Throwable e) { + // 1. 获取异常处理策略结果 + String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e)); + // 2. 写入输出流 + ctx.renderText(result); + return; + } + + // 执行 + chain.doFilter(ctx); + }*/ +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/AppRun.java b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/AppRun.java new file mode 100644 index 000000000..7e025633b --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/AppRun.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal.test; + +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.stp.StpUtil; +import com.jfinal.core.Controller; +import com.jfinal.core.Path; +import com.jfinal.server.undertow.UndertowServer; + +@Path("/") +public class AppRun extends Controller { + public static void main(String[] args) { + UndertowServer.create(Config.class) + .addHotSwapClassPrefix("cn.dev33.satoken.jfinal.") + .start(); + } + + public void index(){ + renderText("index"); + } + + public void doLogin(){ + StpUtil.logout(); + StpUtil.login(10002); + //赋值角色 + renderText("登录成功"); + } + + public void getLoginInfo(){ + System.out.println("是否登录:"+StpUtil.isLogin()); + System.out.println("登录信息"+StpUtil.getTokenInfo()); + renderJson(StpUtil.getTokenInfo()); + } + + @SaCheckRole("super-admin") + public void add(){ + renderText("超级管理员方法!"); + } +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java new file mode 100644 index 000000000..ff9f6ecf5 --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal.test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaCookieConfig; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.jfinal.*; +import cn.dev33.satoken.util.SaTokenConsts; +import com.jfinal.config.*; +import com.jfinal.plugin.redis.RedisPlugin; +import com.jfinal.plugin.redis.serializer.ISerializer; +import com.jfinal.template.Engine; + +public class Config extends JFinalConfig { + + public Config(){ + //注册权限验证功能,由saToken处理请求上下文 + SaTokenContext saTokenContext = new SaTokenContextForJfinal(); + SaManager.setSaTokenContext(saTokenContext); + //加载权限角色设置数据接口 + SaManager.setStpInterface(new StpInterfaceImpl()); + //设置token生成类型 + SaTokenConfig saTokenConfig = new SaTokenConfig(); + saTokenConfig.setTokenStyle(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID); + saTokenConfig.setTimeout(60*60*4); //登录有效时间4小时 + saTokenConfig.setActiveTimeout(30*60); //半小时无操作就冻结 token + saTokenConfig.setIsShare(false); + saTokenConfig.setTokenName("token"); //更改satoken的cookies名称 + SaCookieConfig saCookieConfig = new SaCookieConfig(); + saCookieConfig.setHttpOnly(true); //开启cookies 的httponly属性 + saTokenConfig.setCookie(saCookieConfig); + SaManager.setConfig(saTokenConfig); + } + + @Override + public void configConstant(Constants constants) { + + } + + @Override + public void configRoute(Routes routes) { + //路由扫描 + routes.scan("cn.dev33.satoken.jfinal"); + } + + @Override + public void configEngine(Engine engine) { + + } + + @Override + public void configPlugin(Plugins plugins) { + //添加redis扩展 + plugins.add(createRedisPlugin("satoken",1, SaJdkSerializer.me)); + } + + @Override + public void configInterceptor(Interceptors interceptors) { + //开启注解方式权限验证 + interceptors.add(new SaAnnotationInterceptor()); + } + + @Override + public void configHandler(Handlers handlers) { + //将上下文交给satoken处理 + handlers.setActionHandler(new SaTokenActionHandler()); + } + + /** + * 创建Redis插件 + * @param name 名称 + * @param dbIndex 使用的库ID + * @param serializer 自定义序列化方法 + * @return + */ + private RedisPlugin createRedisPlugin(String name, Integer dbIndex, ISerializer serializer) { + RedisPlugin redisPlugin = new RedisPlugin(name, "redis-host", 6379, 3000,"pwd",dbIndex); + redisPlugin.setSerializer(serializer); + return redisPlugin; + } + @Override + public void onStart(){ + //增加redis缓存,需要先配置redis地址 + SaManager.setSaTokenDao(new SaTokenDaoRedis("satoken")); + } +} diff --git a/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/StpInterfaceImpl.java b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/StpInterfaceImpl.java new file mode 100644 index 000000000..671d1b846 --- /dev/null +++ b/sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/StpInterfaceImpl.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.jfinal.test; + +import cn.dev33.satoken.stp.StpInterface; + +import java.util.ArrayList; +import java.util.List; + +public class StpInterfaceImpl implements StpInterface { + @Override + public List getPermissionList(Object o, String s) { + return null; + } + + @Override + public List getRoleList(Object o, String s) { + List list = new ArrayList(); + list.add("admin"); + list.add("super-admin"); + return list; + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/pom.xml b/sa-token-starter/sa-token-loveqq-boot-starter/pom.xml new file mode 100644 index 000000000..85cefc8c9 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + + sa-token-loveqq-boot-starter + sa-token-loveqq-boot-starter + jar + loveqq-framework integrate sa-token + + + 1.8 + + + + + + com.kfyty + loveqq-core + provided + + + + + com.kfyty + loveqq-mvc-core + provided + + + + + com.kfyty + loveqq-boot-starter-redisson + + + + + cn.dev33 + sa-token-jackson + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + + cn.dev33 + sa-token-redisson + + + + org.redisson + redisson + + + + + + + jakarta.servlet + jakarta.servlet-api + true + + + + + cn.dev33 + sa-token-sso + true + + + + + cn.dev33 + sa-token-oauth2 + true + + + + + cn.dev33 + sa-token-apikey + true + + + + + cn.dev33 + sa-token-sign + true + + + \ No newline at end of file diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanInject.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanInject.java new file mode 100644 index 000000000..e269f6d74 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanInject.java @@ -0,0 +1,275 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.http.SaHttpTemplate; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate; +import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; +import cn.dev33.satoken.json.SaJsonTemplate; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.listener.SaTokenListener; +import cn.dev33.satoken.log.SaLog; +import cn.dev33.satoken.loveqq.boot.support.SaPathMatcherHolder; +import cn.dev33.satoken.plugin.SaTokenPlugin; +import cn.dev33.satoken.plugin.SaTokenPluginHolder; +import cn.dev33.satoken.same.SaSameTemplate; +import cn.dev33.satoken.secure.totp.SaTotpTemplate; +import cn.dev33.satoken.serializer.SaSerializerTemplate; +import cn.dev33.satoken.stp.StpInterface; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook; +import cn.dev33.satoken.temp.SaTempTemplate; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.support.PatternMatcher; + +import java.util.List; + +/** + * 注入 Sa-Token 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +@Component +public class SaBeanInject { + + /** + * 组件注入 + *

    为确保 Log 组件正常打印,必须将 SaLog 和 SaTokenConfig 率先初始化

    + * + * @param log log 对象 + * @param saTokenConfig 配置对象 + */ + public SaBeanInject(@Autowired(required = false) SaLog log, + @Autowired(required = false) SaTokenConfig saTokenConfig, + @Autowired(required = false) SaTokenPluginHolder pluginHolder) { + if (log != null) { + SaManager.setLog(log); + } + + if (saTokenConfig != null) { + SaManager.setConfig(saTokenConfig); + } + + // 初始化 Sa-Token SPI 插件 + if (pluginHolder == null) { + pluginHolder = SaTokenPluginHolder.instance; + } + + pluginHolder.init(); + + SaTokenPluginHolder.instance = pluginHolder; + } + + /** + * 注入持久化Bean + * + * @param saTokenDao SaTokenDao对象 + */ + @Autowired(required = false) + public void setSaTokenDao(SaTokenDao saTokenDao) { + SaManager.setSaTokenDao(saTokenDao); + } + + /** + * 注入权限认证Bean + * + * @param stpInterface StpInterface对象 + */ + @Autowired(required = false) + public void setStpInterface(StpInterface stpInterface) { + SaManager.setStpInterface(stpInterface); + } + + /** + * 注入上下文Bean + * + * @param saTokenContext SaTokenContext对象 + */ + @Autowired(required = false) + public void setSaTokenContext(SaTokenContext saTokenContext) { + SaManager.setSaTokenContext(saTokenContext); + } + + /** + * 注入侦听器Bean + * + * @param listenerList 侦听器集合 + */ + @Autowired(required = false) + public void setSaTokenListener(List listenerList) { + SaTokenEventCenter.registerListenerList(listenerList); + } + + /** + * 注入自定义注解处理器 + * + * @param handlerList 自定义注解处理器集合 + */ + @Autowired(required = false) + public void setSaAnnotationHandler(List> handlerList) { + for (SaAnnotationHandlerInterface handler : handlerList) { + SaAnnotationStrategy.instance.registerAnnotationHandler(handler); + } + } + + /** + * 注入临时令牌验证模块 Bean + * + * @param saTempTemplate / + */ + @Autowired(required = false) + public void setSaTempTemplate(SaTempTemplate saTempTemplate) { + SaManager.setSaTempTemplate(saTempTemplate); + } + + /** + * 注入 Same-Token 模块 Bean + * + * @param saSameTemplate saSameTemplate对象 + */ + @Autowired(required = false) + public void setSaIdTemplate(SaSameTemplate saSameTemplate) { + SaManager.setSaSameTemplate(saSameTemplate); + } + + /** + * 注入 Sa-Token Http Basic 认证模块 + * + * @param saBasicTemplate saBasicTemplate对象 + */ + @Autowired(required = false) + public void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) { + SaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate; + } + + /** + * 注入 Sa-Token Http Digest 认证模块 + * + * @param saHttpDigestTemplate saHttpDigestTemplate 对象 + */ + @Autowired(required = false) + public void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) { + SaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate; + } + + /** + * 注入自定义的 JSON 转换器 Bean + * + * @param saJsonTemplate JSON 转换器 + */ + @Autowired(required = false) + public void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) { + SaManager.setSaJsonTemplate(saJsonTemplate); + } + + /** + * 注入自定义的 Http 转换器 Bean + * + * @param saHttpTemplate / + */ + @Autowired(required = false) + public void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) { + SaManager.setSaHttpTemplate(saHttpTemplate); + } + + /** + * 注入自定义的序列化器 Bean + * + * @param saSerializerTemplate 序列化器 + */ + @Autowired(required = false) + public void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) { + SaManager.setSaSerializerTemplate(saSerializerTemplate); + } + + /** + * 注入自定义的 TOTP 算法 Bean + * + * @param totpTemplate TOTP 算法类 + */ + @Autowired(required = false) + public void setSaTotpTemplate(SaTotpTemplate totpTemplate) { + SaManager.setSaTotpTemplate(totpTemplate); + } + + /** + * 注入自定义的 StpLogic + * + * @param stpLogic / + */ + @Autowired(required = false) + public void setStpLogic(StpLogic stpLogic) { + StpUtil.setStpLogic(stpLogic); + } + + /** + * 利用自动注入特性,获取Spring框架内部使用的路由匹配器 + * + * @param pathMatcher 要设置的 pathMatcher + */ + @Autowired(required = false) + public void setPathMatcher(PatternMatcher pathMatcher) { + SaPathMatcherHolder.setPathMatcher(pathMatcher); + } + + /** + * 注入自定义防火墙校验 hook 集合 + * + * @param hooks / + */ + @Autowired(required = false) + public void setSaFirewallCheckHooks(List hooks) { + for (SaFirewallCheckHook hook : hooks) { + SaFirewallStrategy.instance.registerHook(hook); + } + } + + /** + * 注入CORS 策略处理函数 + * + * @param corsHandle / + */ + @Autowired(required = false) + public void setCorsHandle(SaCorsHandleFunction corsHandle) { + SaStrategy.instance.corsHandle = corsHandle; + } + + /** + * 注入自定义插件集合 + * + * @param plugins / + */ + @Autowired(required = false) + public void setSaTokenPluginList(List plugins) { + for (SaTokenPlugin plugin : plugins) { + SaTokenPluginHolder.instance.installPlugin(plugin); + } + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanRegister.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanRegister.java new file mode 100644 index 000000000..5f130b277 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanRegister.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot; + +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoForRedisson; +import cn.dev33.satoken.loveqq.boot.context.path.ApplicationContextPathLoading; +import cn.dev33.satoken.loveqq.boot.filter.SaFirewallCheckFilter; +import cn.dev33.satoken.loveqq.boot.filter.SaTokenContextFilter; +import cn.dev33.satoken.loveqq.boot.filter.SaTokenCorsFilter; +import cn.dev33.satoken.loveqq.boot.support.SaPathMatcherHolder; +import cn.dev33.satoken.strategy.SaStrategy; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Import; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnBean; +import org.redisson.api.RedissonClient; + +/** + * 注册Sa-Token所需要的Bean + *

    Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖 + * + * @author click33 + */ +@Component +@Import(config = { + SaFirewallCheckFilter.class, + SaTokenContextFilter.class, + SaTokenCorsFilter.class +}) +public class SaBeanRegister { + + public SaBeanRegister() { + // 重写路由匹配算法 + SaStrategy.instance.routeMatcher = SaPathMatcherHolder::match; + } + + /** + * 获取配置Bean + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties("sa-token") + public SaTokenConfig getSaTokenConfig() { + return new SaTokenConfig(); + } + + /** + * redis dao 集成 + * + * @return {@link SaTokenDao} + */ + @Bean + @ConditionalOnBean(RedissonClient.class) + public SaTokenDao saTokenDao(RedissonClient redisson) { + return new SaTokenDaoForRedisson(redisson); + } + + /** + * 应用上下文路径加载器 + * + * @return / + */ + @Bean + public ApplicationContextPathLoading getApplicationContextPathLoading() { + return new ApplicationContextPathLoading(); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanInject.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanInject.java new file mode 100644 index 000000000..7d75c75b1 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanInject.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.apiKey; + +import cn.dev33.satoken.apikey.SaApiKeyManager; +import cn.dev33.satoken.apikey.config.SaApiKeyConfig; +import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; +import cn.dev33.satoken.apikey.template.SaApiKeyTemplate; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; + +/** + * 注入 Sa-Token API Key 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@Component +@ConditionalOnClass("cn.dev33.satoken.apikey.SaApiKeyManager") +public class SaApiKeyBeanInject { + /** + * 注入 API Key 配置对象 + * + * @param saApiKeyConfig 配置对象 + */ + @Autowired(required = false) + public void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) { + SaApiKeyManager.setConfig(saApiKeyConfig); + } + + /** + * 注入自定义的 API Key 模版方法 Bean + * + * @param apiKeyTemplate / + */ + @Autowired(required = false) + public void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) { + SaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate); + } + + /** + * 注入自定义的 API Key 数据加载器 Bean + * + * @param apiKeyDataLoader / + */ + @Autowired(required = false) + public void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) { + SaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanRegister.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanRegister.java new file mode 100644 index 000000000..0f8b74565 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanRegister.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.apiKey; + +import cn.dev33.satoken.apikey.config.SaApiKeyConfig; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; + +/** + * 注册 Sa-Token API Key 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@Component +@ConditionalOnClass("cn.dev33.satoken.apikey.SaApiKeyManager") +public class SaApiKeyBeanRegister { + /** + * 获取 API Key 配置对象 + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties("sa-token.api-key") + public SaApiKeyConfig getSaApiKeyConfig() { + return new SaApiKeyConfig(); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/SaReactorHolder.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/SaReactorHolder.java new file mode 100644 index 000000000..a5af364b7 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/SaReactorHolder.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.context; + +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.fun.SaRetGenericFunction; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; +import reactor.core.publisher.Mono; + +/** + * Reactor 上下文操作(异步),持有当前请求的 ServerWebExchange 全局引用 + * + * @author click33 + * @since 1.19.0 + */ +public class SaReactorHolder { + public static final String REQUEST_CONTEXT_ATTRIBUTE = "com.kfyty.loveqq.framework.web.mvc.reactor.request.support.RequestContextHolder.REQUEST_CONTEXT_ATTRIBUTE"; + public static final String RESPONSE_CONTEXT_ATTRIBUTE = "com.kfyty.loveqq.framework.web.mvc.reactor.request.support.ResponseContextHolder.REQUEST_CONTEXT_ATTRIBUTE"; + + /** + * 获取 Mono < ServerRequest > + * + * @return / + */ + public static Mono getRequest() { + return Mono.deferContextual(Mono::just).map(e -> e.get(REQUEST_CONTEXT_ATTRIBUTE)); + } + + /** + * 获取 Mono < ServerResponse > + * + * @return / + */ + public static Mono getResponse() { + return Mono.deferContextual(Mono::just).map(e -> e.get(RESPONSE_CONTEXT_ATTRIBUTE)); + } + + /** + * 将 ServerRequest/ServerResponse 写入到同步上下文中,并执行一段代码,执行完毕清除上下文 + * + * @return / + */ + public static Mono sync(SaRetGenericFunction fun) { + return Mono.deferContextual(ctx -> { + SaTokenContextModelBox prev = SaTokenContextUtil.setContext(ctx.get(REQUEST_CONTEXT_ATTRIBUTE), ctx.get(RESPONSE_CONTEXT_ATTRIBUTE)); + try { + return Mono.just(fun.run()); + } finally { + SaTokenContextUtil.clearContext(prev); + } + }); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/path/ApplicationContextPathLoading.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/path/ApplicationContextPathLoading.java new file mode 100644 index 000000000..f536cc988 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/path/ApplicationContextPathLoading.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.context.path; + +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.util.SaFoxUtil; +import com.kfyty.loveqq.framework.core.autoconfig.CommandLineRunner; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Value; + +/** + * 应用上下文路径加载器 + * + * @author click33 + * @since 1.37.0 + */ +public class ApplicationContextPathLoading implements CommandLineRunner { + @Value("${k.mvc.tomcat.contextPath:}") + private String contextPath; + + @Override + public void run(String... args) throws Exception { + + String routePrefix = ""; + + if (SaFoxUtil.isNotEmpty(contextPath)) { + if (!contextPath.startsWith("/")) { + contextPath = "/" + contextPath; + } + if (contextPath.endsWith("/")) { + contextPath = contextPath.substring(0, contextPath.length() - 1); + } + routePrefix += contextPath; + } + + if (SaFoxUtil.isNotEmpty(routePrefix) && !routePrefix.equals("/")) { + ApplicationInfo.routePrefix = routePrefix; + } + } +} \ No newline at end of file diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaFirewallCheckFilter.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaFirewallCheckFilter.java new file mode 100644 index 000000000..dc30ad4d8 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaFirewallCheckFilter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.filter; + +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.FirewallCheckException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.loveqq.boot.model.LoveqqSaRequest; +import cn.dev33.satoken.loveqq.boot.model.LoveqqSaResponse; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Order; +import com.kfyty.loveqq.framework.web.core.filter.Filter; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +/** + * 防火墙校验过滤器 (基于 loveqq-framework 统一 Filter,可以统一 servlet 和 reactor 配置) + * + * @author click33 + * @since 1.37.0 + */ +@Component +@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) +public class SaFirewallCheckFilter implements Filter { + + @Override + public Continue doFilter(ServerRequest request, ServerResponse response) { + LoveqqSaRequest saRequest = new LoveqqSaRequest(request); + LoveqqSaResponse saResponse = new LoveqqSaResponse(response); + SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); + try { + SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null); + } catch (StopMatchException ignored) { + // ignored + } catch (BackResultException e) { + SaTokenOperateUtil.writeResult(response, e.getMessage()); + return Continue.FALSE; + } catch (FirewallCheckException e) { + if (SaFirewallStrategy.instance.checkFailHandle == null) { + SaTokenOperateUtil.writeResult(response, e.getMessage()); + } else { + SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); + } + return Continue.FALSE; + } finally { + SaTokenContextUtil.clearContext(prev); + } + + // 更多异常则不处理,交由 Web 框架处理 + + // 向内执行 + return Continue.TRUE; + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaRequestFilter.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaRequestFilter.java new file mode 100644 index 000000000..de0bae460 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaRequestFilter.java @@ -0,0 +1,151 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.filter; + +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.filter.SaFilter; +import cn.dev33.satoken.filter.SaFilterAuthStrategy; +import cn.dev33.satoken.filter.SaFilterErrorStrategy; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.util.SaTokenConsts; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Order; +import com.kfyty.loveqq.framework.web.core.filter.Filter; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 全局鉴权过滤器 (基于 loveqq-framework 统一 Filter,可以统一 servlet 和 reactor 配置) + *

    + * 默认优先级为 -100,尽量保证在其它过滤器之前执行 + *

    + * + * @author click33 + * @since 1.19.0 + */ +@Order(SaTokenConsts.ASSEMBLY_ORDER) +public class SaRequestFilter implements SaFilter, Filter { + + // ------------------------ 设置此过滤器 拦截 & 放行 的路由 + + /** + * 拦截路由 + */ + public List includeList = new ArrayList<>(); + + /** + * 放行路由 + */ + public List excludeList = new ArrayList<>(); + + @Override + public SaRequestFilter addInclude(String... paths) { + includeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaRequestFilter addExclude(String... paths) { + excludeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaRequestFilter setIncludeList(List pathList) { + includeList = pathList; + return this; + } + + @Override + public SaRequestFilter setExcludeList(List pathList) { + excludeList = pathList; + return this; + } + + + // ------------------------ 钩子函数 + + /** + * 认证函数:每次请求执行 + */ + public SaFilterAuthStrategy auth = r -> { + }; + + /** + * 异常处理函数:每次[认证函数]发生异常时执行此函数 + */ + public SaFilterErrorStrategy error = e -> { + throw new SaTokenException(e); + }; + + /** + * 前置函数:在每次[认证函数]之前执行 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth + */ + public SaFilterAuthStrategy beforeAuth = r -> { + }; + + @Override + public SaRequestFilter setAuth(SaFilterAuthStrategy auth) { + this.auth = auth; + return this; + } + + @Override + public SaRequestFilter setError(SaFilterErrorStrategy error) { + this.error = error; + return this; + } + + @Override + public SaRequestFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + + // ------------------------ doFilter + + @Override + public Continue doFilter(ServerRequest request, ServerResponse response) { + SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); + try { + beforeAuth.run(null); + SaRouter.match(includeList).notMatch(excludeList).check(r -> auth.run(null)); + } catch (StopMatchException ignored) { + // ignored + } catch (BackResultException e) { + SaTokenOperateUtil.writeResult(response, e.getMessage()); + return Continue.FALSE; + } catch (Throwable e) { + SaTokenOperateUtil.writeResult(response, String.valueOf(error.run(e))); + return Continue.FALSE; + } finally { + SaTokenContextUtil.clearContext(prev); + } + + // 执行 + return Continue.TRUE; + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenContextFilter.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenContextFilter.java new file mode 100644 index 000000000..61f2c81e6 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenContextFilter.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.filter; + +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Order; +import com.kfyty.loveqq.framework.web.core.filter.Filter; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +/** + * SaTokenContext 上下文初始化过滤器 (基于 loveqq-framework 统一 Filter,可以统一 servlet 和 reactor 配置) + * + * @author click33 + * @since 1.42.0 + */ +@Component +@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) +public class SaTokenContextFilter implements Filter { + + @Override + public Continue doFilter(ServerRequest request, ServerResponse response) { + SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); + return Continue.ofTrue(() -> SaTokenContextUtil.clearContext(prev)); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenCorsFilter.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenCorsFilter.java new file mode 100644 index 000000000..e786d6e10 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenCorsFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.filter; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Order; +import com.kfyty.loveqq.framework.web.core.filter.Filter; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +/** + * CORS 跨域策略过滤器 (基于 loveqq-framework 统一 Filter,可以统一 servlet 和 reactor 配置) + * loveqq-framework 也有跨域过滤器,切勿同时配置 + * + * @author click33 + * @see com.kfyty.loveqq.framework.web.core.cors.CorsFilter + * @since 1.42.0 + */ +@Component +@Order(SaTokenConsts.CORS_FILTER_ORDER) +public class SaTokenCorsFilter implements Filter { + + @Override + public Continue doFilter(ServerRequest request, ServerResponse response) { + SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); + try { + SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); + SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); + } catch (StopMatchException ignored) { + // ignored + } catch (BackResultException e) { + SaTokenOperateUtil.writeResult(response, e.getMessage()); + return Continue.FALSE; + } finally { + SaTokenContextUtil.clearContext(prev); + } + return Continue.TRUE; + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/interceptor/SaInterceptor.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/interceptor/SaInterceptor.java new file mode 100644 index 000000000..7f6e44d27 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/interceptor/SaInterceptor.java @@ -0,0 +1,140 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.interceptor; + +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; +import cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; +import com.kfyty.loveqq.framework.web.core.interceptor.HandlerInterceptor; +import com.kfyty.loveqq.framework.web.core.route.HandlerMethodRoute; +import com.kfyty.loveqq.framework.web.core.route.Route; + +import java.lang.reflect.Method; + +/** + * Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力 + * + * @author click33 + * @since 1.31.0 + */ +public class SaInterceptor implements HandlerInterceptor { + /** + * 是否打开注解鉴权,配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 + */ + public boolean isAnnotation = true; + + /** + * 认证前置函数:在注解鉴权之前执行 + *

    参数:路由处理函数指针 + */ + public SaParamFunction beforeAuth = handler -> { + }; + + /** + * 认证函数:每次请求执行 + *

    参数:路由处理函数指针 + */ + public SaParamFunction auth = handler -> { + }; + + /** + * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 + */ + public SaInterceptor() { + } + + /** + * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 + * + * @param auth 认证函数,每次请求执行 + */ + public SaInterceptor(SaParamFunction auth) { + this.auth = auth; + } + + /** + * 设置是否打开注解鉴权:配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 + * + * @param isAnnotation / + * @return 对象自身 + */ + public SaInterceptor isAnnotation(boolean isAnnotation) { + this.isAnnotation = isAnnotation; + return this; + } + + /** + * 写入 [ 认证前置函数 ]: 在注解鉴权之前执行 + * + * @param beforeAuth / + * @return 对象自身 + */ + public SaInterceptor setBeforeAuth(SaParamFunction beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + /** + * 写入 [ 认证函数 ]: 每次请求执行 + * + * @param auth / + * @return 对象自身 + */ + public SaInterceptor setAuth(SaParamFunction auth) { + this.auth = auth; + return this; + } + + + // ----------------- 验证方法 ----------------- + + /** + * 每次请求之前触发的方法 + */ + @Override + public boolean preHandle(ServerRequest request, ServerResponse response, Route handler) { + SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); + try { + // 前置函数:在注解鉴权之前执行 + beforeAuth.run(handler); + + // 这里必须确保 handler 是 HandlerMethod 类型时,才能进行注解鉴权 + if (isAnnotation && handler instanceof HandlerMethodRoute) { + Method method = ((HandlerMethodRoute) handler).getMappedMethod(); + SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); + } + + // Auth 路由拦截鉴权校验 + auth.run(handler); + } catch (StopMatchException e) { + // StopMatchException 异常代表:停止匹配,进入Controller + } catch (BackResultException e) { + SaTokenOperateUtil.writeResult(response, e.getMessage()); + return false; + } finally { + SaTokenContextUtil.clearContext(prev); + } + + // 通过验证 + return true; + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaRequest.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaRequest.java new file mode 100644 index 000000000..431f517b3 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaRequest.java @@ -0,0 +1,116 @@ +package cn.dev33.satoken.loveqq.boot.model; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.util.SaFoxUtil; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +import java.net.HttpCookie; +import java.util.Collection; +import java.util.Map; + +/** + * 对 SaRequest 包装类的实现 + * + * @author kfyty725 + */ +public class LoveqqSaRequest implements SaRequest { + /** + * loveqq-framework 包装请求 + */ + private final ServerRequest request; + + public LoveqqSaRequest(ServerRequest request) { + this.request = request; + } + + @Override + public Object getSource() { + return request; + } + + @Override + public String getParam(String name) { + return request.getParameter(name); + } + + @Override + public Collection getParamNames() { + return request.getParameterNames(); + } + + @Override + public Map getParamMap() { + return request.getParameterMap(); + } + + @Override + public String getHeader(String name) { + return request.getHeader(name); + } + + @Override + public String getCookieValue(String name) { + HttpCookie cookie = request.getCookie(name); + return cookie == null ? null : cookie.getValue(); + } + + @Override + public String getCookieFirstValue(String name) { + HttpCookie[] cookies = request.getCookies(); + if (cookies != null) { + for (HttpCookie cookie : cookies) { + if (cookie != null && name.equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } + + @Override + public String getCookieLastValue(String name) { + String value = null; + HttpCookie[] cookies = request.getCookies(); + if (cookies != null) { + for (HttpCookie cookie : cookies) { + if (cookie != null && name.equals(cookie.getName())) { + value = cookie.getValue(); + } + } + } + return value; + } + + @Override + public String getRequestPath() { + return ApplicationInfo.cutPathPrefix(request.getRequestURI()); + } + + @Override + public String getUrl() { + String currDomain = SaManager.getConfig().getCurrDomain(); + if (!SaFoxUtil.isEmpty(currDomain)) { + return currDomain + this.getRequestPath(); + } + return request.getRequestURL(); + } + + @Override + public String getMethod() { + return request.getMethod(); + } + + @Override + public String getHost() { + return request.getHost(); + } + + @Override + public Object forward(String path) { + ServerResponse response = (ServerResponse) SaManager.getSaTokenContext().getResponse().getSource(); + return response.sendRedirect(path); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaResponse.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaResponse.java new file mode 100644 index 000000000..af5c7d139 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaResponse.java @@ -0,0 +1,48 @@ +package cn.dev33.satoken.loveqq.boot.model; + +import cn.dev33.satoken.context.model.SaResponse; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +/** + * 对 SaResponse 包装类的实现 + * + * @author kfyty725 + */ +public class LoveqqSaResponse implements SaResponse { + /** + * loveqq-framework 包装响应 + */ + private final ServerResponse response; + + public LoveqqSaResponse(ServerResponse response) { + this.response = response; + } + + @Override + public Object getSource() { + return response; + } + + @Override + public SaResponse setStatus(int sc) { + response.setStatus(sc); + return this; + } + + @Override + public SaResponse setHeader(String name, String value) { + response.setHeader(name, value); + return this; + } + + @Override + public SaResponse addHeader(String name, String value) { + response.addHeader(name, value); + return this; + } + + @Override + public Object redirect(String url) { + return response.sendRedirect(url); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaStorage.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaStorage.java new file mode 100644 index 000000000..835ace092 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaStorage.java @@ -0,0 +1,42 @@ +package cn.dev33.satoken.loveqq.boot.model; + +import cn.dev33.satoken.context.model.SaStorage; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; + +/** + * 对 SaStorage 包装类的实现 + * + * @author kfyty725 + */ +public class LoveqqSaStorage implements SaStorage { + /** + * loveqq-framework 包装请求 + */ + private final ServerRequest request; + + public LoveqqSaStorage(ServerRequest request) { + this.request = request; + } + + @Override + public Object getSource() { + return request; + } + + @Override + public Object get(String key) { + return request.getAttribute(key); + } + + @Override + public SaStorage set(String key, Object value) { + request.setAttribute(key, value); + return this; + } + + @Override + public SaStorage delete(String key) { + request.removeAttribute(key); + return this; + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanInject.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanInject.java new file mode 100644 index 000000000..dc5ef7447 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanInject.java @@ -0,0 +1,153 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.oauth2; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; +import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; +import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; +import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; +import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver; +import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; +import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; +import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +import cn.dev33.satoken.oauth2.template.SaOAuth2Template; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; + +import java.util.List; + + +// 小提示:如果你在 idea 中运行源码时出现异常:java: 程序包cn.dev33.satoken.oauth2不存在。 +// 在项目根目录进入 cmd,执行 mvn package 即可解决 + + +/** + * 注入 Sa-Token-OAuth2 所需要的组件 + * + * @author click33 + * @since 1.34.0 + */ +@Component +@ConditionalOnClass("cn.dev33.satoken.oauth2.SaOAuth2Manager") +public class SaOAuth2BeanInject { + /** + * 注入 OAuth2 配置对象 + * + * @param saOAuth2Config 配置对象 + */ + @Autowired(required = false) + public void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) { + SaOAuth2Manager.setServerConfig(saOAuth2Config); + } + + /** + * 注入 OAuth2 模板代码类 + * + * @param saOAuth2Template 模板代码类 + */ + @Autowired(required = false) + public void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) { + SaOAuth2Manager.setTemplate(saOAuth2Template); + } + + /** + * 注入 OAuth2 请求处理器 + * + * @param serverProcessor 请求处理器 + */ + @Autowired(required = false) + public void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) { + SaOAuth2ServerProcessor.instance = serverProcessor; + } + + /** + * 注入 OAuth2 数据加载器 + * + * @param dataLoader / + */ + @Autowired(required = false) + public void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) { + SaOAuth2Manager.setDataLoader(dataLoader); + } + + /** + * 注入 OAuth2 数据解析器 Bean + * + * @param dataResolver / + */ + @Autowired(required = false) + public void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) { + SaOAuth2Manager.setDataResolver(dataResolver); + } + + /** + * 注入 OAuth2 数据格式转换器 Bean + * + * @param dataConverter / + */ + @Autowired(required = false) + public void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) { + SaOAuth2Manager.setDataConverter(dataConverter); + } + + /** + * 注入 OAuth2 数据构建器 Bean + * + * @param dataGenerate / + */ + @Autowired(required = false) + public void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) { + SaOAuth2Manager.setDataGenerate(dataGenerate); + } + + /** + * 注入 OAuth2 数据持久 Bean + * + * @param dao / + */ + @Autowired(required = false) + public void setSaOAuth2Dao(SaOAuth2Dao dao) { + SaOAuth2Manager.setDao(dao); + } + + /** + * 注入自定义 scope 处理器 + * + * @param handlerList 自定义 scope 处理器集合 + */ + @Autowired(required = false) + public void setSaOAuth2ScopeHandler(List handlerList) { + for (SaOAuth2ScopeHandlerInterface handler : handlerList) { + SaOAuth2Strategy.instance.registerScopeHandler(handler); + } + } + + /** + * 注入自定义 grant_type 处理器 + * + * @param handlerList 自定义 grant_type 处理器集合 + */ + @Autowired(required = false) + public void setSaOAuth2GrantTypeHandlerInterface(List handlerList) { + for (SaOAuth2GrantTypeHandlerInterface handler : handlerList) { + SaOAuth2Strategy.instance.registerGrantTypeHandler(handler); + } + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanRegister.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanRegister.java new file mode 100644 index 000000000..8906389ac --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanRegister.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.oauth2; + +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; + +/** + * 注册 Sa-Token-OAuth2 所需要的Bean + * + * @author click33 + * @since 1.34.0 + */ +@Component +@ConditionalOnClass("cn.dev33.satoken.oauth2.SaOAuth2Manager") +public class SaOAuth2BeanRegister { + /** + * 获取 OAuth2 配置 Bean + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties("sa-token.oauth2-server") + public SaOAuth2ServerConfig getSaOAuth2Config() { + return new SaOAuth2ServerConfig(); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/package-info.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/package-info.java new file mode 100644 index 000000000..012dcc2f2 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * Sa-Token 集成 loveqq-framework 的各个组件 + */ +package cn.dev33.satoken.loveqq.boot; \ No newline at end of file diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanInject.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanInject.java new file mode 100644 index 000000000..c54e8ad88 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanInject.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.sign; + +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; +import cn.dev33.satoken.sign.template.SaSignTemplate; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; + +/** + * 注入 Sa-Token API 参数签名 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@Component +@ConditionalOnClass("cn.dev33.satoken.sign.SaSignManager") +public class SaSignBeanInject { + /** + * 注入 API 参数签名配置对象 + * + * @param saSignConfig 配置对象 + */ + @Autowired(required = false) + public void setSignConfig(SaSignConfig saSignConfig) { + SaSignManager.setConfig(saSignConfig); + } + + /** + * 注入 API 参数签名配置对象 + * + * @param signManyConfigWrapper 配置对象 + */ + @Autowired(required = false) + public void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) { + SaSignManager.setSignMany(signManyConfigWrapper.getSignMany()); + } + + /** + * 注入自定义的 参数签名 模版方法 Bean + * + * @param saSignTemplate 参数签名 Bean + */ + @Autowired(required = false) + public void setSaSignTemplate(SaSignTemplate saSignTemplate) { + SaSignManager.setSaSignTemplate(saSignTemplate); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanRegister.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanRegister.java new file mode 100644 index 000000000..90d19b6d8 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanRegister.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.sign; + +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; + +/** + * 注册 Sa-Token API 参数签名所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@Component +@ConditionalOnClass("cn.dev33.satoken.sign.SaSignManager") +public class SaSignBeanRegister { + /** + * 获取 API 参数签名配置对象 + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties("sa-token.sign") + public SaSignConfig getSaSignConfig() { + return new SaSignConfig(); + } + + /** + * 获取 API 参数签名 Many 配置对象 + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties("sa-token") + public SaSignManyConfigWrapper getSaSignManyConfigWrapper() { + return new SaSignManyConfigWrapper(); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanInject.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanInject.java new file mode 100644 index 000000000..5344c8ca9 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanInject.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.sso; + +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; + +/** + * 注入 Sa-Token SSO 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +@Component +@ConditionalOnClass("cn.dev33.satoken.sso.SaSsoManager") +public class SaSsoBeanInject { + /** + * 注入 Sa-Token SSO Server 端 配置类 + * + * @param serverConfig 配置对象 + */ + @Autowired(required = false) + public void setSaSsoServerConfig(SaSsoServerConfig serverConfig) { + SaSsoManager.setServerConfig(serverConfig); + } + + /** + * 注入 Sa-Token SSO Client 端 配置类 + * + * @param clientConfig 配置对象 + */ + @Autowired(required = false) + public void setSaSsoClientConfig(SaSsoClientConfig clientConfig) { + SaSsoManager.setClientConfig(clientConfig); + } + + /** + * 注入 SSO 模板代码类 (Server 端) + * + * @param ssoServerTemplate / + */ + @Autowired(required = false) + public void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) { + SaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate; + } + + /** + * 注入 SSO 模板代码类 (Client 端) + * + * @param ssoClientTemplate / + */ + @Autowired(required = false) + public void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) { + SaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate; + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanRegister.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanRegister.java new file mode 100644 index 000000000..e9d63a9ee --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanRegister.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.sso; + +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; +import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; +import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnMissingBean; + +/** + * 注册 Sa-Token SSO 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +@Component +@ConditionalOnClass("cn.dev33.satoken.sso.SaSsoManager") +public class SaSsoBeanRegister { + /** + * 获取 SSO Server 端 配置对象 + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties("sa-token.sso-server") + public SaSsoServerConfig getSaSsoServerConfig() { + return new SaSsoServerConfig(); + } + + /** + * 获取 SSO Client 端 配置对象 + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties("sa-token.sso-client") + public SaSsoClientConfig getSaSsoClientConfig() { + return new SaSsoClientConfig(); + } + + /** + * 获取 SSO Server 端 SaSsoServerTemplate + * + * @return / + */ + @Bean + @ConditionalOnMissingBean(SaSsoServerTemplate.class) + public SaSsoServerTemplate getSaSsoServerTemplate() { + return SaSsoServerProcessor.instance.ssoServerTemplate; + } + + /** + * 获取 SSO Client 端 SaSsoClientTemplate + * + * @return / + */ + @Bean + @ConditionalOnMissingBean(SaSsoClientTemplate.class) + public SaSsoClientTemplate getSaSsoClientTemplate() { + return SaSsoClientProcessor.instance.ssoClientTemplate; + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/support/SaPathMatcherHolder.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/support/SaPathMatcherHolder.java new file mode 100644 index 000000000..66958866b --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/support/SaPathMatcherHolder.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.support; + +import com.kfyty.loveqq.framework.core.support.AntPathMatcher; +import com.kfyty.loveqq.framework.core.support.PatternMatcher; + +/** + * 路由匹配工具类:持有 PathMatcher 全局引用,方便快捷的调用 PathMatcher 相关方法 + * + * @author click33 + * @since 1.34.0 + */ +public class SaPathMatcherHolder { + + private SaPathMatcherHolder() { + } + + /** + * 路由匹配器 + */ + public static PatternMatcher pathMatcher; + + /** + * 获取路由匹配器 + * + * @return 路由匹配器 + */ + public static PatternMatcher getPathMatcher() { + if (pathMatcher == null) { + pathMatcher = new AntPathMatcher(); + } + return pathMatcher; + } + + /** + * 写入路由匹配器 + * + * @param pathMatcher 路由匹配器 + */ + public static void setPathMatcher(PatternMatcher pathMatcher) { + SaPathMatcherHolder.pathMatcher = pathMatcher; + } + + /** + * 判断:指定路由匹配符是否可以匹配成功指定路径 + * + * @param pattern 路由匹配符 + * @param path 要匹配的路径 + * @return 是否匹配成功 + */ + public static boolean match(String pattern, String path) { + return getPathMatcher().matches(pattern, path); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenContextUtil.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenContextUtil.java new file mode 100644 index 000000000..0a99f94ce --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenContextUtil.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.utils; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaRetGenericFunction; +import cn.dev33.satoken.loveqq.boot.model.LoveqqSaRequest; +import cn.dev33.satoken.loveqq.boot.model.LoveqqSaResponse; +import cn.dev33.satoken.loveqq.boot.model.LoveqqSaStorage; +import com.kfyty.loveqq.framework.web.core.http.ServerRequest; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +/** + * SaTokenContext 上下文读写工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenContextUtil { + /** + * 写入当前上下文 + * 并返回当前的上下文,以支持 loveqq-framework 的 servlet/reactor 的统一配置 + * + * @param request / + * @param response / + */ + public static SaTokenContextModelBox setContext(ServerRequest request, ServerResponse response) { + SaTokenContextModelBox prev = SaTokenContextForThreadLocalStaff.getModelBoxOrNull(); + SaRequest req = new LoveqqSaRequest(request); + SaResponse res = new LoveqqSaResponse(response); + SaStorage stg = new LoveqqSaStorage(request); + SaManager.getSaTokenContext().setContext(req, res, stg); + return prev; + } + + /** + * 写入上下文对象, 并在执行函数后将其清除 + * + * @param request / + * @param response / + * @param fun / + */ + public static void setContext(ServerRequest request, ServerResponse response, SaFunction fun) { + SaTokenContextModelBox prev = setContext(request, response); + try { + fun.run(); + } finally { + clearContext(prev); + } + } + + /** + * 写入上下文对象, 并在执行函数后将其清除 + * + * @param request / + * @param response / + * @param fun / + * @param / + * @return / + */ + public static T setContext(ServerRequest request, ServerResponse response, SaRetGenericFunction fun) { + SaTokenContextModelBox prev = setContext(request, response); + try { + return fun.run(); + } finally { + clearContext(prev); + } + } + + /** + * 清除当前上下文 + * 并恢复之前的上下文,以支持 loveqq-framework 的 servlet/reactor 的统一配置 + */ + public static void clearContext(SaTokenContextModelBox prev) { + if (prev == null) { + SaManager.getSaTokenContext().clearContext(); + } else { + SaManager.getSaTokenContext().setContext(prev.getRequest(), prev.getResponse(), prev.getStorage()); + } + } + + /** + * 获取当前 ModelBox + * + * @return / + */ + public static SaTokenContextModelBox getModelBox() { + return SaManager.getSaTokenContext().getModelBox(); + } + + /** + * 获取当前 Request + * + * @return / + */ + public static ServerRequest getRequest() { + return (ServerRequest) getModelBox().getRequest().getSource(); + } + + /** + * 获取当前 Response + * + * @return / + */ + public static ServerResponse getResponse() { + return (ServerResponse) getModelBox().getResponse().getSource(); + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenOperateUtil.java b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenOperateUtil.java new file mode 100644 index 000000000..4c5574ee2 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenOperateUtil.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.loveqq.boot.utils; + +import cn.dev33.satoken.util.SaTokenConsts; +import com.kfyty.loveqq.framework.core.exception.ResolvableException; +import com.kfyty.loveqq.framework.web.core.http.ServerResponse; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * {@link ServerResponse} 操作工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenOperateUtil { + /** + * 写入结果到输出流 + * + * @param response / + * @param result / + */ + public static void writeResult(ServerResponse response, String result) { + // 写入输出流 + // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json + // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); + if (response.getContentType() == null) { + response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); + } + try (OutputStream out = response.getOutputStream()) { + out.write(result.getBytes(StandardCharsets.UTF_8)); + out.flush(); + } catch (IOException e) { + throw new ResolvableException(e); + } + } +} diff --git a/sa-token-starter/sa-token-loveqq-boot-starter/src/main/resources/META-INF/k.factories b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/resources/META-INF/k.factories new file mode 100644 index 000000000..c7370f8c3 --- /dev/null +++ b/sa-token-starter/sa-token-loveqq-boot-starter/src/main/resources/META-INF/k.factories @@ -0,0 +1,11 @@ +com.kfyty.loveqq.framework.core.autoconfig.annotation.EnableAutoConfiguration=\ + cn.dev33.satoken.loveqq.boot.SaBeanRegister,\ + cn.dev33.satoken.loveqq.boot.SaBeanInject,\ + cn.dev33.satoken.loveqq.boot.apiKey.SaApiKeyBeanRegister,\ + cn.dev33.satoken.loveqq.boot.apiKey.SaApiKeyBeanInject,\ + cn.dev33.satoken.loveqq.boot.oauth2.SaOAuth2BeanRegister,\ + cn.dev33.satoken.loveqq.boot.oauth2.SaOAuth2BeanInject,\ + cn.dev33.satoken.loveqq.boot.sign.SaSignBeanRegister,\ + cn.dev33.satoken.loveqq.boot.sign.SaSignBeanInject,\ + cn.dev33.satoken.loveqq.boot.sso.SaSsoBeanRegister,\ + cn.dev33.satoken.loveqq.boot.sso.SaSsoBeanInject diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/.gitignore b/sa-token-starter/sa-token-reactor-spring-boot-starter/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/pom.xml b/sa-token-starter/sa-token-reactor-spring-boot-starter/pom.xml index 5c37e57b5..92d0eba89 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/pom.xml +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/pom.xml @@ -7,57 +7,79 @@ cn.dev33 sa-token-starter - 1.24.0 + ${revision} + ../pom.xml jar sa-token-reactor-spring-boot-starter sa-token-reactor-spring-boot-starter springboot reactor integrate sa-token - + - cn.dev33 - sa-token-core - ${sa-token-version} - - + cn.dev33 + sa-token-core + + + org.springframework.boot spring-boot-starter - 2.0.0.RELEASE - compile + true - - - org.springframework + + + + org.springframework spring-web - 5.0.4.RELEASE - compile - - + true + + + io.projectreactor reactor-core - 3.1.4.RELEASE - compile + true + org.springframework.boot spring-boot-configuration-processor - 2.0.0.RELEASE true - - + + + + cn.dev33 + sa-token-spring-boot-webmvc-reactor-v2v3v4-common + + + + + cn.dev33 + sa-token-jackson + + + + + + + + cn.dev33 + sa-token-spring-boot2-dependencies + ${revision} + pom + import + + + + + diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java index 5c3105b59..636ba354e 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java @@ -1,48 +1,97 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.context; +import cn.dev33.satoken.fun.SaRetGenericFunction; import org.springframework.web.server.ServerWebExchange; - +import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; +import reactor.util.context.Context; +import reactor.util.context.ContextView; /** - * Reactor上下文操作 [异步] - * @author kong + * Reactor 上下文操作(异步),持有当前请求的 ServerWebExchange 全局引用 * + * @author click33 + * @since 1.19.0 */ public class SaReactorHolder { - + + /** + * ServerWebExchange key + */ + public static final String EXCHANGE_KEY = "SA_REACTOR_EXCHANGE_KEY"; + + /** + * WebFilterChain key + */ + public static final String CHAIN_KEY = "SA_REACTOR__CHAIN_KEY"; + + /** + * 在流式上下文写入 ServerWebExchange + * @param ctx 必填 + * @param exchange 必填 + * @param chain 非必填 + * @return / + */ + public static Context setContext(Context ctx, ServerWebExchange exchange, WebFilterChain chain) { + return ctx + .put(EXCHANGE_KEY, exchange) + .put(CHAIN_KEY, chain); + } + /** - * key + * 在流式上下文获取 ServerWebExchange + * @param ctx / + * @return / */ - public static final Class CONTEXT_KEY = ServerWebExchange.class; + public static ServerWebExchange getExchange(ContextView ctx) { + return ctx.get(EXCHANGE_KEY); + } /** - * chain_key + * 在流式上下文获取 WebFilterChain + * @param ctx / + * @return / */ - public static final String CHAIN_KEY = "WEB_FILTER_CHAIN_KEY"; - + public static WebFilterChain getChain(ContextView ctx) { + return ctx.get(CHAIN_KEY); + } + /** - * 获取上下文对象 - * @return see note + * 获取 Mono < ServerWebExchange > + * @return / */ - public static Mono getContext() { - // 从全局 Mono 获取 - return Mono.subscriberContext().map(ctx -> ctx.get(CONTEXT_KEY)); + public static Mono getMonoExchange() { + return Mono.deferContextual(ctx -> Mono.just(getExchange(ctx))); } - + /** - * 获取上下文对象, 并设置到同步上下文中 - * @return see note + * 将 exchange 写入到同步上下文中,并执行一段代码,执行完毕清除上下文 + * + * @return / */ - public static Mono getContextAndSetSync() { - // 从全局 Mono 获取 - return Mono.subscriberContext().map(ctx -> { - // 设置到sync中 - SaReactorSyncHolder.setContext(ctx.get(CONTEXT_KEY)); - return ctx.get(CONTEXT_KEY); - }).doFinally(r->{ - // 从sync中清除 - SaReactorSyncHolder.clearContext(); + public static Mono sync(SaRetGenericFunction fun) { + return Mono.deferContextual(ctx -> { + try { + SaReactorSyncHolder.setContext(ctx.get(EXCHANGE_KEY)); + return Mono.just(fun.run()); + } finally { + SaReactorSyncHolder.clearContext(); + } }); } diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java index 1c4189463..da53f3a6e 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java @@ -1,63 +1,78 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.context; -import org.springframework.web.server.ServerWebExchange; - -import cn.dev33.satoken.context.SaTokenContextForThreadLocalStorage; -import cn.dev33.satoken.context.SaTokenContextForThreadLocalStorage.Box; +import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; -import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.fun.SaRetGenericFunction; import cn.dev33.satoken.reactor.model.SaRequestForReactor; import cn.dev33.satoken.reactor.model.SaResponseForReactor; import cn.dev33.satoken.reactor.model.SaStorageForReactor; +import org.springframework.web.server.ServerWebExchange; /** - * Reactor上下文操作 [同步] - * @author kong + * Reactor上下文操作(同步),持有当前请求的 ServerWebExchange 全局引用 * + * @author click33 + * @since 1.19.0 */ public class SaReactorSyncHolder { - + /** - * 写入上下文对象 - * @param exchange see note + * 在同步上下文写入 ServerWebExchange + * @param exchange / */ public static void setContext(ServerWebExchange exchange) { SaRequest request = new SaRequestForReactor(exchange.getRequest()); SaResponse response = new SaResponseForReactor(exchange.getResponse()); SaStorage storage = new SaStorageForReactor(exchange); - SaTokenContextForThreadLocalStorage.setBox(request, response, storage); + SaManager.getSaTokenContext().setContext(request, response, storage); } - + /** - * 获取上下文对象 - * @return see note + * 在同步上下文清除 ServerWebExchange */ - public static ServerWebExchange getContext() { - Box box = SaTokenContextForThreadLocalStorage.getBoxNotNull(); - return (ServerWebExchange)box.getStorage().getSource(); + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); } - + /** - * 清除上下文对象 + * 在同步上下文获取 ServerWebExchange + * @return / */ - public static void clearContext() { - SaTokenContextForThreadLocalStorage.clearBox(); + public static ServerWebExchange getExchange() { + SaTokenContextModelBox box = SaManager.getSaTokenContext().getModelBox(); + return (ServerWebExchange)box.getStorage().getSource(); } - + /** - * 写入上下文对象, 并在执行函数后将其清除 - * @param exchange see note - * @param fun see note + * 将 exchange 写入到同步上下文中,并执行一段代码,执行完毕清除上下文 + * @param exchange / + * @param fun / */ - public static void setContext(ServerWebExchange exchange, SaFunction fun) { + public static R setContext(ServerWebExchange exchange, SaRetGenericFunction fun) { try { setContext(exchange); - fun.run(); + return fun.run(); } finally { clearContext(); } } - + } diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaFirewallCheckFilterForReactor.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaFirewallCheckFilterForReactor.java new file mode 100644 index 000000000..cc97e5e03 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaFirewallCheckFilterForReactor.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.filter; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.FirewallCheckException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.reactor.model.SaRequestForReactor; +import cn.dev33.satoken.reactor.model.SaResponseForReactor; +import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * 防火墙校验过滤器 (Reactor版) + * + * @author click33 + * @since 1.37.0 + */ +@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) +public class SaFirewallCheckFilterForReactor implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + SaRequestForReactor saRequest = new SaRequestForReactor(exchange.getRequest()); + SaResponseForReactor saResponse = new SaResponseForReactor(exchange.getResponse()); + + try { + SaReactorSyncHolder.setContext(exchange); + SaFirewallStrategy.instance.check.execute(saRequest, saResponse, exchange); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); + } + // FirewallCheckException 异常则交由异常处理策略处理 + catch (FirewallCheckException e) { + if(SaFirewallStrategy.instance.checkFailHandle == null) { + return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); + } else { + SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); + return Mono.empty(); + } + } + finally { + SaReactorSyncHolder.clearContext(); + } + // 更多异常则不处理,交由 Web 框架处理 + + // 向下执行 + return chain.filter(exchange); + } + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java index 4f33c3a18..9acdaf1bc 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java @@ -1,100 +1,87 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.filter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.springframework.core.annotation.Order; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; - import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.filter.SaFilter; import cn.dev33.satoken.filter.SaFilterAuthStrategy; import cn.dev33.satoken.filter.SaFilterErrorStrategy; -import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** - * Reactor全局过滤器 - * @author kong + * Reactor 全局鉴权过滤器 + *

    + * 默认优先级为 -100,尽量保证在其它过滤器之前执行 + *

    * + * @author click33 + * @since 1.34.0 */ @Order(SaTokenConsts.ASSEMBLY_ORDER) -public class SaReactorFilter implements WebFilter { +public class SaReactorFilter implements SaFilter, WebFilter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ - private List includeList = new ArrayList<>(); + public List includeList = new ArrayList<>(); /** * 放行路由 */ - private List excludeList = new ArrayList<>(); + public List excludeList = new ArrayList<>(); - /** - * 添加 [拦截路由] - * @param paths 路由 - * @return 对象自身 - */ + @Override public SaReactorFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } - - /** - * 添加 [放行路由] - * @param paths 路由 - * @return 对象自身 - */ + + @Override public SaReactorFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } - /** - * 写入 [拦截路由] 集合 - * @param pathList 路由集合 - * @return 对象自身 - */ + @Override public SaReactorFilter setIncludeList(List pathList) { includeList = pathList; return this; } - - /** - * 写入 [放行路由] 集合 - * @param pathList 路由集合 - * @return 对象自身 - */ + + @Override public SaReactorFilter setExcludeList(List pathList) { excludeList = pathList; return this; } - - /** - * 获取 [拦截路由] 集合 - * @return see note - */ - public List getIncludeList() { - return includeList; - } - - /** - * 获取 [放行路由] 集合 - * @return see note - */ - public List getExcludeList() { - return excludeList; - } // ------------------------ 钩子函数 @@ -112,91 +99,58 @@ public List getExcludeList() { }; /** - * 前置函数:在每次[认证函数]之前执行 + * 前置函数:在每次[认证函数]之前执行 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> {}; - /** - * 写入[认证函数]: 每次请求执行 - * @param auth see note - * @return 对象自身 - */ + @Override public SaReactorFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } - /** - * 写入[异常处理函数]:每次[认证函数]发生异常时执行此函数 - * @param error see note - * @return 对象自身 - */ + @Override public SaReactorFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } - /** - * 写入[前置函数]:在每次[认证函数]之前执行 - * @param beforeAuth see note - * @return 对象自身 - */ + @Override public SaReactorFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } - // ------------------------ filter @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - - // 写入WebFilterChain对象 - exchange.getAttributes().put(SaReactorHolder.CHAIN_KEY, chain); - - // ---------- 全局认证处理 + + // ---------- 全局认证处理 try { - // 写入全局上下文 (同步) SaReactorSyncHolder.setContext(exchange); - - // 执行全局过滤器 - SaRouter.match(includeList, excludeList, () -> { - beforeAuth.run(null); - auth.run(null); - }); - - } catch (StopMatchException e) { - - } catch (Throwable e) { - // 1. 获取异常处理策略结果 - String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e)); - - // 2. 写入输出流 - if(exchange.getResponse().getHeaders().getFirst("Content-Type") == null) { - exchange.getResponse().getHeaders().set("Content-Type", "text/plain; charset=utf-8"); - } - return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes()))); - - } finally { - // 清除上下文 + beforeAuth.run(null); + SaRouter.match(includeList).notMatch(excludeList).check(r -> auth.run(null)); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); + } + catch (Throwable e) { + return SaReactorOperateUtil.writeResult(exchange, String.valueOf(error.run(e))); + } + finally { SaReactorSyncHolder.clearContext(); } - // ---------- 执行 - - // 写入全局上下文 (同步) - SaReactorSyncHolder.setContext(exchange); - - // 执行 - return chain.filter(exchange).subscriberContext(ctx -> { - // 写入全局上下文 (异步) - ctx = ctx.put(SaReactorHolder.CONTEXT_KEY, exchange); - return ctx; - }).doFinally(r -> { - // 清除上下文 - SaReactorSyncHolder.clearContext(); - }); + return chain.filter(exchange); } - } + +/* + * 三种 Filter : + * WebFilter: Spring WebFlux 的过滤器,用于拦截 Web 请求 + * GlobalFilter: Spring Cloud Gateway 的全局过滤器,用于拦截 Gateway 请求 + * GatewayFilter: Spring Cloud Gateway 的局部过滤器,用于拦截 Gateway 请求 + */ \ No newline at end of file diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenContextFilterForReactor.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenContextFilterForReactor.java new file mode 100644 index 000000000..c51088376 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenContextFilterForReactor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.filter; + +import cn.dev33.satoken.reactor.context.SaReactorHolder; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * SaTokenContext 上下文初始化过滤器 (基于 Reactor) + * + * @author click33 + * @since 1.42.0 + */ +@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) +public class SaTokenContextFilterForReactor implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return chain.filter(exchange) + .contextWrite(ctx -> SaReactorHolder.setContext(ctx, exchange, chain)) + .doFinally(r -> { + // 在流式上下文中保存的数据会随着流式操作的结束而销毁,所以此处无需手动清除数据 + }); + } + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenCorsFilterForReactor.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenCorsFilterForReactor.java new file mode 100644 index 000000000..4519d95fc --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenCorsFilterForReactor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.filter; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * CORS 跨域策略过滤器 (基于 Reactor) + * + * @author click33 + * @since 1.42.0 + */ +@Order(SaTokenConsts.CORS_FILTER_ORDER) +public class SaTokenCorsFilterForReactor implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + try { + SaReactorSyncHolder.setContext(exchange); + SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); + SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); + } + finally { + SaReactorSyncHolder.clearContext(); + } + + return chain.filter(exchange); + } + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java index 8733b324e..8d06d8bd3 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java @@ -1,26 +1,47 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.model; +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.reactor.context.SaReactorHolder; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.util.SaFoxUtil; import org.springframework.http.HttpCookie; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.reactor.context.SaReactorHolder; -import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import java.util.Collection; +import java.util.Map; /** - * Request for Reactor - * @author kong + * 对 SaRequest 包装类的实现(Reactor 响应式编程版) * + * @author click33 + * @since 1.34.0 */ public class SaRequestForReactor implements SaRequest { /** * 底层Request对象 */ - ServerHttpRequest request; + protected ServerHttpRequest request; /** * 实例化 @@ -46,6 +67,24 @@ public String getParam(String name) { return request.getQueryParams().getFirst(name); } + /** + * 获取 [请求体] 里提交的所有参数名称 + * @return 参数名称列表 + */ + @Override + public Collection getParamNames(){ + return request.getQueryParams().keySet(); + } + + /** + * 获取 [请求体] 里提交的所有参数 + * @return 参数列表 + */ + @Override + public Map getParamMap(){ + return request.getQueryParams().toSingleValueMap(); + } + /** * 在 [请求头] 里获取一个值 */ @@ -59,6 +98,14 @@ public String getHeader(String name) { */ @Override public String getCookieValue(String name) { + return getCookieLastValue(name); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name){ HttpCookie cookie = request.getCookies().getFirst(name); if(cookie == null) { return null; @@ -66,19 +113,53 @@ public String getCookieValue(String name) { return cookie.getValue(); } + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name){ + String value = null; + String cookieStr = getHeader("Cookie"); + if(SaFoxUtil.isNotEmpty(cookieStr)) { + String[] cookieItems = cookieStr.split(";"); + for (String item : cookieItems) { + String[] kv = item.split("="); + if (kv.length == 2) { + if (kv[0].trim().equals(name)) { + value = kv[1].trim(); + } + } + } + } + return value; + + // 此种写法无法获取到最后一个 Cookie,WebFlux 底层代码应该是有bug,前端提交多个同名Cookie时只能解析出第一个来 + // List cookies = request.getCookies().get(name); + // if(cookies.isEmpty()) { + // return null; + // } + // return cookies.get(cookies.size() - 1).getValue(); + } + /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { - return request.getURI().getPath(); + return ApplicationInfo.cutPathPrefix(request.getPath().toString()); } /** - * 返回当前请求的url,例:http://xxx.com/?id=127 + * 返回当前请求的url,例:http://xxx.com/test * @return see note */ public String getUrl() { + String currDomain = SaManager.getConfig().getCurrDomain(); + if( ! SaFoxUtil.isEmpty(currDomain)) { + return currDomain + this.getRequestPath(); + } return request.getURI().toString(); } @@ -87,7 +168,15 @@ public String getUrl() { */ @Override public String getMethod() { - return request.getMethodValue(); + return request.getMethod().name(); + } + + /** + * 查询请求 host + */ + @Override + public String getHost() { + return request.getURI().getHost(); } /** @@ -95,7 +184,7 @@ public String getMethod() { */ @Override public Object forward(String path) { - ServerWebExchange exchange = SaReactorSyncHolder.getContext(); + ServerWebExchange exchange = SaReactorSyncHolder.getExchange(); WebFilterChain chain = exchange.getAttribute(SaReactorHolder.CHAIN_KEY); ServerHttpRequest newRequest = request.mutate().path(path).build(); diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java index 4954e903a..ec1f52b17 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java @@ -1,26 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.model; import java.net.URI; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseCookie; -import org.springframework.http.ResponseCookie.ResponseCookieBuilder; import org.springframework.http.server.reactive.ServerHttpResponse; import cn.dev33.satoken.context.model.SaResponse; -import cn.dev33.satoken.util.SaFoxUtil; /** - * Response for Reactor - * @author kong + * 对 SaResponse 包装类的实现(Reactor 响应式编程版) * + * @author click33 + * @since 1.34.0 */ public class SaResponseForReactor implements SaResponse { /** * 底层Response对象 */ - ServerHttpResponse response; + protected ServerHttpResponse response; /** * 实例化 @@ -39,41 +52,14 @@ public Object getSource() { } /** - * 删除指定Cookie - */ - @Override - public void deleteCookie(String name) { - addCookie(name, null, null, null, 0); - } - - /** - * 写入指定Cookie + * 设置响应状态码 */ @Override - public void addCookie(String name, String value, String path, String domain, int timeout) { - - // 构建CookieBuilder - ResponseCookieBuilder builder = ResponseCookie.from(name, value) - .domain(domain) - .path(path) - .maxAge(timeout) - ; - - // set path - if(SaFoxUtil.isEmpty(path) == true) { - path = "/"; - } - builder.path(path); - - // set domain - if(SaFoxUtil.isEmpty(domain) == false) { - builder.domain(domain); - } - - // 写入Cookie - response.addCookie(builder.build()); + public SaResponse setStatus(int sc) { + response.setStatusCode(HttpStatus.valueOf(sc)); + return this; } - + /** * 在响应头里写入一个值 */ @@ -83,6 +69,17 @@ public SaResponse setHeader(String name, String value) { return this; } + /** + * 在响应头里添加一个值 + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + public SaResponse addHeader(String name, String value) { + response.getHeaders().add(name, value); + return this; + } + /** * 重定向 */ diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java index 266ded6b2..c609ee3a5 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.model; import org.springframework.web.server.ServerWebExchange; @@ -5,16 +20,17 @@ import cn.dev33.satoken.context.model.SaStorage; /** - * Storage for Reactor - * @author kong + * 对 SaStorage 包装类的实现(Reactor 响应式编程版) * + * @author click33 + * @since 1.34.0 */ public class SaStorageForReactor implements SaStorage { /** - * 底层Request对象 + * 底层 ServerWebExchange 对象 */ - ServerWebExchange exchange; + protected ServerWebExchange exchange; /** * 实例化 @@ -36,8 +52,9 @@ public Object getSource() { * 在 [Request作用域] 里写入一个值 */ @Override - public void set(String key, Object value) { + public SaStorageForReactor set(String key, Object value) { exchange.getAttributes().put(key, value); + return this; } /** @@ -52,8 +69,9 @@ public Object get(String key) { * 在 [Request作用域] 里删除一个值 */ @Override - public void delete(String key) { + public SaStorageForReactor delete(String key) { exchange.getAttributes().remove(key); + return this; } } diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/package-info.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/package-info.java index bfccfbad7..761e1274b 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/package-info.java +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/package-info.java @@ -1,4 +1,20 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ + /** - * sa-token集成Reactor响应式编程的各个组件 + * Sa-Token 集成 Reactor 响应式编程 (SpringBoot 2.x) */ -package cn.dev33.satoken.reactor; \ No newline at end of file +package cn.dev33.satoken.reactor; diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaBeanInject.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaBeanInject.java deleted file mode 100644 index 0eaf4cf32..000000000 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaBeanInject.java +++ /dev/null @@ -1,130 +0,0 @@ -package cn.dev33.satoken.reactor.spring; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.util.PathMatcher; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.action.SaTokenAction; -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.context.SaTokenContext; -import cn.dev33.satoken.dao.SaTokenDao; -import cn.dev33.satoken.id.SaIdTemplate; -import cn.dev33.satoken.id.SaIdUtil; -import cn.dev33.satoken.listener.SaTokenListener; -import cn.dev33.satoken.sso.SaSsoTemplate; -import cn.dev33.satoken.sso.SaSsoUtil; -import cn.dev33.satoken.stp.StpInterface; -import cn.dev33.satoken.temp.SaTempInterface; - -/** - * 利用spring的自动装配来加载开发者重写的Bean - * - * @author kong - * - */ -public class SaBeanInject { - - /** - * 注入配置Bean - * - * @param saTokenConfig 配置对象 - */ - @Autowired(required = false) - public void setConfig(SaTokenConfig saTokenConfig) { - SaManager.setConfig(saTokenConfig); - } - - /** - * 注入持久化Bean - * - * @param saTokenDao SaTokenDao对象 - */ - @Autowired(required = false) - public void setSaTokenDao(SaTokenDao saTokenDao) { - SaManager.setSaTokenDao(saTokenDao); - } - - /** - * 注入权限认证Bean - * - * @param stpInterface StpInterface对象 - */ - @Autowired(required = false) - public void setStpInterface(StpInterface stpInterface) { - SaManager.setStpInterface(stpInterface); - } - - /** - * 注入框架行为Bean - * - * @param saTokenAction SaTokenAction对象 - */ - @Autowired(required = false) - public void setSaTokenAction(SaTokenAction saTokenAction) { - SaManager.setSaTokenAction(saTokenAction); - } - - /** - * 注入容器交互Bean - * - * @param saTokenContext SaTokenContext对象 - */ - @Autowired(required = false) - public void setSaTokenContext(SaTokenContext saTokenContext) { - SaManager.setSaTokenContext(saTokenContext); - } - - /** - * 注入侦听器Bean - * - * @param saTokenListener saTokenListener对象 - */ - @Autowired(required = false) - public void setSaTokenListener(SaTokenListener saTokenListener) { - SaManager.setSaTokenListener(saTokenListener); - } - - /** - * 注入临时令牌验证模块 Bean - * - * @param saTemp saTemp对象 - */ - @Autowired(required = false) - public void setSaTemp(SaTempInterface saTemp) { - SaManager.setSaTemp(saTemp); - } - - /** - * 注入 Sa-Id-Token 模块 Bean - * - * @param saIdTemplate saIdTemplate对象 - */ - @Autowired(required = false) - public void setSaIdTemplate(SaIdTemplate saIdTemplate) { - SaIdUtil.saIdTemplate = saIdTemplate; - } - - /** - * 注入 Sa-Token-SSO 单点登录模块 Bean - * - * @param saSsoTemplate saSsoTemplate对象 - */ - @Autowired(required = false) - public void setSaSsoTemplate(SaSsoTemplate saSsoTemplate) { - SaSsoUtil.saSsoTemplate = saSsoTemplate; - } - - /** - * 利用自动注入特性,获取Spring框架内部使用的路由匹配器 - * - * @param pathMatcher 要设置的 pathMatcher - */ - @Autowired(required = false) - @Qualifier("mvcPathMatcher") - public void setPathMatcher(PathMatcher pathMatcher) { - SaPathMatcherHolder.setPathMatcher(pathMatcher); - } - - -} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaBeanRegister.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaBeanRegister.java deleted file mode 100644 index 7cdff979e..000000000 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaBeanRegister.java +++ /dev/null @@ -1,47 +0,0 @@ -package cn.dev33.satoken.reactor.spring; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; - -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.context.SaTokenContext; -import cn.dev33.satoken.context.SaTokenContextForThreadLocal; - -/** - * 注册Sa-Token所需要的Bean - *

    Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖 - * @author kong - * - */ -public class SaBeanRegister { - - /** - * 获取配置Bean - * - * @return 配置对象 - */ - @Bean - @ConfigurationProperties(prefix = "sa-token") - public SaTokenConfig getSaTokenConfig() { - return new SaTokenConfig(); - } - - /** - * 获取容器交互Bean (ThreadLocal版) - * - * @return 容器交互Bean (ThreadLocal版) - */ - @Bean - public SaTokenContext getSaTokenContext() { - return new SaTokenContextForThreadLocal() { - /** - * 重写路由匹配方法 - */ - @Override - public boolean matchPath(String pattern, String path) { - return SaPathMatcherHolder.getPathMatcher().match(pattern, path); - } - }; - } - -} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaHistoryVersionInject.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaHistoryVersionInject.java deleted file mode 100644 index 4ac8cb206..000000000 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaHistoryVersionInject.java +++ /dev/null @@ -1,62 +0,0 @@ -package cn.dev33.satoken.reactor.spring; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.env.OriginTrackedMapPropertySource; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertySource; - -import cn.dev33.satoken.util.SaTokenConsts; - -/** - * 兼容旧版本的配置信息注入 - *

    目前已处理: - *

    1. yml配置前缀 [spring.sa-token.] 更改为 [sa-token.] - * @author kong - */ -//SpringBoot 1.5.x 版本下不存在 OriginTrackedMapPropertySource ,不加这个注解会导致项目无法启动 -@ConditionalOnClass(OriginTrackedMapPropertySource.class) -public class SaHistoryVersionInject implements EnvironmentAware{ - - @Override - @SuppressWarnings("unchecked") - public void setEnvironment(Environment env) { - try { - ConfigurableEnvironment c = (ConfigurableEnvironment) env; - MutablePropertySources sources = c.getPropertySources(); - - // 将yml中所有 [spring.sa-token.] 开头的配置转移到 [sa-token.] 下 - Map newMap = new LinkedHashMap(); - for (PropertySource source : sources) { - // 根据Name开头单词判断是否为 SpringBoot .yml 或者.properties 的配置 - if(source.getName().startsWith("applicationConfig")) { - Map bootProp = (Map)source.getSource(); - for (String key : bootProp.keySet()) { - if(key != null && key.startsWith("spring.sa-token.")) { - String newKey = key.substring(7); - newMap.put(newKey, bootProp.get(key)); - } - } - } - } - - // 追加到总配置里面 - if(newMap.size() > 0) { - System.err.println("\n" - + "Sa-Token Warning: 当前配置文件方式已过时,请更改配置前缀:原 [spring.sa-token.] 更改为 [sa-token.]\n" - + "Sa-Token Warning: 当前版本(" + SaTokenConsts.VERSION_NO + ")暂时向下兼容,未来版本可能会完全移除旧形式"); - OriginTrackedMapPropertySource source = new OriginTrackedMapPropertySource("SaHistoryVersionInjectProperty", newMap); - // 追加到末尾,优先级最低 - c.getPropertySources().addLast(source); - } - } catch (Exception e) { - // not handle - } - } - -} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaPathMatcherHolder.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaPathMatcherHolder.java deleted file mode 100644 index eee3f3c8b..000000000 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaPathMatcherHolder.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.dev33.satoken.reactor.spring; - -import org.springframework.util.AntPathMatcher; -import org.springframework.util.PathMatcher; - -/** - * - * @author kong - * - */ -public class SaPathMatcherHolder { - - /** - * 路由匹配器 - */ - public static PathMatcher pathMatcher; - - /** - * 获取路由匹配器 - * @return 路由匹配器 - */ - public static PathMatcher getPathMatcher() { - if(pathMatcher == null) { - pathMatcher = new AntPathMatcher(); - } - return pathMatcher; - } - - /** - * 写入路由匹配器 - * @param pathMatcher 路由匹配器 - */ - public static void setPathMatcher(PathMatcher pathMatcher) { - SaPathMatcherHolder.pathMatcher = pathMatcher; - } - -} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextForSpringReactor.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextForSpringReactor.java new file mode 100644 index 000000000..769716c1d --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextForSpringReactor.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.spring; + +import cn.dev33.satoken.context.SaTokenContextForThreadLocal; + +/** + *

    此为低版本(<1.42.0) 的上下文处理方案,仅做留档,如无必要请勿使用

    + * + * Sa-Token 上下文处理器 [ Spring Reactor 版本实现 ] ,基于 SaTokenContextForThreadLocal 定制 + * + * @author click33 + * @since 1.33.0 + */ +public class SaTokenContextForSpringReactor extends SaTokenContextForThreadLocal { + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java new file mode 100644 index 000000000..20c8b1698 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.spring; + +import cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor; +import cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor; +import cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor; +import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import org.springframework.context.annotation.Bean; + +/** + * 注册 Sa-Token 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenContextRegister { + + public SaTokenContextRegister() { + // 重写路由匹配算法 + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return SaPathPatternParserUtil.match(pattern, path); + }; + } + + /** + * 上下文过滤器 + * + * @return / + */ + @Bean + public SaTokenContextFilterForReactor saTokenContextFilterForServlet() { + return new SaTokenContextFilterForReactor(); + } + + /** + * CORS 跨域策略过滤器 + * + * @return / + */ + @Bean + public SaTokenCorsFilterForReactor saTokenCorsFilterForReactor() { + return new SaTokenCorsFilterForReactor(); + } + + /** + * 防火墙过滤器 + * + * @return / + */ + @Bean + public SaFirewallCheckFilterForReactor saFirewallCheckFilterForReactor() { + return new SaFirewallCheckFilterForReactor(); + } + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SpringBootVersionCompatibilityChecker.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SpringBootVersionCompatibilityChecker.java new file mode 100644 index 000000000..c8453a0ad --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SpringBootVersionCompatibilityChecker.java @@ -0,0 +1,26 @@ +package cn.dev33.satoken.reactor.spring; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.boot.SpringBootVersion; + +/** + * SpringBoot 版本与 Sa-Token 版本兼容检查器,当开发者错误的在 SpringBoot3/4.x 项目中引入当前集成包时,将在控制台做出提醒并阻断项目启动 + * + * @author Uncarbon + * @since 1.38.0 + */ +public class SpringBootVersionCompatibilityChecker { + + public SpringBootVersionCompatibilityChecker() { + String version = SpringBootVersion.getVersion(); + if (SaFoxUtil.isEmpty(version) || version.startsWith("1.") || version.startsWith("2.")) { + return; + } + String str = "当前 SpringBoot 版本(" + version + ")与 Sa-Token 依赖不兼容," + + "请将依赖 sa-token-reactor-spring-boot-starter 修改为:sa-token-reactor-spring-boot3/4-starter"; + System.err.println(str); + throw new SaTokenException(str); + } + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/util/SaReactorOperateUtil.java b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/util/SaReactorOperateUtil.java new file mode 100644 index 000000000..5ee4809e4 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/util/SaReactorOperateUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.util; + +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * Reactor 操作工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaReactorOperateUtil { + + /** + * 写入结果到输出流 + * @param exchange / + * @param result / + * @return / + */ + public static Mono writeResult(ServerWebExchange exchange, String result) { + // 写入输出流 + // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json + // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); + if(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) { + exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); + } + return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes()))); + } + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring.factories b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring.factories index cecda6838..7e7b9d4c4 100644 --- a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -1,4 +1,2 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -cn.dev33.satoken.reactor.spring.SaBeanRegister,\ -cn.dev33.satoken.reactor.spring.SaBeanInject,\ -cn.dev33.satoken.reactor.spring.SaHistoryVersionInject \ No newline at end of file +cn.dev33.satoken.reactor.spring.SaTokenContextRegister \ No newline at end of file diff --git a/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..53fa391ba --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.reactor.spring.SpringBootVersionCompatibilityChecker diff --git a/sa-token-starter/sa-token-reactor-spring-boot3-starter/pom.xml b/sa-token-starter/sa-token-reactor-spring-boot3-starter/pom.xml new file mode 100644 index 000000000..5b8088ec9 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot3-starter/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + jar + + sa-token-reactor-spring-boot3-starter + sa-token-reactor-spring-boot3-starter + springboot3 reactor integrate sa-token + + + + + + cn.dev33 + sa-token-spring-boot-reactor-v3v4-common + + + + + cn.dev33 + sa-token-jackson + + + + + + + + + + cn.dev33 + sa-token-spring-boot3-dependencies + ${revision} + pom + import + + + + + + diff --git a/sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java b/sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java new file mode 100644 index 000000000..babadd328 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor; + + +/** + * 占位符 + * + * @author click33 + * @since 1.45.0 + */ +public class Placeholder { + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..325771974 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.reactor.spring.SaTokenContextRegister \ No newline at end of file diff --git a/sa-token-starter/sa-token-reactor-spring-boot4-starter/pom.xml b/sa-token-starter/sa-token-reactor-spring-boot4-starter/pom.xml new file mode 100644 index 000000000..709a865f9 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot4-starter/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + jar + + sa-token-reactor-spring-boot4-starter + sa-token-reactor-spring-boot4-starter + springboot4 reactor integrate sa-token + + + + + + cn.dev33 + sa-token-spring-boot-reactor-v3v4-common + + + + + cn.dev33 + sa-token-jackson3 + + + + + + + + + + cn.dev33 + sa-token-spring-boot4-dependencies + ${revision} + pom + import + + + + + + diff --git a/sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java b/sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java new file mode 100644 index 000000000..babadd328 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor; + + +/** + * 占位符 + * + * @author click33 + * @since 1.45.0 + */ +public class Placeholder { + +} diff --git a/sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..36c89d2e3 --- /dev/null +++ b/sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.reactor.spring.SaTokenContextRegister diff --git a/sa-token-starter/sa-token-servlet/.gitignore b/sa-token-starter/sa-token-servlet/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-starter/sa-token-servlet/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-starter/sa-token-servlet/pom.xml b/sa-token-starter/sa-token-servlet/pom.xml index 38317c9c7..df2a68c80 100644 --- a/sa-token-starter/sa-token-servlet/pom.xml +++ b/sa-token-starter/sa-token-servlet/pom.xml @@ -7,26 +7,26 @@ cn.dev33 sa-token-starter - 1.24.0 + ${revision} + ../pom.xml jar sa-token-servlet sa-token-servlet - sa-token authentication by Sservlet API + sa-token authentication by Servlet API cn.dev33 sa-token-core - ${sa-token-version} - + + javax.servlet javax.servlet-api - 3.1.0 true diff --git a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java new file mode 100644 index 000000000..8684e0baa --- /dev/null +++ b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.error; + +/** + * 定义 sa-token-servlet 所有异常细分状态码 + * + * @author click33 + * @since 1.33.0 + */ +public interface SaServletErrorCode { + + /** 转发失败 */ + int CODE_20001 = 20001; + + /** 重定向失败 */ + int CODE_20002 = 20002; + +} diff --git a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java index 84e43f8ed..4159789b3 100644 --- a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java +++ b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java @@ -1,36 +1,58 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.model; -import java.io.IOException; +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.servlet.error.SaServletErrorCode; +import cn.dev33.satoken.util.SaFoxUtil; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.exception.SaTokenException; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; /** - * Request for Servlet - * @author kong + * 对 SaRequest 包装类的实现(Servlet 版) * + * @author click33 + * @since 1.19.0 */ public class SaRequestForServlet implements SaRequest { /** * 底层Request对象 */ - HttpServletRequest request; - + protected HttpServletRequest request; + /** * 实例化 - * @param request request对象 + * @param request request对象 */ public SaRequestForServlet(HttpServletRequest request) { this.request = request; } - + /** * 获取底层源对象 */ @@ -47,6 +69,31 @@ public String getParam(String name) { return request.getParameter(name); } + /** + * 获取 [请求体] 里提交的所有参数名称 + * @return 参数名称列表 + */ + @Override + public Collection getParamNames(){ + return Collections.list(request.getParameterNames()); + } + + /** + * 获取 [请求体] 里提交的所有参数 + * @return 参数列表 + */ + @Override + public Map getParamMap(){ + // 获取所有参数 + Map parameterMap = request.getParameterMap(); + Map map = new LinkedHashMap<>(parameterMap.size()); + for (String key : parameterMap.keySet()) { + String[] values = parameterMap.get(key); + map.put(key, values[0]); + } + return map; + } + /** * 在 [请求头] 里获取一个值 */ @@ -60,6 +107,14 @@ public String getHeader(String name) { */ @Override public String getCookieValue(String name) { + return getCookieLastValue(name); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name){ Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { @@ -71,19 +126,42 @@ public String getCookieValue(String name) { return null; } + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name){ + String value = null; + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie != null && name.equals(cookie.getName())) { + value = cookie.getValue(); + } + } + } + return value; + } + /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { - return request.getServletPath(); + return ApplicationInfo.cutPathPrefix(request.getRequestURI()); } /** - * 返回当前请求的url,例:http://xxx.com/?id=127 + * 返回当前请求的url,例:http://xxx.com/test * @return see note */ public String getUrl() { + String currDomain = SaManager.getConfig().getCurrDomain(); + if( ! SaFoxUtil.isEmpty(currDomain)) { + return currDomain + this.getRequestPath(); + } return request.getRequestURL().toString(); } @@ -95,6 +173,14 @@ public String getMethod() { return request.getMethod(); } + /** + * 查询请求 host + */ + @Override + public String getHost() { + return request.getServerName(); + } + /** * 转发请求 */ @@ -105,7 +191,7 @@ public Object forward(String path) { request.getRequestDispatcher(path).forward(request, response); return null; } catch (ServletException | IOException e) { - throw new SaTokenException(e); + throw new SaTokenException(e).setCode(SaServletErrorCode.CODE_20001); } } diff --git a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java index 8e611c735..7a34d64f8 100644 --- a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java +++ b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java @@ -1,25 +1,38 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.model; -import java.io.IOException; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; - import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.servlet.error.SaServletErrorCode; + +import javax.servlet.http.HttpServletResponse; /** - * Response for Servlet - * @author kong + * 对 SaResponse 包装类的实现(Servlet 版) * + * @author click33 + * @since 1.19.0 */ public class SaResponseForServlet implements SaResponse { /** * 底层Request对象 */ - HttpServletResponse response; + protected HttpServletResponse response; /** * 实例化 @@ -38,30 +51,14 @@ public Object getSource() { } /** - * 删除指定Cookie + * 设置响应状态码 */ @Override - public void deleteCookie(String name) { - addCookie(name, null, null, null, 0); - } - - /** - * 写入指定Cookie - */ - @Override - public void addCookie(String name, String value, String path, String domain, int timeout) { - Cookie cookie = new Cookie(name, value); - if(SaFoxUtil.isEmpty(path) == true) { - path = "/"; - } - if(SaFoxUtil.isEmpty(domain) == false) { - cookie.setDomain(domain); - } - cookie.setPath(path); - cookie.setMaxAge(timeout); - response.addCookie(cookie); + public SaResponse setStatus(int sc) { + response.setStatus(sc); + return this; } - + /** * 在响应头里写入一个值 */ @@ -71,6 +68,17 @@ public SaResponse setHeader(String name, String value) { return this; } + /** + * 在响应头里添加一个值 + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + public SaResponse addHeader(String name, String value) { + response.addHeader(name, value); + return this; + } + /** * 重定向 */ @@ -78,10 +86,11 @@ public SaResponse setHeader(String name, String value) { public Object redirect(String url) { try { response.sendRedirect(url); - } catch (IOException e) { - throw new SaTokenException(e); + } catch (Exception e) { + throw new SaTokenException(e).setCode(SaServletErrorCode.CODE_20002); } return null; } + } diff --git a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java index b5f57b1ef..c795fa26f 100644 --- a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java +++ b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java @@ -1,20 +1,36 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.model; -import javax.servlet.http.HttpServletRequest; - import cn.dev33.satoken.context.model.SaStorage; +import javax.servlet.http.HttpServletRequest; + /** - * Storage for Servlet - * @author kong + * 对 SaStorage 包装类的实现(Servlet 版) * + * @author click33 + * @since 1.19.0 */ public class SaStorageForServlet implements SaStorage { /** * 底层Request对象 */ - HttpServletRequest request; + protected HttpServletRequest request; /** * 实例化 @@ -36,8 +52,9 @@ public Object getSource() { * 在 [Request作用域] 里写入一个值 */ @Override - public void set(String key, Object value) { + public SaStorageForServlet set(String key, Object value) { request.setAttribute(key, value); + return this; } /** @@ -52,8 +69,9 @@ public Object get(String key) { * 在 [Request作用域] 里删除一个值 */ @Override - public void delete(String key) { + public SaStorageForServlet delete(String key) { request.removeAttribute(key); + return this; } } diff --git a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java index 306aeb695..5b373c45f 100644 --- a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java +++ b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java @@ -1,4 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ /** - * Sa-Token对接ServletAPI容器所需要的实现类接口包 + * Sa-Token对接 Servlet API 容器所需要的实现类接口包 */ package cn.dev33.satoken.servlet; \ No newline at end of file diff --git a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaServletOperateUtil.java b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaServletOperateUtil.java new file mode 100644 index 000000000..e9d795e69 --- /dev/null +++ b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaServletOperateUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.util; + +import cn.dev33.satoken.util.SaTokenConsts; + +import javax.servlet.ServletResponse; +import java.io.IOException; + +/** + * Servlet 操作工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaServletOperateUtil { + + /** + * 写入结果到输出流 + * @param response / + * @param result / + */ + public static void writeResult(ServletResponse response, String result) throws IOException { + // 写入输出流 + // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json + // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); + if(response.getContentType() == null) { + response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); + } + response.getWriter().print(result); + response.getWriter().flush(); + } + + +} diff --git a/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextServletUtil.java b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextServletUtil.java new file mode 100644 index 000000000..4d97b932a --- /dev/null +++ b/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextServletUtil.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.servlet.util; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaRetGenericFunction; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.servlet.model.SaResponseForServlet; +import cn.dev33.satoken.servlet.model.SaStorageForServlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * SaTokenContext 上下文读写工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenContextServletUtil { + + /** + * 写入当前上下文 + * @param request / + * @param response / + */ + public static void setContext(HttpServletRequest request, HttpServletResponse response) { + SaRequest req = new SaRequestForServlet(request); + SaResponse res = new SaResponseForServlet(response); + SaStorage stg = new SaStorageForServlet(request); + SaManager.getSaTokenContext().setContext(req, res, stg); + } + + /** + * 写入上下文对象, 并在执行函数后将其清除 + * @param request / + * @param response / + * @param fun / + */ + public static void setContext(HttpServletRequest request, HttpServletResponse response, SaFunction fun) { + try { + setContext(request, response); + fun.run(); + } finally { + clearContext(); + } + } + + /** + * 写入上下文对象, 并在执行函数后将其清除 + * + * @param request / + * @param response / + * @param fun / + * @return / + * @param / + */ + public static T setContext(HttpServletRequest request, HttpServletResponse response, SaRetGenericFunction fun) { + try { + setContext(request, response); + return fun.run(); + } finally { + clearContext(); + } + } + + /** + * 清除当前上下文 + */ + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); + } + + /** + * 获取当前 ModelBox + * @return / + */ + public static SaTokenContextModelBox getModelBox() { + return SaManager.getSaTokenContext().getModelBox(); + } + + /** + * 获取当前 Request + * @return / + */ + public static HttpServletRequest getRequest() { + return (HttpServletRequest) getModelBox().getRequest().getSource(); + } + + /** + * 获取当前 Response + * @return / + */ + public static HttpServletResponse getResponse() { + return (HttpServletResponse) getModelBox().getResponse().getSource(); + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/pom.xml b/sa-token-starter/sa-token-solon-plugin/pom.xml index 98fe5e493..4ffe72c74 100644 --- a/sa-token-starter/sa-token-solon-plugin/pom.xml +++ b/sa-token-starter/sa-token-solon-plugin/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-starter - 1.24.0 + ${revision} + ../pom.xml jar @@ -19,13 +20,73 @@ org.noear solon - 1.5.17 + provided cn.dev33 sa-token-core - ${sa-token-version} + + + + + cn.dev33 + sa-token-oauth2 + true + + + + + cn.dev33 + sa-token-sso + true + + + + + cn.dev33 + sa-token-apikey + true + + + + + cn.dev33 + sa-token-sign + true + + + + + org.noear + redisx + provided + + + + org.noear + snack3 + provided + + + + + org.redisson + redisson + provided + + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + provided diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java new file mode 100644 index 000000000..7c7fc29dc --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java @@ -0,0 +1,279 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.http.SaHttpTemplate; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate; +import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; +import cn.dev33.satoken.json.SaJsonTemplate; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.listener.SaTokenListener; +import cn.dev33.satoken.log.SaLog; +import cn.dev33.satoken.plugin.SaTokenPlugin; +import cn.dev33.satoken.plugin.SaTokenPluginHolder; +import cn.dev33.satoken.same.SaSameTemplate; +import cn.dev33.satoken.secure.totp.SaTotpTemplate; +import cn.dev33.satoken.serializer.SaSerializerTemplate; +import cn.dev33.satoken.stp.StpInterface; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook; +import cn.dev33.satoken.temp.SaTempTemplate; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.util.List; + +/** + * 注入 Sa-Token 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +@Configuration +public class SaBeanInject { + + /** + * 组件注入 + *

    为确保 Log 组件正常打印,必须将 SaLog 和 SaTokenConfig 率先初始化

    + * + * @param log log 对象 + * @param saTokenConfig 配置对象 + */ + public SaBeanInject( + @Inject(required = false) SaLog log, + @Inject(required = false) SaTokenConfig saTokenConfig, + @Inject(required = false) SaTokenPluginHolder pluginHolder + ) { + if (log != null) { + SaManager.setLog(log); + } + + if (saTokenConfig != null) { + SaManager.setConfig(saTokenConfig); + } + + // 初始化 Sa-Token SPI 插件 + if (pluginHolder == null) { + pluginHolder = SaTokenPluginHolder.instance; + } + pluginHolder.init(); + SaTokenPluginHolder.instance = pluginHolder; + } + + /** + * 注入持久化Bean + * + * @param saTokenDao SaTokenDao对象 + */ + @Condition(onBean = SaTokenDao.class) + @Bean + public void setSaTokenDao(SaTokenDao saTokenDao) { + SaManager.setSaTokenDao(saTokenDao); + } + + /** + * 注入权限认证Bean + * + * @param stpInterface StpInterface对象 + */ + @Condition(onBean = StpInterface.class) + @Bean + public void setStpInterface(StpInterface stpInterface) { + SaManager.setStpInterface(stpInterface); + } + + /** + * 注入上下文Bean + * + * @param saTokenContext SaTokenContext对象 + */ + @Condition(onBean = SaTokenContext.class) + @Bean + public void setSaTokenContext(SaTokenContext saTokenContext) { + SaManager.setSaTokenContext(saTokenContext); + } + + /** + * 注入侦听器Bean + * + * @param listenerList 侦听器集合 + */ + @Bean + public void setSaTokenListener(List listenerList) { + SaTokenEventCenter.registerListenerList(listenerList); + } + + /** + * 注入自定义注解处理器 + * + * @param handlerList 自定义注解处理器集合 + */ + @Bean + public void setSaAnnotationHandler(List> handlerList) { + for (SaAnnotationHandlerInterface handler : handlerList) { + SaAnnotationStrategy.instance.registerAnnotationHandler(handler); + } + } + + /** + * 注入临时令牌验证模块 Bean + * + * @param saTempTemplate / + */ + @Condition(onBean = SaTempTemplate.class) + @Bean + public void setSaTempTemplate(SaTempTemplate saTempTemplate) { + SaManager.setSaTempTemplate(saTempTemplate); + } + + /** + * 注入 Same-Token 模块 Bean + * + * @param saSameTemplate saSameTemplate对象 + */ + @Condition(onBean = SaSameTemplate.class) + @Bean + public void setSaIdTemplate(SaSameTemplate saSameTemplate) { + SaManager.setSaSameTemplate(saSameTemplate); + } + + /** + * 注入 Sa-Token Http Basic 认证模块 + * + * @param saBasicTemplate saBasicTemplate对象 + */ + @Condition(onBean = SaHttpBasicTemplate.class) + @Bean + public void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) { + SaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate; + } + + /** + * 注入 Sa-Token Http Digest 认证模块 + * + * @param saHttpDigestTemplate saHttpDigestTemplate 对象 + */ + @Condition(onBean = SaHttpDigestTemplate.class) + @Bean + public void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) { + SaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate; + } + + /** + * 注入自定义的 JSON 转换器 Bean + * + * @param saJsonTemplate JSON 转换器 + */ + @Condition(onBean = SaJsonTemplate.class) + @Bean + public void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) { + SaManager.setSaJsonTemplate(saJsonTemplate); + } + + /** + * 注入自定义的 Http 转换器 Bean + * + * @param saHttpTemplate Http 转换器 + */ + @Condition(onBean = SaHttpTemplate.class) + @Bean + public void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) { + SaManager.setSaHttpTemplate(saHttpTemplate); + } + + /** + * 注入自定义的序列化器 Bean + * + * @param saSerializerTemplate 序列化器 + */ + @Condition(onBean = SaSerializerTemplate.class) + @Bean + public void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) { + SaManager.setSaSerializerTemplate(saSerializerTemplate); + } + + /** + * 注入自定义的 TOTP 算法 Bean + * + * @param totpTemplate TOTP 算法类 + */ + @Condition(onBean = SaTotpTemplate.class) + @Bean + public void setSaTotpTemplate(SaTotpTemplate totpTemplate) { + SaManager.setSaTotpTemplate(totpTemplate); + } + + /** + * 注入自定义的 StpLogic + * + * @param stpLogic / + */ + @Condition(onBean = StpLogic.class) + @Bean + public void setStpLogic(StpLogic stpLogic) { + StpUtil.setStpLogic(stpLogic); + } + + /** + * 注入自定义防火墙校验 hook 集合 + * + * @param hooks / + */ + @Bean + public void setSaFirewallCheckHooks(List hooks) { + for (SaFirewallCheckHook hook : hooks) { + SaFirewallStrategy.instance.registerHook(hook); + } + } + + /** + * 注入CORS 策略处理函数 + * + * @param corsHandle / + */ + @Condition(onBean = SaCorsHandleFunction.class) + @Bean + public void setCorsHandle(SaCorsHandleFunction corsHandle) { + SaStrategy.instance.corsHandle = corsHandle; + } + + /** + * 注入自定义插件集合 + * + * @param plugins / + */ + @Bean + public void setSaTokenPluginList(List plugins) { + for (SaTokenPlugin plugin : plugins) { + SaTokenPluginHolder.instance.installPlugin(plugin); + } + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanRegister.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanRegister.java new file mode 100644 index 000000000..e26e38f4b --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanRegister.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon; + +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.solon.integration.SaFirewallCheckFilterForSolon; +import cn.dev33.satoken.solon.integration.SaTokenContextFilterForSolon; +import cn.dev33.satoken.solon.integration.SaTokenCorsFilterForSolon; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.handle.Filter; +import org.noear.solon.core.util.PathMatcher; + +/** + * 注册Sa-Token所需要的Bean + *

    Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖 + * @author click33 + * + */ +@Configuration +public class SaBeanRegister { + + public SaBeanRegister() { + // 重写路由匹配算法 + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return PathMatcher.get(pattern).matches(path); + }; + } + + /** + * 获取配置Bean + * + * @return 配置对象 + */ + @Bean + public SaTokenConfig getSaTokenConfig(@Inject(value = "${sa-token}", required = false) SaTokenConfig config) { + if (config == null) { + return new SaTokenConfig(); + } else { + return config; + } + } + + /** + * 上下文过滤器 + * + * @return / + */ + @Bean(index = SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) + public Filter saTokenContextFilterForSolon() { + return new SaTokenContextFilterForSolon(); + } + + /** + * CORS 跨域策略过滤器 + * + * @return / + */ + @Bean(index = SaTokenConsts.CORS_FILTER_ORDER) + public Filter saTokenCorsFilterForSolon() { + return new SaTokenCorsFilterForSolon(); + } + + /** + * 防火墙过滤器 + * + * @return / + */ + @Bean(index = SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) + public Filter saFirewallCheckFilterForSolon() { + return new SaFirewallCheckFilterForSolon(); + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaSolonPlugin.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaSolonPlugin.java new file mode 100644 index 000000000..f1b05d8f4 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaSolonPlugin.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon; + +import cn.dev33.satoken.solon.apikey.SaApiKeyBeanInject; +import cn.dev33.satoken.solon.apikey.SaApiKeyBeanRegister; +import cn.dev33.satoken.solon.oauth2.SaOAuth2BeanInject; +import cn.dev33.satoken.solon.oauth2.SaOAuth2BeanRegister; +import cn.dev33.satoken.solon.sign.SaSignBeanInject; +import cn.dev33.satoken.solon.sign.SaSignBeanRegister; +import cn.dev33.satoken.solon.sso.SaSsoBeanInject; +import cn.dev33.satoken.solon.sso.SaSsoBeanRegister; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear + * @since 1.4 + */ +public class SaSolonPlugin implements Plugin { + + @Override + public void start(AppContext context) { + // sa-token + context.beanMake(SaBeanRegister.class); + context.beanMake(SaBeanInject.class); + + // sa-sso + context.beanMake(SaSsoBeanRegister.class); + context.beanMake(SaSsoBeanInject.class); + + // sa-oauth2 + context.beanMake(SaOAuth2BeanRegister.class); + context.beanMake(SaOAuth2BeanInject.class); + + // sa-apikey + context.beanMake(SaApiKeyBeanRegister.class); + context.beanMake(SaApiKeyBeanInject.class); + + // sa-sign + context.beanMake(SaSignBeanRegister.class); + context.beanMake(SaSignBeanInject.class); + } +} \ No newline at end of file diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/XPluginImp.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/XPluginImp.java deleted file mode 100644 index 3e1b93224..000000000 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/XPluginImp.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.dev33.satoken.solon; - -import org.noear.solon.Solon; -import org.noear.solon.SolonApp; -import org.noear.solon.core.Aop; -import org.noear.solon.core.Plugin; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.action.SaTokenAction; -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.config.SaTokenConfig; -import cn.dev33.satoken.dao.SaTokenDao; -import cn.dev33.satoken.id.SaIdTemplate; -import cn.dev33.satoken.id.SaIdUtil; -import cn.dev33.satoken.listener.SaTokenListener; -import cn.dev33.satoken.solon.integration.SaContextForSolon; -import cn.dev33.satoken.solon.integration.SaTokenMethodInterceptor; -import cn.dev33.satoken.sso.SaSsoTemplate; -import cn.dev33.satoken.sso.SaSsoUtil; -import cn.dev33.satoken.stp.StpInterface; -import cn.dev33.satoken.temp.SaTempInterface; - -/** - * @author noear - * @since 1.4 - */ -public class XPluginImp implements Plugin { - - @Override - public void start(SolonApp app) { - Aop.context().beanAroundAdd(SaCheckPermission.class, SaTokenMethodInterceptor.INSTANCE); - Aop.context().beanAroundAdd(SaCheckRole.class, SaTokenMethodInterceptor.INSTANCE); - Aop.context().beanAroundAdd(SaCheckLogin.class, SaTokenMethodInterceptor.INSTANCE); - Aop.context().beanAroundAdd(SaCheckSafe.class, SaTokenMethodInterceptor.INSTANCE); - - //集成初始化 - - //注入配置Bean - SaTokenConfig saTokenConfig = Solon.cfg().getBean("sa-token", SaTokenConfig.class); - SaManager.setConfig(saTokenConfig); - - //注入容器交互Bean - SaManager.setSaTokenContext(new SaContextForSolon()); - - // 注入侦听器 Bean - Aop.getAsyn(SaTokenListener.class, bw->{ - SaManager.setSaTokenListener(bw.raw()); - }); - - // 注入框架行为 Bean - Aop.getAsyn(SaTokenAction.class, bw->{ - SaManager.setSaTokenAction(bw.raw()); - }); - - // 注入权限认证 Bean - Aop.getAsyn(StpInterface.class, bw->{ - SaManager.setStpInterface(bw.raw()); - }); - - // 注入持久化 Bean - Aop.getAsyn(SaTokenDao.class, bw->{ - SaManager.setSaTokenDao(bw.raw()); - }); - - // 临时令牌验证模块 Bean - Aop.getAsyn(SaTempInterface.class, bw->{ - SaManager.setSaTemp(bw.raw()); - }); - - // Sa-Token-Id 身份凭证模块 Bean - Aop.getAsyn(SaIdTemplate.class, bw->{ - SaIdUtil.saIdTemplate = bw.raw(); - }); - - // Sa-Token-SSO 单点登录模块 Bean - Aop.getAsyn(SaSsoTemplate.class, bw->{ - SaSsoUtil.saSsoTemplate = bw.raw(); - }); - - } -} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanInject.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanInject.java new file mode 100644 index 000000000..049043d06 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanInject.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.apikey; + +import cn.dev33.satoken.apikey.SaApiKeyManager; +import cn.dev33.satoken.apikey.config.SaApiKeyConfig; +import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; +import cn.dev33.satoken.apikey.template.SaApiKeyTemplate; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 注入 Sa-Token API Key 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@Condition(onClass=SaApiKeyManager.class) +@Configuration +public class SaApiKeyBeanInject { + + /** + * 注入 API Key 配置对象 + * + * @param saApiKeyConfig 配置对象 + */ + @Bean + @Condition(onBean = SaApiKeyConfig.class) + public void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) { + SaApiKeyManager.setConfig(saApiKeyConfig); + } + + /** + * 注入自定义的 API Key 模版方法 Bean + * + * @param apiKeyTemplate / + */ + @Bean + @Condition(onBean = SaApiKeyTemplate.class) + public void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) { + SaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate); + } + + /** + * 注入自定义的 API Key 数据加载器 Bean + * + * @param apiKeyDataLoader / + */ + @Bean + @Condition(onBean = SaApiKeyDataLoader.class) + public void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) { + SaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader); + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanRegister.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanRegister.java new file mode 100644 index 000000000..c2e1d5dcd --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanRegister.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.apikey; + +import cn.dev33.satoken.apikey.SaApiKeyManager; +import cn.dev33.satoken.apikey.config.SaApiKeyConfig; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * 注册 Sa-Token API Key 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@Configuration +@Condition(onClass= SaApiKeyManager.class) +public class SaApiKeyBeanRegister { + + /** + * 获取 API Key 配置对象 + * @return 配置对象 + */ + @Bean + public SaApiKeyConfig getSaApiKeyConfig(@Inject(value = "${sa-token.api-key}", required = false) SaApiKeyConfig saApiKeyConfig) { + if (saApiKeyConfig == null) { + return new SaApiKeyConfig(); + } else { + return saApiKeyConfig; + } + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/error/SaSolonErrorCode.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/error/SaSolonErrorCode.java new file mode 100644 index 000000000..66a0fd79e --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/error/SaSolonErrorCode.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.error; + +/** + * 定义 sa-token-solon-plugin 所有异常细分状态码 + * + * @author click33 + * @since 2022-10-30 + */ +public interface SaSolonErrorCode { + + /** 默认的拦截器异常处理函数 */ + int CODE_20301 = 20301; + + /** 默认的 Filter 异常处理函数 */ + int CODE_20302 = 20302; + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaContextForSolon.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaContextForSolon.java deleted file mode 100644 index 4984d782d..000000000 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaContextForSolon.java +++ /dev/null @@ -1,48 +0,0 @@ -package cn.dev33.satoken.solon.integration; - -import cn.dev33.satoken.context.SaTokenContext; -import cn.dev33.satoken.context.model.SaRequest; -import cn.dev33.satoken.context.model.SaResponse; -import cn.dev33.satoken.context.model.SaStorage; -import cn.dev33.satoken.solon.model.SaRequestForSolon; -import cn.dev33.satoken.solon.model.SaResponseForSolon; -import cn.dev33.satoken.solon.model.SaStorageForSolon; -import org.noear.solon.core.util.PathAnalyzer; - -/** - * @author noear - * @since 1.4 - */ -public class SaContextForSolon implements SaTokenContext { - /** - * 获取当前请求的Request对象 - */ - @Override - public SaRequest getRequest() { - return new SaRequestForSolon(); - } - - /** - * 获取当前请求的Response对象 - */ - @Override - public SaResponse getResponse() { - return new SaResponseForSolon(); - } - - /** - * 获取当前请求的 [存储器] 对象 - */ - @Override - public SaStorage getStorage() { - return new SaStorageForSolon(); - } - - /** - * 校验指定路由匹配符是否可以匹配成功指定路径 - */ - @Override - public boolean matchPath(String pattern, String path) { - return PathAnalyzer.get(pattern).matches(path); - } -} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaFirewallCheckFilterForSolon.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaFirewallCheckFilterForSolon.java new file mode 100644 index 000000000..d9965fda9 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaFirewallCheckFilterForSolon.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.integration; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.FirewallCheckException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.solon.model.SaRequestForSolon; +import cn.dev33.satoken.solon.model.SaResponseForSolon; +import cn.dev33.satoken.solon.util.SaSolonOperateUtil; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Filter; +import org.noear.solon.core.handle.FilterChain; + +/** + * 防火墙校验过滤器 (基于 Solon) + * + * @author noear + * @since 1.41.0 + */ +public class SaFirewallCheckFilterForSolon implements Filter { + + @Override + public void doFilter(Context ctx, FilterChain chain) throws Throwable { + + SaRequestForSolon saRequest = new SaRequestForSolon(); + SaResponseForSolon saResponse = new SaResponseForSolon(); + + try { + SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaSolonOperateUtil.writeResult(ctx, e.getMessage()); + return; + } + catch (FirewallCheckException e) { + if(SaFirewallStrategy.instance.checkFailHandle == null) { + SaSolonOperateUtil.writeResult(ctx, e.getMessage()); + } else { + SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); + } + return; + } + // 更多异常则不处理,交由 Web 框架处理 + + chain.doFilter(ctx); + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenContextFilterForSolon.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenContextFilterForSolon.java new file mode 100644 index 000000000..82abf7f13 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenContextFilterForSolon.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.integration; + +import cn.dev33.satoken.solon.util.SaTokenContextSolonUtil; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Filter; +import org.noear.solon.core.handle.FilterChain; + +/** + * 上下文初始化过滤器 (基于 Solon) + * + * @author noear + * @since 1.42.0 + */ +public class SaTokenContextFilterForSolon implements Filter { + + @Override + public void doFilter(Context ctx, FilterChain chain) throws Throwable { + try { + SaTokenContextSolonUtil.setContext(ctx); + chain.doFilter(ctx); + } finally { + SaTokenContextSolonUtil.clearContext(); + } + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenCorsFilterForSolon.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenCorsFilterForSolon.java new file mode 100644 index 000000000..34a4c6ffc --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenCorsFilterForSolon.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.integration; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.solon.util.SaSolonOperateUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Filter; +import org.noear.solon.core.handle.FilterChain; + +/** + * CORS 跨域策略过滤器 (基于 Solon) + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenCorsFilterForSolon implements Filter { + + @Override + public void doFilter(Context ctx, FilterChain chain) throws Throwable { + + try { + SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); + SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaSolonOperateUtil.writeResult(ctx, e.getMessage()); + return; + } + + chain.doFilter(ctx); + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenFilter.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenFilter.java new file mode 100644 index 000000000..56087d9b4 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenFilter.java @@ -0,0 +1,210 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.integration; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.filter.SaFilter; +import cn.dev33.satoken.filter.SaFilterAuthStrategy; +import cn.dev33.satoken.filter.SaFilterErrorStrategy; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.solon.util.SaSolonOperateUtil; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import org.noear.solon.Solon; +import org.noear.solon.core.handle.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * sa-token 基于路由的过滤式鉴权(增加了注解的处理);使用优先级要低些 + *

    + * 对静态文件有处理效果 + *

    + * order: -100 (SaTokenInterceptor 和 SaTokenFilter 二选一;不要同时用) + * + * @author noear + * @since 1.10 + */ +public class SaTokenFilter implements SaFilter, Filter { //之所以改名,为了跟 SaTokenInterceptor 形成一对 + + /** + * 是否打开注解鉴权 + */ + public boolean isAnnotation = true; + + // ------------------------ 设置此过滤器 拦截 & 放行 的路由 + + /** + * 拦截路由 + */ + public List includeList = new ArrayList<>(); + + /** + * 放行路由 + */ + public List excludeList = new ArrayList<>(); + + @Override + public SaTokenFilter addInclude(String... paths) { + includeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaTokenFilter addExclude(String... paths) { + excludeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaTokenFilter setIncludeList(List pathList) { + includeList = pathList; + return this; + } + + @Override + public SaTokenFilter setExcludeList(List pathList) { + excludeList = pathList; + return this; + } + + /** + * 获取 [拦截路由] 集合 + * + * @return see note + */ + public List getIncludeList() { + return includeList; + } + + /** + * 获取 [放行路由] 集合 + * + * @return see note + */ + public List getExcludeList() { + return excludeList; + } + + // ------------------------ 钩子函数 + + /** + * 认证函数:每次请求执行 + */ + public SaFilterAuthStrategy auth = r -> { + }; + + /** + * 异常处理函数:每次[认证函数]发生异常时执行此函数 + */ + public SaFilterErrorStrategy error = e -> { + if (e instanceof SaTokenException) { + throw (SaTokenException) e; + } else { + throw new SaTokenException(e); + } + }; + + /** + * 前置函数:在每次[认证函数]之前执行 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth + */ + public SaFilterAuthStrategy beforeAuth = r -> { + }; + + @Override + public SaTokenFilter setAuth(SaFilterAuthStrategy auth) { + this.auth = auth; + return this; + } + + @Override + public SaTokenFilter setError(SaFilterErrorStrategy error) { + this.error = error; + return this; + } + + @Override + public SaTokenFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + + @Override + public void doFilter(Context ctx, FilterChain chain) throws Throwable { + try { + //查找当前主处理(在网关内用时,可直接获取缓存) + Handler mainHandler = ctx.mainHandler(); + if (mainHandler == null) { + mainHandler = Solon.app().router().matchMain(ctx); + } + + if (mainHandler instanceof Gateway) { + //支持网关处理 + Gateway gateway = (Gateway) mainHandler; + mainHandler = gateway.find(ctx); + } + + Action action = (mainHandler instanceof Action ? (Action) mainHandler : null); + + //1.执行前置处理(主要是一些跨域之类的) + if(beforeAuth != null) { + beforeAuth.run(mainHandler); + } + + //先路径过滤下(包括了静态文件) + Handler finalMainHandler = mainHandler; + SaRouter.match(includeList).notMatch(excludeList).check(r -> { + //2.执行注解处理 + if(authAnno(action)) { + //3.执行规则处理(如果没有被 @SaIgnore 忽略) + auth.run(finalMainHandler); + } + }); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaSolonOperateUtil.writeResult(ctx, e.getMessage()); + return; + } + catch (SaTokenException e) { + SaSolonOperateUtil.writeResult(ctx, error.run(e)); + return; + } + + chain.doFilter(ctx); + } + + private boolean authAnno(Action action) { + //2.验证注解处理 + if (isAnnotation && action != null) { + // 注解校验 + try{ + Method method = action.method().getMethod(); + SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); + } catch (StopMatchException ignored) { + return false; + } + } + + return true; + } +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenInterceptor.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenInterceptor.java new file mode 100644 index 000000000..75d428eaa --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenInterceptor.java @@ -0,0 +1,247 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.integration; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.filter.SaFilter; +import cn.dev33.satoken.filter.SaFilterAuthStrategy; +import cn.dev33.satoken.filter.SaFilterErrorStrategy; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.solon.util.SaSolonOperateUtil; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import org.noear.solon.core.handle.*; +import org.noear.solon.core.route.RouterInterceptor; +import org.noear.solon.core.route.RouterInterceptorChain; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * sa-token 基于路由的过滤式鉴权(增加了注解的处理);使用优先级要低些 + *

    + * 对静态文件无处理效果 + *

    + * order: -100 (SaTokenInterceptor 和 SaTokenFilter 二选一;不要同时用) + * + * @author noear + * @since 1.12 + */ +public class SaTokenInterceptor implements SaFilter, RouterInterceptor { + /** + * 是否打开注解鉴权 + */ + public boolean isAnnotation = true; + + // ------------------------ 设置此过滤器 拦截 & 放行 的路由 + + /** + * 拦截路由 + */ + protected List includeList = new ArrayList<>(); + + /** + * 放行路由 + */ + protected List excludeList = new ArrayList<>(); + + /** + * 添加 [拦截路由] + * + * @param paths 路由 + * @return 对象自身 + */ + @Override + public SaTokenInterceptor addInclude(String... paths) { + includeList.addAll(Arrays.asList(paths)); + return this; + } + + /** + * 添加 [放行路由] + * + * @param paths 路由 + * @return 对象自身 + */ + @Override + public SaTokenInterceptor addExclude(String... paths) { + excludeList.addAll(Arrays.asList(paths)); + return this; + } + + /** + * 写入 [拦截路由] 集合 + * + * @param pathList 路由集合 + * @return 对象自身 + */ + @Override + public SaTokenInterceptor setIncludeList(List pathList) { + includeList = pathList; + return this; + } + + /** + * 写入 [放行路由] 集合 + * + * @param pathList 路由集合 + * @return 对象自身 + */ + @Override + public SaTokenInterceptor setExcludeList(List pathList) { + excludeList = pathList; + return this; + } + + /** + * 获取 [拦截路由] 集合 + * + * @return see note + */ + public List getIncludeList() { + return includeList; + } + + /** + * 获取 [放行路由] 集合 + * + * @return see note + */ + public List getExcludeList() { + return excludeList; + } + + + // ------------------------ 钩子函数 + + /** + * 认证函数:每次请求执行 + */ + protected SaFilterAuthStrategy auth = r -> { + }; + + /** + * 异常处理函数:每次[认证函数]发生异常时执行此函数 + */ + protected SaFilterErrorStrategy error = e -> { + if (e instanceof SaTokenException) { + throw (SaTokenException) e; + } else { + throw new SaTokenException(e); + } + }; + + /** + * 前置函数:在每次[认证函数]之前执行 + */ + protected SaFilterAuthStrategy beforeAuth = r -> { + }; + + /** + * 写入[认证函数]: 每次请求执行 + * + * @param auth see note + * @return 对象自身 + */ + @Override + public SaTokenInterceptor setAuth(SaFilterAuthStrategy auth) { + this.auth = auth; + return this; + } + + /** + * 写入[异常处理函数]:每次[认证函数]发生异常时执行此函数 + * + * @param error see note + * @return 对象自身 + */ + @Override + public SaTokenInterceptor setError(SaFilterErrorStrategy error) { + this.error = error; + return this; + } + + /** + * 写入[前置函数]:在每次[认证函数]之前执行 + * + * @param beforeAuth see note + * @return 对象自身 + */ + @Override + public SaTokenInterceptor setBeforeAuth(SaFilterAuthStrategy beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + + @Override + public void doIntercept(Context ctx, Handler mainHandler, RouterInterceptorChain chain) throws Throwable { + try { + if (mainHandler instanceof Gateway) { + //支持网关处理 + Gateway gateway = (Gateway) mainHandler; + mainHandler = gateway.find(ctx); + } + + Action action = (mainHandler instanceof Action ? (Action) mainHandler : null); + + //1.执行前置处理(主要是一些跨域之类的) + if(beforeAuth != null) { + beforeAuth.run(mainHandler); + } + + //先路径过滤下(不包括静态文件) + Handler finalMainHandler = mainHandler; + SaRouter.match(includeList).notMatch(excludeList).check(r -> { + //2.执行注解处理 + if(authAnno(action)) { + //3.执行规则处理(如果没有被 @SaIgnore 忽略) + auth.run(finalMainHandler); + } + }); + + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaSolonOperateUtil.writeResult(ctx, e.getMessage()); + return; + } + catch (SaTokenException e) { + SaSolonOperateUtil.writeResult(ctx, error.run(e)); + return; + } + + chain.doIntercept(ctx, mainHandler); + } + + private boolean authAnno(Action action) { + //2.验证注解处理 + if (isAnnotation && action != null) { + // 注解校验 + try{ + Method method = action.method().getMethod(); + SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); + } catch (StopMatchException ignored) { + return false; + } + } + + return true; + } +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenMethodInterceptor.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenMethodInterceptor.java deleted file mode 100644 index d18e737c4..000000000 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenMethodInterceptor.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.dev33.satoken.solon.integration; - -import cn.dev33.satoken.SaManager; -import org.noear.solon.core.aspect.Interceptor; -import org.noear.solon.core.aspect.Invocation; - -/** - * @author noear - * @since 1.4 - */ -public class SaTokenMethodInterceptor implements Interceptor { - - public static final SaTokenMethodInterceptor INSTANCE = new SaTokenMethodInterceptor(); - - @Override - public Object doIntercept(Invocation inv) throws Throwable { - // 注解鉴权 - SaManager.getSaTokenAction().checkMethodAnnotation(inv.method().getMethod()); - - // 执行原有逻辑 - return inv.invoke(); - } -} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenPathFilter.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenPathFilter.java deleted file mode 100644 index 60c6233d5..000000000 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenPathFilter.java +++ /dev/null @@ -1,166 +0,0 @@ -package cn.dev33.satoken.solon.integration; - - -import cn.dev33.satoken.exception.BackResultException; -import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.exception.StopMatchException; -import cn.dev33.satoken.filter.SaFilterAuthStrategy; -import cn.dev33.satoken.filter.SaFilterErrorStrategy; -import cn.dev33.satoken.router.SaRouter; -import org.noear.solon.core.handle.Context; -import org.noear.solon.core.handle.Filter; -import org.noear.solon.core.handle.FilterChain; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * @author noear 2021/5/30 created - */ -public class SaTokenPathFilter implements Filter { - - // ------------------------ 设置此过滤器 拦截 & 放行 的路由 - - /** - * 拦截路由 - */ - private List includeList = new ArrayList<>(); - - /** - * 放行路由 - */ - private List excludeList = new ArrayList<>(); - - /** - * 添加 [拦截路由] - * @param paths 路由 - * @return 对象自身 - */ - public SaTokenPathFilter addInclude(String... paths) { - includeList.addAll(Arrays.asList(paths)); - return this; - } - - /** - * 添加 [放行路由] - * @param paths 路由 - * @return 对象自身 - */ - public SaTokenPathFilter addExclude(String... paths) { - excludeList.addAll(Arrays.asList(paths)); - return this; - } - - /** - * 写入 [拦截路由] 集合 - * @param pathList 路由集合 - * @return 对象自身 - */ - public SaTokenPathFilter setIncludeList(List pathList) { - includeList = pathList; - return this; - } - - /** - * 写入 [放行路由] 集合 - * @param pathList 路由集合 - * @return 对象自身 - */ - public SaTokenPathFilter setExcludeList(List pathList) { - excludeList = pathList; - return this; - } - - /** - * 获取 [拦截路由] 集合 - * @return see note - */ - public List getIncludeList() { - return includeList; - } - - /** - * 获取 [放行路由] 集合 - * @return see note - */ - public List getExcludeList() { - return excludeList; - } - - - // ------------------------ 钩子函数 - - /** - * 认证函数:每次请求执行 - */ - public SaFilterAuthStrategy auth = r -> {}; - - /** - * 异常处理函数:每次[认证函数]发生异常时执行此函数 - */ - public SaFilterErrorStrategy error = e -> { - throw new SaTokenException(e); - }; - - /** - * 前置函数:在每次[认证函数]之前执行 - */ - public SaFilterAuthStrategy beforeAuth = r -> {}; - - /** - * 写入[认证函数]: 每次请求执行 - * @param auth see note - * @return 对象自身 - */ - public SaTokenPathFilter setAuth(SaFilterAuthStrategy auth) { - this.auth = auth; - return this; - } - - /** - * 写入[异常处理函数]:每次[认证函数]发生异常时执行此函数 - * @param error see note - * @return 对象自身 - */ - public SaTokenPathFilter setError(SaFilterErrorStrategy error) { - this.error = error; - return this; - } - - /** - * 写入[前置函数]:在每次[认证函数]之前执行 - * @param beforeAuth see note - * @return 对象自身 - */ - public SaTokenPathFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { - this.beforeAuth = beforeAuth; - return this; - } - - - @Override - public void doFilter(Context ctx, FilterChain chain) throws Throwable { - try { - // 执行全局过滤器 - SaRouter.match(includeList, excludeList, () -> { - beforeAuth.run(null); - auth.run(null); - }); - - } catch (StopMatchException e) { - - } catch (Throwable e) { - // 1. 获取异常处理策略结果 - String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e)); - - // 2. 写入输出流 - ctx.contentType("text/plain; charset=utf-8"); - ctx.output(result); - return; - } - - // 执行 - chain.doFilter(ctx); - } -} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaContextForSolon.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaContextForSolon.java new file mode 100644 index 000000000..d6495eae9 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaContextForSolon.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.model; + +import cn.dev33.satoken.context.SaTokenContextForReadOnly; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import org.noear.solon.core.handle.Context; + +/** + *

    此为低版本(<1.42.0) 的上下文处理方案,基于 Solon 内部封装 Context.current() 读写上下文,仅做留档,如无必要请勿使用

    + * + * @author noear + * @since 1.4 + */ +public class SaContextForSolon implements SaTokenContextForReadOnly { + + /** + * 获取当前请求的Request对象 + */ + @Override + public SaRequest getRequest() { + return new SaRequestForSolon(); + } + + /** + * 获取当前请求的Response对象 + */ + @Override + public SaResponse getResponse() { + return new SaResponseForSolon(); + } + + /** + * 获取当前请求的 [存储器] 对象 + */ + @Override + public SaStorage getStorage() { + return new SaStorageForSolon(); + } + + /** + * 此上下文是否有效 + * @return / + */ + public boolean isValid() { + return Context.current() != null; + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaRequestForSolon.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaRequestForSolon.java index 183e5ed88..7fe355bc6 100644 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaRequestForSolon.java +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaRequestForSolon.java @@ -1,19 +1,42 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.model; +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.util.SaFoxUtil; import org.noear.solon.core.handle.Context; -import cn.dev33.satoken.context.model.SaRequest; +import java.util.Collection; +import java.util.Map; /** * @author noear * @since 1.4 */ public class SaRequestForSolon implements SaRequest { - - Context ctx; - - public SaRequestForSolon(){ - ctx = Context.current(); + + protected Context ctx; + + public SaRequestForSolon() { + this(Context.current()); + } + + public SaRequestForSolon(Context ctx) { + this.ctx = ctx; } @Override @@ -26,14 +49,48 @@ public String getParam(String s) { return ctx.param(s); } + @Override + public Collection getParamNames() { + return ctx.paramNames(); + } + + /** + * 获取 [请求体] 里提交的所有参数 + * + * @return 参数列表 + */ + @Override + public Map getParamMap() { + return ctx.paramMap().toValueMap(); + } + @Override public String getHeader(String s) { return ctx.header(s); } @Override - public String getCookieValue(String s) { - return ctx.cookie(s); + public String getCookieValue(String name) { + return getCookieLastValue(name); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name) { + return ctx.cookie(name); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name) { + return ctx.cookieMap().holder(name).getLastValue(); } @Override @@ -42,13 +99,27 @@ public String getRequestPath() { } @Override - public String getUrl() { - return ctx.url(); - } - + public String getUrl() { + String currDomain = SaManager.getConfig().getCurrDomain(); + if (!SaFoxUtil.isEmpty(currDomain)) { + return currDomain + this.getRequestPath(); + } + return ctx.url(); + } + @Override public String getMethod() { return ctx.method(); } + @Override + public String getHost() { + return ctx.uri().getHost(); + } + + @Override + public Object forward(String path) { + ctx.forward(path); + return null; + } } diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaResponseForSolon.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaResponseForSolon.java index d2511d1d8..a2716cf33 100644 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaResponseForSolon.java +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaResponseForSolon.java @@ -1,47 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.model; -import org.noear.solon.Utils; -import org.noear.solon.core.handle.Context; - import cn.dev33.satoken.context.model.SaResponse; +import org.noear.solon.core.handle.Context; /** * @author noear * @since 1.4 */ public class SaResponseForSolon implements SaResponse { - - Context ctx; - - public SaResponseForSolon() { - ctx = Context.current(); - } - - @Override - public Object getSource() { - return ctx; - } - - @Override - public void deleteCookie(String s) { - ctx.cookieRemove(s); - } - - @Override - public void addCookie(String name, String value, String path, String domain, int timeout) { - if (Utils.isNotEmpty(path)) { - path = "/"; - } - - ctx.cookieSet(name, value, domain, path, timeout); - } - - @Override - public SaResponse setHeader(String name, String value) { - ctx.headerSet(name, value); - return this; - } - + + protected Context ctx; + + public SaResponseForSolon() { + this(Context.current()); + } + + public SaResponseForSolon(Context ctx) { + this.ctx = ctx; + } + + @Override + public Object getSource() { + return ctx; + } + + @Override + public SaResponse setStatus(int sc) { + ctx.status(sc); + return this; + } + + @Override + public SaResponse setHeader(String name, String value) { + ctx.headerSet(name, value); + return this; + } + + /** + * 在响应头里添加一个值 + * + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + public SaResponse addHeader(String name, String value) { + ctx.headerAdd(name, value); + return this; + } + @Override public Object redirect(String url) { ctx.redirect(url); diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaStorageForSolon.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaStorageForSolon.java index 7fc04579b..1a36b7d01 100644 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaStorageForSolon.java +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaStorageForSolon.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.model; import cn.dev33.satoken.context.model.SaStorage; @@ -8,11 +23,15 @@ * @since 1.4 */ public class SaStorageForSolon implements SaStorage { - - Context ctx; + + protected Context ctx; public SaStorageForSolon() { - ctx = Context.current(); + this(Context.current()); + } + + public SaStorageForSolon(Context ctx) { + this.ctx = ctx; } @Override @@ -21,8 +40,9 @@ public Object getSource() { } @Override - public void set(String key, Object value) { + public SaStorageForSolon set(String key, Object value) { ctx.attrSet(key, value); + return this; } @Override @@ -31,7 +51,8 @@ public Object get(String key) { } @Override - public void delete(String key) { + public SaStorageForSolon delete(String key) { ctx.attrMap().remove(key); + return this; } } diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanInject.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanInject.java new file mode 100644 index 000000000..ecb434b94 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanInject.java @@ -0,0 +1,162 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.oauth2; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; +import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; +import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; +import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; +import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver; +import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; +import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; +import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +import cn.dev33.satoken.oauth2.template.SaOAuth2Template; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +import java.util.List; + + +// 小提示:如果你在 idea 中运行源码时出现异常:java: 程序包cn.dev33.satoken.oauth2不存在。 +// 在项目根目录进入 cmd,执行 mvn package 即可解决 + + +/** + * 注入 Sa-Token-OAuth2 所需要的组件 + * + * @author click33 + * @since 1.34.0 + */ +@Condition(onClass=SaOAuth2Manager.class) +@Configuration +public class SaOAuth2BeanInject { + + /** + * 注入 OAuth2 配置对象 + * + * @param saOAuth2Config 配置对象 + */ + @Condition(onBean = SaOAuth2ServerConfig.class) + @Bean + public void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) { + SaOAuth2Manager.setServerConfig(saOAuth2Config); + } + + /** + * 注入 OAuth2 模板代码类 + * + * @param saOAuth2Template 模板代码类 + */ + @Condition(onBean = SaOAuth2Template.class) + @Bean + public void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) { + SaOAuth2Manager.setTemplate(saOAuth2Template); + } + + /** + * 注入 OAuth2 请求处理器 + * + * @param serverProcessor 请求处理器 + */ + @Condition(onBean = SaOAuth2ServerProcessor.class) + @Bean + public void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) { + SaOAuth2ServerProcessor.instance = serverProcessor; + } + + /** + * 注入 OAuth2 数据加载器 + * + * @param dataLoader / + */ + @Condition(onBean = SaOAuth2DataLoader.class) + @Bean + public void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) { + SaOAuth2Manager.setDataLoader(dataLoader); + } + + /** + * 注入 OAuth2 数据解析器 Bean + * + * @param dataResolver / + */ + @Condition(onBean = SaOAuth2DataResolver.class) + @Bean + public void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) { + SaOAuth2Manager.setDataResolver(dataResolver); + } + + /** + * 注入 OAuth2 数据格式转换器 Bean + * + * @param dataConverter / + */ + @Condition(onBean = SaOAuth2DataConverter.class) + @Bean + public void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) { + SaOAuth2Manager.setDataConverter(dataConverter); + } + + /** + * 注入 OAuth2 数据构建器 Bean + * + * @param dataGenerate / + */ + @Condition(onBean = SaOAuth2DataGenerate.class) + @Bean + public void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) { + SaOAuth2Manager.setDataGenerate(dataGenerate); + } + + /** + * 注入 OAuth2 数据持久 Bean + * + * @param dao / + */ + @Condition(onBean = SaOAuth2Dao.class) + @Bean + public void setSaOAuth2Dao(SaOAuth2Dao dao) { + SaOAuth2Manager.setDao(dao); + } + + /** + * 注入自定义 scope 处理器 + * + * @param handlerList 自定义 scope 处理器集合 + */ + @Bean + public void setSaOAuth2ScopeHandler(List handlerList) { + for (SaOAuth2ScopeHandlerInterface handler : handlerList) { + SaOAuth2Strategy.instance.registerScopeHandler(handler); + } + } + + /** + * 注入自定义 grant_type 处理器 + * + * @param handlerList 自定义 grant_type 处理器集合 + */ + @Bean + public void setSaOAuth2GrantTypeHandlerInterface(List handlerList) { + for (SaOAuth2GrantTypeHandlerInterface handler : handlerList) { + SaOAuth2Strategy.instance.registerGrantTypeHandler(handler); + } + } +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanRegister.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanRegister.java new file mode 100644 index 000000000..52588e9bd --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanRegister.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.oauth2; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * 注册 Sa-Token-OAuth2 所需要的Bean + * + * @author click33 + * @since 1.34.0 + */ +@Condition(onClass=SaOAuth2Manager.class) +@Configuration +public class SaOAuth2BeanRegister { + + /** + * 获取 OAuth2 配置 Bean + * + * @return 配置对象 + */ + @Bean + public SaOAuth2ServerConfig getSaOAuth2Config(@Inject(value = "${sa-token.oauth2-server}", required = false) SaOAuth2ServerConfig serverConfig) { + if (serverConfig == null) { + return new SaOAuth2ServerConfig(); + } else { + return serverConfig; + } + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/package-info.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/package-info.java index 4b10a207d..2d61a7afe 100644 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/package-info.java +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/package-info.java @@ -1,4 +1,19 @@ /** * sa-token 集成 solon 的各个组件 */ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon; \ No newline at end of file diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanInject.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanInject.java new file mode 100644 index 000000000..932836fcf --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanInject.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.sign; + +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; +import cn.dev33.satoken.sign.template.SaSignTemplate; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 注入 Sa-Token API 参数签名 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@Configuration +@Condition(onClass= SaSignManager.class) +public class SaSignBeanInject { + + /** + * 注入 API 参数签名配置对象 + * + * @param saSignConfig 配置对象 + */ + @Bean + @Condition(onBean = SaSignConfig.class) + public void setSignConfig(SaSignConfig saSignConfig) { + SaSignManager.setConfig(saSignConfig); + } + + /** + * 注入 API 参数签名配置对象 + * + * @param signManyConfigWrapper 配置对象 + */ + @Bean + @Condition(onBean = SaSignManyConfigWrapper.class) + public void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) { + SaSignManager.setSignMany(signManyConfigWrapper.getSignMany()); + } + + /** + * 注入自定义的 参数签名 模版方法 Bean + * + * @param saSignTemplate 参数签名 Bean + */ + @Bean + @Condition(onBean = SaSignTemplate.class) + public void setSaSignTemplate(SaSignTemplate saSignTemplate) { + SaSignManager.setSaSignTemplate(saSignTemplate); + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanRegister.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanRegister.java new file mode 100644 index 000000000..7cb4c2c98 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanRegister.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.sign; + +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * 注册 Sa-Token API 参数签名所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@Configuration +@Condition(onClass= SaSignManager.class) +public class SaSignBeanRegister { + + /** + * 获取 API 参数签名配置对象 + * @return 配置对象 + */ + @Bean + public SaSignConfig getSaSignConfig(@Inject(value = "${sa-token.sign}", required = false) SaSignConfig saSignConfig) { + if (saSignConfig == null) { + return new SaSignConfig(); + } else { + return saSignConfig; + } + } + + /** + * 获取 API 参数签名 Many 配置对象 + * @return 配置对象 + */ + @Bean + public SaSignManyConfigWrapper getSaSignManyConfigWrapper(@Inject(value = "${sa-token}", required = false) SaSignManyConfigWrapper signManyConfigWrapper) { + if (signManyConfigWrapper == null) { + return new SaSignManyConfigWrapper(); + } else { + return signManyConfigWrapper; + } + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanInject.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanInject.java new file mode 100644 index 000000000..8288f354c --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanInject.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.sso; + +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 注入 Sa-Token SSO 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +@Condition(onClass=SaSsoManager.class) +@Configuration +public class SaSsoBeanInject { + + /** + * 注入 Sa-Token SSO Server 端 配置类 + * + * @param serverConfig 配置对象 + */ + @Condition(onBean = SaSsoServerConfig.class) + @Bean + public void setSaSsoServerConfig(SaSsoServerConfig serverConfig) { + SaSsoManager.setServerConfig(serverConfig); + } + + /** + * 注入 Sa-Token SSO Client 端 配置类 + * + * @param clientConfig 配置对象 + */ + @Condition(onBean = SaSsoClientConfig.class) + @Bean + public void setSaSsoClientConfig(SaSsoClientConfig clientConfig) { + SaSsoManager.setClientConfig(clientConfig); + } + + /** + * 注入 SSO 模板代码类 (Server 端) + * + * @param ssoServerTemplate / + */ + @Condition(onBean = SaSsoServerTemplate.class) + @Bean + public void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) { + SaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate; + } + + /** + * 注入 SSO 模板代码类 (Client 端) + * + * @param ssoClientTemplate / + */ + @Condition(onBean = SaSsoClientTemplate.class) + @Bean + public void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) { + SaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate; + } +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanRegister.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanRegister.java new file mode 100644 index 000000000..afce2a07d --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanRegister.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.sso; + +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * 注册 Sa-Token SSO 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +@Condition(onClass=SaSsoManager.class) +@Configuration +public class SaSsoBeanRegister { + + /** + * 获取 SSO Server 端 配置对象 + * + * @return 配置对象 + */ + @Bean + public SaSsoServerConfig getSaSsoServerConfig(@Inject(value = "${sa-token.sso-server}", required = false) SaSsoServerConfig serverConfig) { + if (serverConfig == null) { + return new SaSsoServerConfig(); + } else { + return serverConfig; + } + } + + /** + * 获取 SSO Client 端 配置对象 + * + * @return 配置对象 + */ + @Bean + public SaSsoClientConfig getSaSsoClientConfig(@Inject(value = "${sa-token.sso-client}", required = false) SaSsoClientConfig clientConfig) { + if (clientConfig == null) { + return new SaSsoClientConfig(); + } else { + return clientConfig; + } + } + + /** + * 获取 SSO Server 端 SaSsoServerTemplate + * + * @return / + */ + @Bean + @Condition(onMissingBean = SaSsoServerTemplate.class) + public SaSsoServerTemplate getSaSsoServerTemplate() { + return SaSsoServerProcessor.instance.ssoServerTemplate; + } + + /** + * 获取 SSO Client 端 SaSsoClientTemplate + * + * @return / + */ + @Bean + @Condition(onMissingBean = SaSsoClientTemplate.class) + public SaSsoClientTemplate getSaSsoClientTemplate() { + return SaSsoClientProcessor.instance.ssoClientTemplate; + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaSolonOperateUtil.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaSolonOperateUtil.java new file mode 100644 index 000000000..d023b9b0d --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaSolonOperateUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.util; + +import org.noear.solon.core.handle.Context; + +/** + * Solon 操作工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaSolonOperateUtil { + + /** + * 写入结果到输出流 + * @param ctx / + * @param result / + */ + public static void writeResult(Context ctx, Object result) throws Throwable { + if (result != null) { + ctx.render(result); + } + ctx.setHandled(true); + } + + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaTokenContextSolonUtil.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaTokenContextSolonUtil.java new file mode 100644 index 000000000..83b9d40c0 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaTokenContextSolonUtil.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.solon.util; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.fun.SaFunction; +import cn.dev33.satoken.fun.SaRetGenericFunction; +import cn.dev33.satoken.solon.model.SaRequestForSolon; +import cn.dev33.satoken.solon.model.SaResponseForSolon; +import cn.dev33.satoken.solon.model.SaStorageForSolon; +import org.noear.solon.core.handle.Context; + +/** + * SaTokenContext 上下文读写工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaTokenContextSolonUtil { + + /** + * 写入当前上下文 + */ + public static void setContext(Context ctx) { + SaRequest req = new SaRequestForSolon(ctx); + SaResponse res = new SaResponseForSolon(ctx); + SaStorage stg = new SaStorageForSolon(ctx); + SaManager.getSaTokenContext().setContext(req, res, stg); + } + + /** + * 写入上下文对象, 并在执行函数后将其清除 + * @param ctx / + * @param fun / + */ + public static void setContext(Context ctx, SaFunction fun) { + try { + setContext(ctx); + fun.run(); + } finally { + clearContext(); + } + } + + /** + * 写入上下文对象, 并在执行函数后将其清除 + * + * @param ctx / + * @param fun / + * @return / + * @param / + */ + public static T setContext(Context ctx, SaRetGenericFunction fun) { + try { + setContext(ctx); + return fun.run(); + } finally { + clearContext(); + } + } + + /** + * 清除当前上下文 + */ + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); + } + + /** + * 获取当前 ModelBox + * @return / + */ + public static SaTokenContextModelBox getModelBox() { + return SaManager.getSaTokenContext().getModelBox(); + } + + /** + * 获取当前 Context + * @return / + */ + public static Context getContext() { + return (Context) getModelBox().getStorage().getSource(); + } + +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/resources/META-INF/solon/cn.dev33.satoken.solon.properties b/sa-token-starter/sa-token-solon-plugin/src/main/resources/META-INF/solon/cn.dev33.satoken.solon.properties index 51d8fd9ae..ff1011b72 100644 --- a/sa-token-starter/sa-token-solon-plugin/src/main/resources/META-INF/solon/cn.dev33.satoken.solon.properties +++ b/sa-token-starter/sa-token-solon-plugin/src/main/resources/META-INF/solon/cn.dev33.satoken.solon.properties @@ -1 +1 @@ -solon.plugin=cn.dev33.satoken.solon.XPluginImp \ No newline at end of file +solon.plugin=cn.dev33.satoken.solon.SaSolonPlugin \ No newline at end of file diff --git a/sa-token-starter/sa-token-solon-plugin/src/test/java/demo/App.java b/sa-token-starter/sa-token-solon-plugin/src/test/java/demo/App.java new file mode 100644 index 000000000..ac68f579b --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/test/java/demo/App.java @@ -0,0 +1,12 @@ +package demo; + +import org.noear.solon.Solon; + +/** + * @author noear 2022/3/30 created + */ +public class App { + public static void main(String[] args) { + Solon.start(App.class, args); + } +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/test/java/demo/Config.java b/sa-token-starter/sa-token-solon-plugin/src/test/java/demo/Config.java new file mode 100644 index 000000000..891be892c --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/test/java/demo/Config.java @@ -0,0 +1,42 @@ +//package demo; +// +//import org.noear.solon.annotation.Bean; +//import org.noear.solon.annotation.Configuration; +//import org.noear.solon.core.handle.Filter; +// +//import cn.dev33.satoken.router.SaRouter; +//import cn.dev33.satoken.solon.integration.SaTokenPathFilter; +//import cn.dev33.satoken.stp.StpUtil; +// +///** +// * @author noear 2022/3/30 created +// */ +//@Configuration +//public class Config { +// +// @Bean +// public Filter saTokenFilter() { +// return new SaTokenPathFilter() +// // 指定 [拦截路由] 与 [放行路由] +// .addInclude("/**").addExclude("/favicon.ico") +// +// // 认证函数: 每次请求执行 +// .setAuth(s -> { +// SaRouter.match("/**", StpUtil::checkLogin); +// +// // 根据路由划分模块,不同模块不同鉴权 +// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); +// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); +// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); +// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); +// }) +// +// // 异常处理函数:每次认证函数发生异常时执行此函数 +// .setError(e -> { +// System.out.println("---------- sa全局异常 "); +// System.out.println(e.getMessage()); +// StpUtil.login(123); +// return e.getMessage(); +// }); +// } +//} diff --git a/sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/App.java b/sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/App.java new file mode 100644 index 000000000..8b9ff8b51 --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/App.java @@ -0,0 +1,12 @@ +package demo2; + +import org.noear.solon.Solon; + +/** + * @author noear 2022/3/30 created + */ +public class App { + public static void main(String[] args) { + Solon.start(App.class, args); + } +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/Config.java b/sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/Config.java new file mode 100644 index 000000000..b5292ae0a --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/Config.java @@ -0,0 +1,54 @@ +package demo2; + +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.stp.StpUtil; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.route.RouterInterceptor; + +/** + * @author noear 2022/7/11 created + */ +@Configuration +public class Config { +// @Bean +// public void saTokenPathInterceptor() { +// Solon.app().before(new SaTokenPathInterceptor() +// // 指定 [拦截路由] 与 [放行路由] +// .addInclude("/**").addExclude("/favicon.ico") +// +// // 认证函数: 每次请求执行 +// .setAuth(s -> { +// SaRouter.match("/**", StpUtil::checkLogin); +// +// // 根据路由划分模块,不同模块不同鉴权 +// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); +// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); +// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); +// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); +// }) +// +// // 异常处理函数:每次认证函数发生异常时执行此函数 +// .setError(e -> { +// System.out.println("---------- sa全局异常 "); +// System.out.println(e.getMessage()); +// StpUtil.login(123); +// return e.getMessage(); +// }) +// ); +// } + + @Bean + public RouterInterceptor saTokenPathInterceptor2() { + return (ctx, mainHandler, chain) -> { + SaRouter.match("/**", StpUtil::checkLogin); + // 根据路由划分模块,不同模块不同鉴权 + SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); + SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); + SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); + SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); + + chain.doIntercept(ctx, mainHandler); + }; + } +} diff --git a/sa-token-starter/sa-token-solon-plugin/src/test/resources/app.yml b/sa-token-starter/sa-token-solon-plugin/src/test/resources/app.yml new file mode 100644 index 000000000..4e6c1939a --- /dev/null +++ b/sa-token-starter/sa-token-solon-plugin/src/test/resources/app.yml @@ -0,0 +1,26 @@ + + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + + +sa-token-dao: #名字可以随意取 + redis: + server: "localhost:6379" + password: 123456 + db: 1 + maxTotal: 200 diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/pom.xml b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/pom.xml new file mode 100644 index 000000000..0c99f8bf8 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + jar + + sa-token-spring-boot-reactor-v3v4-common + sa-token-spring-boot-reactor-v3v4-common + sa-token springboot reactor v3v4 common + + + + + + cn.dev33 + sa-token-core + + + + + org.springframework.boot + spring-boot-starter + true + + + + + org.springframework + spring-web + true + + + + + io.projectreactor + reactor-core + true + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + cn.dev33 + sa-token-spring-boot-webmvc-reactor-v2v3v4-common + + + + + + + + + + + cn.dev33 + sa-token-spring-boot3-dependencies + ${revision} + pom + import + + + + + + diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java new file mode 100644 index 000000000..636ba354e --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.context; + +import cn.dev33.satoken.fun.SaRetGenericFunction; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +/** + * Reactor 上下文操作(异步),持有当前请求的 ServerWebExchange 全局引用 + * + * @author click33 + * @since 1.19.0 + */ +public class SaReactorHolder { + + /** + * ServerWebExchange key + */ + public static final String EXCHANGE_KEY = "SA_REACTOR_EXCHANGE_KEY"; + + /** + * WebFilterChain key + */ + public static final String CHAIN_KEY = "SA_REACTOR__CHAIN_KEY"; + + /** + * 在流式上下文写入 ServerWebExchange + * @param ctx 必填 + * @param exchange 必填 + * @param chain 非必填 + * @return / + */ + public static Context setContext(Context ctx, ServerWebExchange exchange, WebFilterChain chain) { + return ctx + .put(EXCHANGE_KEY, exchange) + .put(CHAIN_KEY, chain); + } + + /** + * 在流式上下文获取 ServerWebExchange + * @param ctx / + * @return / + */ + public static ServerWebExchange getExchange(ContextView ctx) { + return ctx.get(EXCHANGE_KEY); + } + + /** + * 在流式上下文获取 WebFilterChain + * @param ctx / + * @return / + */ + public static WebFilterChain getChain(ContextView ctx) { + return ctx.get(CHAIN_KEY); + } + + /** + * 获取 Mono < ServerWebExchange > + * @return / + */ + public static Mono getMonoExchange() { + return Mono.deferContextual(ctx -> Mono.just(getExchange(ctx))); + } + + /** + * 将 exchange 写入到同步上下文中,并执行一段代码,执行完毕清除上下文 + * + * @return / + */ + public static Mono sync(SaRetGenericFunction fun) { + return Mono.deferContextual(ctx -> { + try { + SaReactorSyncHolder.setContext(ctx.get(EXCHANGE_KEY)); + return Mono.just(fun.run()); + } finally { + SaReactorSyncHolder.clearContext(); + } + }); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java new file mode 100644 index 000000000..da53f3a6e --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.context; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.fun.SaRetGenericFunction; +import cn.dev33.satoken.reactor.model.SaRequestForReactor; +import cn.dev33.satoken.reactor.model.SaResponseForReactor; +import cn.dev33.satoken.reactor.model.SaStorageForReactor; +import org.springframework.web.server.ServerWebExchange; + +/** + * Reactor上下文操作(同步),持有当前请求的 ServerWebExchange 全局引用 + * + * @author click33 + * @since 1.19.0 + */ +public class SaReactorSyncHolder { + + /** + * 在同步上下文写入 ServerWebExchange + * @param exchange / + */ + public static void setContext(ServerWebExchange exchange) { + SaRequest request = new SaRequestForReactor(exchange.getRequest()); + SaResponse response = new SaResponseForReactor(exchange.getResponse()); + SaStorage storage = new SaStorageForReactor(exchange); + SaManager.getSaTokenContext().setContext(request, response, storage); + } + + /** + * 在同步上下文清除 ServerWebExchange + */ + public static void clearContext() { + SaManager.getSaTokenContext().clearContext(); + } + + /** + * 在同步上下文获取 ServerWebExchange + * @return / + */ + public static ServerWebExchange getExchange() { + SaTokenContextModelBox box = SaManager.getSaTokenContext().getModelBox(); + return (ServerWebExchange)box.getStorage().getSource(); + } + + /** + * 将 exchange 写入到同步上下文中,并执行一段代码,执行完毕清除上下文 + * @param exchange / + * @param fun / + */ + public static R setContext(ServerWebExchange exchange, SaRetGenericFunction fun) { + try { + setContext(exchange); + return fun.run(); + } finally { + clearContext(); + } + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaFirewallCheckFilterForReactor.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaFirewallCheckFilterForReactor.java new file mode 100644 index 000000000..cc97e5e03 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaFirewallCheckFilterForReactor.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.filter; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.FirewallCheckException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.reactor.model.SaRequestForReactor; +import cn.dev33.satoken.reactor.model.SaResponseForReactor; +import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * 防火墙校验过滤器 (Reactor版) + * + * @author click33 + * @since 1.37.0 + */ +@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) +public class SaFirewallCheckFilterForReactor implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + SaRequestForReactor saRequest = new SaRequestForReactor(exchange.getRequest()); + SaResponseForReactor saResponse = new SaResponseForReactor(exchange.getResponse()); + + try { + SaReactorSyncHolder.setContext(exchange); + SaFirewallStrategy.instance.check.execute(saRequest, saResponse, exchange); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); + } + // FirewallCheckException 异常则交由异常处理策略处理 + catch (FirewallCheckException e) { + if(SaFirewallStrategy.instance.checkFailHandle == null) { + return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); + } else { + SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); + return Mono.empty(); + } + } + finally { + SaReactorSyncHolder.clearContext(); + } + // 更多异常则不处理,交由 Web 框架处理 + + // 向下执行 + return chain.filter(exchange); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java new file mode 100644 index 000000000..9acdaf1bc --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java @@ -0,0 +1,156 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.filter; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.filter.SaFilter; +import cn.dev33.satoken.filter.SaFilterAuthStrategy; +import cn.dev33.satoken.filter.SaFilterErrorStrategy; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Reactor 全局鉴权过滤器 + *

    + * 默认优先级为 -100,尽量保证在其它过滤器之前执行 + *

    + * + * @author click33 + * @since 1.34.0 + */ +@Order(SaTokenConsts.ASSEMBLY_ORDER) +public class SaReactorFilter implements SaFilter, WebFilter { + + // ------------------------ 设置此过滤器 拦截 & 放行 的路由 + + /** + * 拦截路由 + */ + public List includeList = new ArrayList<>(); + + /** + * 放行路由 + */ + public List excludeList = new ArrayList<>(); + + @Override + public SaReactorFilter addInclude(String... paths) { + includeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaReactorFilter addExclude(String... paths) { + excludeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaReactorFilter setIncludeList(List pathList) { + includeList = pathList; + return this; + } + + @Override + public SaReactorFilter setExcludeList(List pathList) { + excludeList = pathList; + return this; + } + + + // ------------------------ 钩子函数 + + /** + * 认证函数:每次请求执行 + */ + public SaFilterAuthStrategy auth = r -> {}; + + /** + * 异常处理函数:每次[认证函数]发生异常时执行此函数 + */ + public SaFilterErrorStrategy error = e -> { + throw new SaTokenException(e); + }; + + /** + * 前置函数:在每次[认证函数]之前执行 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth + */ + public SaFilterAuthStrategy beforeAuth = r -> {}; + + @Override + public SaReactorFilter setAuth(SaFilterAuthStrategy auth) { + this.auth = auth; + return this; + } + + @Override + public SaReactorFilter setError(SaFilterErrorStrategy error) { + this.error = error; + return this; + } + + @Override + public SaReactorFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + // ------------------------ filter + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + // ---------- 全局认证处理 + try { + SaReactorSyncHolder.setContext(exchange); + beforeAuth.run(null); + SaRouter.match(includeList).notMatch(excludeList).check(r -> auth.run(null)); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); + } + catch (Throwable e) { + return SaReactorOperateUtil.writeResult(exchange, String.valueOf(error.run(e))); + } + finally { + SaReactorSyncHolder.clearContext(); + } + + return chain.filter(exchange); + } +} + +/* + * 三种 Filter : + * WebFilter: Spring WebFlux 的过滤器,用于拦截 Web 请求 + * GlobalFilter: Spring Cloud Gateway 的全局过滤器,用于拦截 Gateway 请求 + * GatewayFilter: Spring Cloud Gateway 的局部过滤器,用于拦截 Gateway 请求 + */ \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenContextFilterForReactor.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenContextFilterForReactor.java new file mode 100644 index 000000000..c51088376 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenContextFilterForReactor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.filter; + +import cn.dev33.satoken.reactor.context.SaReactorHolder; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * SaTokenContext 上下文初始化过滤器 (基于 Reactor) + * + * @author click33 + * @since 1.42.0 + */ +@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) +public class SaTokenContextFilterForReactor implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return chain.filter(exchange) + .contextWrite(ctx -> SaReactorHolder.setContext(ctx, exchange, chain)) + .doFinally(r -> { + // 在流式上下文中保存的数据会随着流式操作的结束而销毁,所以此处无需手动清除数据 + }); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenCorsFilterForReactor.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenCorsFilterForReactor.java new file mode 100644 index 000000000..4519d95fc --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenCorsFilterForReactor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.filter; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * CORS 跨域策略过滤器 (基于 Reactor) + * + * @author click33 + * @since 1.42.0 + */ +@Order(SaTokenConsts.CORS_FILTER_ORDER) +public class SaTokenCorsFilterForReactor implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + try { + SaReactorSyncHolder.setContext(exchange); + SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); + SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); + } + finally { + SaReactorSyncHolder.clearContext(); + } + + return chain.filter(exchange); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java new file mode 100644 index 000000000..8d06d8bd3 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java @@ -0,0 +1,196 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.model; + + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.reactor.context.SaReactorHolder; +import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; +import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.http.HttpCookie; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; + +import java.util.Collection; +import java.util.Map; + +/** + * 对 SaRequest 包装类的实现(Reactor 响应式编程版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaRequestForReactor implements SaRequest { + + /** + * 底层Request对象 + */ + protected ServerHttpRequest request; + + /** + * 实例化 + * @param request request对象 + */ + public SaRequestForReactor(ServerHttpRequest request) { + this.request = request; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return request; + } + + /** + * 在 [请求体] 里获取一个值 + */ + @Override + public String getParam(String name) { + return request.getQueryParams().getFirst(name); + } + + /** + * 获取 [请求体] 里提交的所有参数名称 + * @return 参数名称列表 + */ + @Override + public Collection getParamNames(){ + return request.getQueryParams().keySet(); + } + + /** + * 获取 [请求体] 里提交的所有参数 + * @return 参数列表 + */ + @Override + public Map getParamMap(){ + return request.getQueryParams().toSingleValueMap(); + } + + /** + * 在 [请求头] 里获取一个值 + */ + @Override + public String getHeader(String name) { + return request.getHeaders().getFirst(name); + } + + /** + * 在 [Cookie作用域] 里获取一个值 + */ + @Override + public String getCookieValue(String name) { + return getCookieLastValue(name); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) + */ + @Override + public String getCookieFirstValue(String name){ + HttpCookie cookie = request.getCookies().getFirst(name); + if(cookie == null) { + return null; + } + return cookie.getValue(); + } + + /** + * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) + * @param name 键 + * @return 值 + */ + @Override + public String getCookieLastValue(String name){ + String value = null; + String cookieStr = getHeader("Cookie"); + if(SaFoxUtil.isNotEmpty(cookieStr)) { + String[] cookieItems = cookieStr.split(";"); + for (String item : cookieItems) { + String[] kv = item.split("="); + if (kv.length == 2) { + if (kv[0].trim().equals(name)) { + value = kv[1].trim(); + } + } + } + } + return value; + + // 此种写法无法获取到最后一个 Cookie,WebFlux 底层代码应该是有bug,前端提交多个同名Cookie时只能解析出第一个来 + // List cookies = request.getCookies().get(name); + // if(cookies.isEmpty()) { + // return null; + // } + // return cookies.get(cookies.size() - 1).getValue(); + } + + /** + * 返回当前请求path (不包括上下文名称) + */ + @Override + public String getRequestPath() { + return ApplicationInfo.cutPathPrefix(request.getPath().toString()); + } + + /** + * 返回当前请求的url,例:http://xxx.com/test + * @return see note + */ + public String getUrl() { + String currDomain = SaManager.getConfig().getCurrDomain(); + if( ! SaFoxUtil.isEmpty(currDomain)) { + return currDomain + this.getRequestPath(); + } + return request.getURI().toString(); + } + + /** + * 返回当前请求的类型 + */ + @Override + public String getMethod() { + return request.getMethod().name(); + } + + /** + * 查询请求 host + */ + @Override + public String getHost() { + return request.getURI().getHost(); + } + + /** + * 转发请求 + */ + @Override + public Object forward(String path) { + ServerWebExchange exchange = SaReactorSyncHolder.getExchange(); + WebFilterChain chain = exchange.getAttribute(SaReactorHolder.CHAIN_KEY); + + ServerHttpRequest newRequest = request.mutate().path(path).build(); + ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); + + return chain.filter(newExchange); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java new file mode 100644 index 000000000..b9cc7c362 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java @@ -0,0 +1,93 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.model; + +import java.net.URI; + +import org.springframework.http.HttpStatusCode; +import org.springframework.http.server.reactive.ServerHttpResponse; + +import cn.dev33.satoken.context.model.SaResponse; + +/** + * 对 SaResponse 包装类的实现(Reactor 响应式编程版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaResponseForReactor implements SaResponse { + + /** + * 底层Response对象 + */ + protected ServerHttpResponse response; + + /** + * 实例化 + * @param response response对象 + */ + public SaResponseForReactor(ServerHttpResponse response) { + this.response = response; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return response; + } + + /** + * 设置响应状态码 + */ + @Override + public SaResponse setStatus(int sc) { + response.setStatusCode(HttpStatusCode.valueOf(sc)); + return this; + } + + /** + * 在响应头里写入一个值 + */ + @Override + public SaResponse setHeader(String name, String value) { + response.getHeaders().set(name, value); + return this; + } + + /** + * 在响应头里添加一个值 + * @param name 名字 + * @param value 值 + * @return 对象自身 + */ + public SaResponse addHeader(String name, String value) { + response.getHeaders().add(name, value); + return this; + } + + /** + * 重定向 + */ + @Override + public Object redirect(String url) { + response.setStatusCode(HttpStatusCode.valueOf(302)); + response.getHeaders().setLocation(URI.create(url)); + return null; + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java new file mode 100644 index 000000000..c609ee3a5 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.model; + +import org.springframework.web.server.ServerWebExchange; + +import cn.dev33.satoken.context.model.SaStorage; + +/** + * 对 SaStorage 包装类的实现(Reactor 响应式编程版) + * + * @author click33 + * @since 1.34.0 + */ +public class SaStorageForReactor implements SaStorage { + + /** + * 底层 ServerWebExchange 对象 + */ + protected ServerWebExchange exchange; + + /** + * 实例化 + * @param exchange exchange对象 + */ + public SaStorageForReactor(ServerWebExchange exchange) { + this.exchange = exchange; + } + + /** + * 获取底层源对象 + */ + @Override + public Object getSource() { + return exchange; + } + + /** + * 在 [Request作用域] 里写入一个值 + */ + @Override + public SaStorageForReactor set(String key, Object value) { + exchange.getAttributes().put(key, value); + return this; + } + + /** + * 在 [Request作用域] 里获取一个值 + */ + @Override + public Object get(String key) { + return exchange.getAttributes().get(key); + } + + /** + * 在 [Request作用域] 里删除一个值 + */ + @Override + public SaStorageForReactor delete(String key) { + exchange.getAttributes().remove(key); + return this; + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/package-info.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/package-info.java new file mode 100644 index 000000000..1e6383d7a --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * Sa-Token 集成 Reactor 响应式编程的各个组件 + */ +package cn.dev33.satoken.reactor; diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextForSpringReactor.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextForSpringReactor.java new file mode 100644 index 000000000..769716c1d --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextForSpringReactor.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.spring; + +import cn.dev33.satoken.context.SaTokenContextForThreadLocal; + +/** + *

    此为低版本(<1.42.0) 的上下文处理方案,仅做留档,如无必要请勿使用

    + * + * Sa-Token 上下文处理器 [ Spring Reactor 版本实现 ] ,基于 SaTokenContextForThreadLocal 定制 + * + * @author click33 + * @since 1.33.0 + */ +public class SaTokenContextForSpringReactor extends SaTokenContextForThreadLocal { + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java new file mode 100644 index 000000000..20c8b1698 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.spring; + +import cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor; +import cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor; +import cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor; +import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import org.springframework.context.annotation.Bean; + +/** + * 注册 Sa-Token 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenContextRegister { + + public SaTokenContextRegister() { + // 重写路由匹配算法 + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return SaPathPatternParserUtil.match(pattern, path); + }; + } + + /** + * 上下文过滤器 + * + * @return / + */ + @Bean + public SaTokenContextFilterForReactor saTokenContextFilterForServlet() { + return new SaTokenContextFilterForReactor(); + } + + /** + * CORS 跨域策略过滤器 + * + * @return / + */ + @Bean + public SaTokenCorsFilterForReactor saTokenCorsFilterForReactor() { + return new SaTokenCorsFilterForReactor(); + } + + /** + * 防火墙过滤器 + * + * @return / + */ + @Bean + public SaFirewallCheckFilterForReactor saFirewallCheckFilterForReactor() { + return new SaFirewallCheckFilterForReactor(); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/util/SaReactorOperateUtil.java b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/util/SaReactorOperateUtil.java new file mode 100644 index 000000000..5ee4809e4 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-reactor-v3v4-common/src/main/java/cn/dev33/satoken/reactor/util/SaReactorOperateUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.reactor.util; + +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * Reactor 操作工具类 + * + * @author click33 + * @since 1.42.0 + */ +public class SaReactorOperateUtil { + + /** + * 写入结果到输出流 + * @param exchange / + * @param result / + * @return / + */ + public static Mono writeResult(ServerWebExchange exchange, String result) { + // 写入输出流 + // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json + // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); + if(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) { + exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); + } + return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes()))); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-starter/.gitignore b/sa-token-starter/sa-token-spring-boot-starter/.gitignore deleted file mode 100644 index f56feec72..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ - -node_modules/ -bin/ -.settings/ -unpackage/ -.classpath -.project - -.factorypath - -.idea/ \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-starter/pom.xml b/sa-token-starter/sa-token-spring-boot-starter/pom.xml index 93846aee7..ece994b9b 100644 --- a/sa-token-starter/sa-token-spring-boot-starter/pom.xml +++ b/sa-token-starter/sa-token-spring-boot-starter/pom.xml @@ -7,7 +7,8 @@ cn.dev33 sa-token-starter - 1.24.0 + ${revision} + ../pom.xml jar @@ -16,39 +17,52 @@ springboot integrate sa-token + - cn.dev33 - sa-token-servlet - ${sa-token-version} + org.springframework.boot + spring-boot-starter-web + + org.springframework.boot - spring-boot-starter-web - 2.0.0.RELEASE + spring-boot-configuration-processor + true - + + cn.dev33 - sa-token-oauth2 - ${sa-token-version} - true + sa-token-servlet - + + - org.springframework.boot - spring-boot-starter-test - 2.0.0.RELEASE - test + cn.dev33 + sa-token-spring-boot-webmvc-reactor-v2v3v4-common - + + - org.springframework.boot - spring-boot-configuration-processor - 2.0.0.RELEASE - true + cn.dev33 + sa-token-jackson + + + + + + + cn.dev33 + sa-token-spring-boot2-dependencies + ${revision} + pom + import + + + diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForServlet.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForServlet.java new file mode 100644 index 000000000..32d79c28d --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForServlet.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.FirewallCheckException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.servlet.model.SaResponseForServlet; +import cn.dev33.satoken.servlet.util.SaServletOperateUtil; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 防火墙校验过滤器 (基于 Servlet) + * + * @author click33 + * @since 1.37.0 + */ +@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) +public class SaFirewallCheckFilterForServlet implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + SaRequestForServlet saRequest = new SaRequestForServlet(req); + SaResponseForServlet saResponse = new SaResponseForServlet(res); + + try { + SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaServletOperateUtil.writeResult(response, e.getMessage()); + return; + } + catch (FirewallCheckException e) { + if(SaFirewallStrategy.instance.checkFailHandle == null) { + SaServletOperateUtil.writeResult(response, e.getMessage()); + } else { + SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); + } + return; + } + // 更多异常则不处理,交由 Web 框架处理 + + // 向内执行 + chain.doFilter(request, response); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java index 46eb59e88..731120e44 100644 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java @@ -1,100 +1,81 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - -import org.springframework.core.annotation.Order; - import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.servlet.util.SaServletOperateUtil; import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; + +import javax.servlet.*; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** - * Servlet全局过滤器 - * @author kong + * 全局鉴权过滤器 (基于 Servlet) + *

    + * 默认优先级为 -100,尽量保证在其它过滤器之前执行 + *

    * + * @author click33 + * @since 1.19.0 */ @Order(SaTokenConsts.ASSEMBLY_ORDER) -public class SaServletFilter implements Filter { +public class SaServletFilter implements SaFilter, Filter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ - private List includeList = new ArrayList<>(); + public List includeList = new ArrayList<>(); /** * 放行路由 */ - private List excludeList = new ArrayList<>(); + public List excludeList = new ArrayList<>(); - /** - * 添加 [拦截路由] - * @param paths 路由 - * @return 对象自身 - */ + @Override public SaServletFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } - - /** - * 添加 [放行路由] - * @param paths 路由 - * @return 对象自身 - */ + + @Override public SaServletFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } - /** - * 写入 [拦截路由] 集合 - * @param pathList 路由集合 - * @return 对象自身 - */ + @Override public SaServletFilter setIncludeList(List pathList) { includeList = pathList; return this; } - - /** - * 写入 [放行路由] 集合 - * @param pathList 路由集合 - * @return 对象自身 - */ + + @Override public SaServletFilter setExcludeList(List pathList) { excludeList = pathList; return this; } - - /** - * 获取 [拦截路由] 集合 - * @return see note - */ - public List getIncludeList() { - return includeList; - } - - /** - * 获取 [放行路由] 集合 - * @return see note - */ - public List getExcludeList() { - return excludeList; - } // ------------------------ 钩子函数 @@ -112,35 +93,24 @@ public List getExcludeList() { }; /** - * 前置函数:在每次[认证函数]之前执行 + * 前置函数:在每次[认证函数]之前执行 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> {}; - /** - * 写入[认证函数]: 每次请求执行 - * @param auth see note - * @return 对象自身 - */ + @Override public SaServletFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } - /** - * 写入[异常处理函数]:每次[认证函数]发生异常时执行此函数 - * @param error see note - * @return 对象自身 - */ + @Override public SaServletFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } - /** - * 写入[前置函数]:在每次[认证函数]之前执行 - * @param beforeAuth see note - * @return 对象自身 - */ + @Override public SaServletFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; @@ -150,27 +120,22 @@ public SaServletFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { // ------------------------ doFilter @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { - // 执行全局过滤器 - SaRouter.match(includeList, excludeList, () -> { - beforeAuth.run(null); + // 执行全局过滤器 + beforeAuth.run(null); + SaRouter.match(includeList).notMatch(excludeList).check(r -> { auth.run(null); }); - - } catch (StopMatchException e) { - - } catch (Throwable e) { - // 1. 获取异常处理策略结果 - String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e)); - - // 2. 写入输出流 - if(response.getContentType() == null) { - response.setContentType("text/plain; charset=utf-8"); - } - response.getWriter().print(result); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaServletOperateUtil.writeResult(response, e.getMessage()); + return; + } + catch (Throwable e) { + SaServletOperateUtil.writeResult(response, String.valueOf(error.run(e))); return; } @@ -178,14 +143,4 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha chain.doFilter(request, response); } - @Override - public void init(FilterConfig filterConfig) throws ServletException { - } - - @Override - public void destroy() { - } - - - } diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForServlet.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForServlet.java new file mode 100644 index 000000000..20931569e --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForServlet.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; + +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * SaTokenContext 上下文初始化过滤器 (基于 Servlet) + * + * @author click33 + * @since 1.42.0 + */ +@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) +public class SaTokenContextFilterForServlet implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + SaTokenContextServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response); + chain.doFilter(request, response); + } finally { + SaTokenContextServletUtil.clearContext(); + } + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForServlet.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForServlet.java new file mode 100644 index 000000000..aa1aa12e4 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForServlet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.servlet.util.SaServletOperateUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; + +import javax.servlet.*; +import java.io.IOException; + +/** + * CORS 跨域策略过滤器 (基于 Servlet) + * + * @author click33 + * @since 1.42.0 + */ +@Order(SaTokenConsts.CORS_FILTER_ORDER) +public class SaTokenCorsFilterForServlet implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + try { + SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); + SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaServletOperateUtil.writeResult(response, e.getMessage()); + return; + } + + chain.doFilter(request, response); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaAnnotationInterceptor.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaAnnotationInterceptor.java deleted file mode 100644 index 18c686147..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaAnnotationInterceptor.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.dev33.satoken.interceptor; - -import java.lang.reflect.Method; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; - -import cn.dev33.satoken.SaManager; - -/** - * 注解式鉴权 - 拦截器 - * - * @author kong - */ -public class SaAnnotationInterceptor implements HandlerInterceptor { - - /** - * 构建: 注解式鉴权 - 拦截器 - */ - public SaAnnotationInterceptor() { - } - - /** - * 每次请求之前触发的方法 - */ - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - - // 获取处理method - if (handler instanceof HandlerMethod == false) { - return true; - } - Method method = ((HandlerMethod) handler).getMethod(); - - // 进行验证 - SaManager.getSaTokenAction().checkMethodAnnotation(method); - - // 通过验证 - return true; - } - -} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java new file mode 100644 index 000000000..52fb37ac0 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java @@ -0,0 +1,140 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.interceptor; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; + +/** + * Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力 + * + * @author click33 + * @since 1.31.0 + */ +public class SaInterceptor implements HandlerInterceptor { + + /** + * 是否打开注解鉴权,配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 + */ + public boolean isAnnotation = true; + + /** + * 认证前置函数:在注解鉴权之前执行 + *

    参数:路由处理函数指针 + */ + public SaParamFunction beforeAuth = handler -> {}; + + /** + * 认证函数:每次请求执行 + *

    参数:路由处理函数指针 + */ + public SaParamFunction auth = handler -> {}; + + /** + * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 + */ + public SaInterceptor() { + } + + /** + * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 + * @param auth 认证函数,每次请求执行 + */ + public SaInterceptor(SaParamFunction auth) { + this.auth = auth; + } + + /** + * 设置是否打开注解鉴权:配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 + * @param isAnnotation / + * @return 对象自身 + */ + public SaInterceptor isAnnotation(boolean isAnnotation) { + this.isAnnotation = isAnnotation; + return this; + } + + /** + * 写入 [ 认证前置函数 ]: 在注解鉴权之前执行 + * @param beforeAuth / + * @return 对象自身 + */ + public SaInterceptor setBeforeAuth(SaParamFunction beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + /** + * 写入 [ 认证函数 ]: 每次请求执行 + * @param auth / + * @return 对象自身 + */ + public SaInterceptor setAuth(SaParamFunction auth) { + this.auth = auth; + return this; + } + + + // ----------------- 验证方法 ----------------- + + /** + * 每次请求之前触发的方法 + */ + @Override + @SuppressWarnings("all") + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + try { + // 前置函数:在注解鉴权之前执行 + beforeAuth.run(handler); + + // 这里必须确保 handler 是 HandlerMethod 类型时,才能进行注解鉴权 + if(isAnnotation && handler instanceof HandlerMethod) { + Method method = ((HandlerMethod) handler).getMethod(); + SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); + } + + // Auth 路由拦截鉴权校验 + auth.run(handler); + + } catch (StopMatchException e) { + // StopMatchException 异常代表:停止匹配,进入Controller + + } catch (BackResultException e) { + // BackResultException 异常代表:停止匹配,向前端输出结果 + // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 back 前自行设置 Content-Type 为 application/json + // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); + if(response.getContentType() == null) { + response.setContentType("text/plain; charset=utf-8"); + } + response.getWriter().print(e.getMessage()); + return false; + } + + // 通过验证 + return true; + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaRouteInterceptor.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaRouteInterceptor.java deleted file mode 100644 index 7a08e4332..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaRouteInterceptor.java +++ /dev/null @@ -1,83 +0,0 @@ -package cn.dev33.satoken.interceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.web.servlet.HandlerInterceptor; - -import cn.dev33.satoken.exception.BackResultException; -import cn.dev33.satoken.exception.StopMatchException; -import cn.dev33.satoken.router.SaRouteFunction; -import cn.dev33.satoken.servlet.model.SaRequestForServlet; -import cn.dev33.satoken.servlet.model.SaResponseForServlet; -import cn.dev33.satoken.stp.StpUtil; - -/** - * sa-token基于路由的拦截式鉴权 - * @author kong - */ -public class SaRouteInterceptor implements HandlerInterceptor { - - /** - * 每次进入拦截器的[执行函数] - */ - public SaRouteFunction function; - - /** - * 创建一个路由拦截器 - */ - public SaRouteInterceptor() { - } - - /** - * 创建, 并指定[执行函数] - * @param function [执行函数] - */ - public SaRouteInterceptor(SaRouteFunction function) { - this.function = function; - } - - /** - * 静态方法快速构建一个 - * @param function 自定义模式下的执行函数 - * @return sa路由拦截器 - */ - public static SaRouteInterceptor newInstance(SaRouteFunction function) { - return new SaRouteInterceptor(function); - } - - - // ----------------- 验证方法 ----------------- - - /** - * 每次请求之前触发的方法 - */ - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - - // 如果未提供function,默认进行登录验证 - if(function == null) { - StpUtil.checkLogin(); - } else { - // 否则执行认证函数 - try { - function.run(new SaRequestForServlet(request), new SaResponseForServlet(response), handler); - } catch (StopMatchException e) { - // 停止匹配,进入Controller - } catch (BackResultException e) { - // 停止匹配,向前端输出结果 - if(response.getContentType() == null) { - response.setContentType("text/plain; charset=utf-8"); - } - response.getWriter().print(e.getMessage()); - return false; - } - } - - // 通过验证 - return true; - } - - -} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/package-info.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/package-info.java index 56ef7301e..9e87bb6e9 100644 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/package-info.java +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/package-info.java @@ -1,4 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ /** - * sa-token集成SpringBoot的各个组件 + * Sa-Token 集成 SpringBoot 的各个组件 */ package cn.dev33.satoken; \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java deleted file mode 100644 index 0b3eae228..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java +++ /dev/null @@ -1,129 +0,0 @@ -package cn.dev33.satoken.spring; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.util.PathMatcher; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.action.SaTokenAction; -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.context.SaTokenContext; -import cn.dev33.satoken.dao.SaTokenDao; -import cn.dev33.satoken.id.SaIdTemplate; -import cn.dev33.satoken.id.SaIdUtil; -import cn.dev33.satoken.listener.SaTokenListener; -import cn.dev33.satoken.sso.SaSsoTemplate; -import cn.dev33.satoken.sso.SaSsoUtil; -import cn.dev33.satoken.stp.StpInterface; -import cn.dev33.satoken.temp.SaTempInterface; - -/** - * 注入Sa-Token所需要的Bean - * - * @author kong - * - */ -public class SaBeanInject { - - /** - * 注入配置Bean - * - * @param saTokenConfig 配置对象 - */ - @Autowired(required = false) - public void setConfig(SaTokenConfig saTokenConfig) { - SaManager.setConfig(saTokenConfig); - } - - /** - * 注入持久化Bean - * - * @param saTokenDao SaTokenDao对象 - */ - @Autowired(required = false) - public void setSaTokenDao(SaTokenDao saTokenDao) { - SaManager.setSaTokenDao(saTokenDao); - } - - /** - * 注入权限认证Bean - * - * @param stpInterface StpInterface对象 - */ - @Autowired(required = false) - public void setStpInterface(StpInterface stpInterface) { - SaManager.setStpInterface(stpInterface); - } - - /** - * 注入框架行为Bean - * - * @param saTokenAction SaTokenAction对象 - */ - @Autowired(required = false) - public void setSaTokenAction(SaTokenAction saTokenAction) { - SaManager.setSaTokenAction(saTokenAction); - } - - /** - * 注入容器交互Bean - * - * @param saTokenContext SaTokenContext对象 - */ - @Autowired(required = false) - public void setSaTokenContext(SaTokenContext saTokenContext) { - SaManager.setSaTokenContext(saTokenContext); - } - - /** - * 注入侦听器Bean - * - * @param saTokenListener saTokenListener对象 - */ - @Autowired(required = false) - public void setSaTokenListener(SaTokenListener saTokenListener) { - SaManager.setSaTokenListener(saTokenListener); - } - - /** - * 注入临时令牌验证模块 Bean - * - * @param saTemp saTemp对象 - */ - @Autowired(required = false) - public void setSaTemp(SaTempInterface saTemp) { - SaManager.setSaTemp(saTemp); - } - - /** - * 注入 Sa-Id-Token 模块 Bean - * - * @param saIdTemplate saIdTemplate对象 - */ - @Autowired(required = false) - public void setSaIdTemplate(SaIdTemplate saIdTemplate) { - SaIdUtil.saIdTemplate = saIdTemplate; - } - - /** - * 注入 Sa-Token-SSO 单点登录模块 Bean - * - * @param saSsoTemplate saSsoTemplate对象 - */ - @Autowired(required = false) - public void setSaSsoTemplate(SaSsoTemplate saSsoTemplate) { - SaSsoUtil.saSsoTemplate = saSsoTemplate; - } - - /** - * 利用自动注入特性,获取Spring框架内部使用的路由匹配器 - * - * @param pathMatcher 要设置的 pathMatcher - */ - @Autowired(required = false) - @Qualifier("mvcPathMatcher") - public void setPathMatcher(PathMatcher pathMatcher) { - SaPathMatcherHolder.setPathMatcher(pathMatcher); - } - -} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java deleted file mode 100644 index db4805cc0..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.dev33.satoken.spring; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; - -import cn.dev33.satoken.config.SaTokenConfig; -import cn.dev33.satoken.context.SaTokenContext; - -/** - * 注册Sa-Token所需要的Bean - *

    Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖 - * @author kong - * - */ -public class SaBeanRegister { - - /** - * 获取配置Bean - * - * @return 配置对象 - */ - @Bean - @ConfigurationProperties(prefix = "sa-token") - public SaTokenConfig getSaTokenConfig() { - return new SaTokenConfig(); - } - - /** - * 获取容器交互Bean (Spring版) - * - * @return 容器交互Bean (Spring版) - */ - @Bean - public SaTokenContext getSaTokenContext() { - return new SaTokenContextForSpring(); - } - -} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaHistoryVersionInject.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaHistoryVersionInject.java deleted file mode 100644 index 75d98f20b..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaHistoryVersionInject.java +++ /dev/null @@ -1,62 +0,0 @@ -package cn.dev33.satoken.spring; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.env.OriginTrackedMapPropertySource; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.PropertySource; - -import cn.dev33.satoken.util.SaTokenConsts; - -/** - * 兼容旧版本的配置信息注入 - *

    目前已处理: - *

    1. yml配置前缀 [spring.sa-token.] 更改为 [sa-token.] - * @author kong - */ -//SpringBoot 1.5.x 版本下不存在 OriginTrackedMapPropertySource ,不加这个注解会导致项目无法启动 -@ConditionalOnClass(OriginTrackedMapPropertySource.class) -public class SaHistoryVersionInject implements EnvironmentAware{ - - @Override - @SuppressWarnings("unchecked") - public void setEnvironment(Environment env) { - try { - ConfigurableEnvironment c = (ConfigurableEnvironment) env; - MutablePropertySources sources = c.getPropertySources(); - - // 将yml中所有 [spring.sa-token.] 开头的配置转移到 [sa-token.] 下 - Map newMap = new LinkedHashMap(); - for (PropertySource source : sources) { - // 根据Name开头单词判断是否为 SpringBoot .yml 或者.properties 的配置 - if(source.getName().startsWith("applicationConfig")) { - Map bootProp = (Map)source.getSource(); - for (String key : bootProp.keySet()) { - if(key != null && key.startsWith("spring.sa-token.")) { - String newKey = key.substring(7); - newMap.put(newKey, bootProp.get(key)); - } - } - } - } - - // 追加到总配置里面 - if(newMap.size() > 0) { - System.err.println("\n" - + "Sa-Token Warning: 当前配置文件方式已过时,请更改配置前缀:原 [spring.sa-token.] 更改为 [sa-token.]\n" - + "Sa-Token Warning: 当前版本(" + SaTokenConsts.VERSION_NO + ")暂时向下兼容,未来版本可能会完全移除旧形式"); - OriginTrackedMapPropertySource source = new OriginTrackedMapPropertySource("SaHistoryVersionInjectProperty", newMap); - // 追加到末尾,优先级最低 - c.getPropertySources().addLast(source); - } - } catch (Exception e) { - // not handle - } - } - -} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaPathMatcherHolder.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaPathMatcherHolder.java deleted file mode 100644 index 349e80944..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaPathMatcherHolder.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.dev33.satoken.spring; - -import org.springframework.util.AntPathMatcher; -import org.springframework.util.PathMatcher; - -/** - * - * @author kong - * - */ -public class SaPathMatcherHolder { - - /** - * 路由匹配器 - */ - public static PathMatcher pathMatcher; - - /** - * 获取路由匹配器 - * @return 路由匹配器 - */ - public static PathMatcher getPathMatcher() { - if(pathMatcher == null) { - pathMatcher = new AntPathMatcher(); - } - return pathMatcher; - } - - /** - * 写入路由匹配器 - * @param pathMatcher 路由匹配器 - */ - public static void setPathMatcher(PathMatcher pathMatcher) { - SaPathMatcherHolder.pathMatcher = pathMatcher; - } - -} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java index 6f6f1fe24..2e303fad3 100644 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java @@ -1,6 +1,21 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; -import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.context.SaTokenContextForReadOnly; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; @@ -9,15 +24,17 @@ import cn.dev33.satoken.servlet.model.SaStorageForServlet; /** - * sa-token 对Cookie的相关操作 接口实现类 - * - * @author kong + *

    此为低版本(<1.42.0) 的上下文处理方案,基于 Spring 内部工具类 RequestContextHolder 读写上下文,仅做留档,如无必要请勿使用

    * + * Sa-Token 上下文处理器 [ SpringMVC版本实现 ]。在 SpringMVC、SpringBoot 中使用 Sa-Token 时,必须注入此实现类,否则会出现上下文无效异常 + * + * @author click33 + * @since 1.19.0 */ -public class SaTokenContextForSpring implements SaTokenContext { +public class SaTokenContextForSpring implements SaTokenContextForReadOnly { /** - * 获取当前请求的Request对象 + * 获取当前请求的 Request 包装对象 */ @Override public SaRequest getRequest() { @@ -25,7 +42,7 @@ public SaRequest getRequest() { } /** - * 获取当前请求的Response对象 + * 获取当前请求的 Response 包装对象 */ @Override public SaResponse getResponse() { @@ -33,21 +50,19 @@ public SaResponse getResponse() { } /** - * 获取当前请求的 [存储器] 对象 + * 获取当前请求的 Storage 包装对象 */ @Override public SaStorage getStorage() { return new SaStorageForServlet(SpringMVCUtil.getRequest()); } - + /** - * 校验指定路由匹配符是否可以匹配成功指定路径 + * 判断:在本次请求中,此上下文是否可用。 */ @Override - public boolean matchPath(String pattern, String path) { - return SaPathMatcherHolder.getPathMatcher().match(pattern, path); + public boolean isValid() { + return SpringMVCUtil.isWeb(); } - - } diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java new file mode 100644 index 000000000..68b0bc5f3 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; + +import cn.dev33.satoken.filter.SaFirewallCheckFilterForServlet; +import cn.dev33.satoken.filter.SaTokenContextFilterForServlet; +import cn.dev33.satoken.filter.SaTokenCorsFilterForServlet; +import cn.dev33.satoken.spring.pathmatch.SaPatternsRequestConditionHolder; +import cn.dev33.satoken.strategy.SaStrategy; +import org.springframework.context.annotation.Bean; + +/** + * 注册 Sa-Token 框架所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenContextRegister { + + public SaTokenContextRegister() { + // 重写路由匹配算法 + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return SaPatternsRequestConditionHolder.match(pattern, path); + }; + } + + /** + * 上下文过滤器 + * + * @return / + */ + @Bean + public SaTokenContextFilterForServlet saTokenContextFilterForServlet() { + return new SaTokenContextFilterForServlet(); + } + + /** + * CORS 跨域策略过滤器 + * + * @return / + */ + @Bean + public SaTokenCorsFilterForServlet saTokenCorsFilterForServlet() { + return new SaTokenCorsFilterForServlet(); + } + + /** + * 防火墙过滤器 + * + * @return / + */ + @Bean + public SaFirewallCheckFilterForServlet saFirewallCheckFilterForServlet() { + return new SaFirewallCheckFilterForServlet(); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringBootVersionCompatibilityChecker.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringBootVersionCompatibilityChecker.java new file mode 100644 index 000000000..1684f6c8d --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringBootVersionCompatibilityChecker.java @@ -0,0 +1,25 @@ +package cn.dev33.satoken.spring; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.boot.SpringBootVersion; + +/** + * SpringBoot 版本与 Sa-Token 版本兼容检查器,当开发者错误的在 SpringBoot3/4.x 项目中引入当前集成包时,将在控制台做出提醒并阻断项目启动 + * + * @author Uncarbon + * @since 1.38.0 + */ +public class SpringBootVersionCompatibilityChecker { + + public SpringBootVersionCompatibilityChecker() { + String version = SpringBootVersion.getVersion(); + if (SaFoxUtil.isEmpty(version) || version.startsWith("1.") || version.startsWith("2.")) { + return; + } + String str = "当前 SpringBoot 版本(" + version + ")与 Sa-Token 依赖不兼容," + + "请将依赖 sa-token-spring-boot-starter 修改为:sa-token-spring-boot3/4-starter"; + System.err.println(str); + throw new SaTokenException(str); + } +} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java index f647b39c3..c3c8a098d 100644 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java @@ -1,42 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - +import cn.dev33.satoken.exception.NotWebContextException; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import cn.dev33.satoken.exception.SaTokenException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** - * SpringMVC相关操作 - * @author kong + * SpringMVC 相关操作工具类,快速获取当前会话的 HttpServletRequest、HttpServletResponse 对象 * + * @author click33 + * @since 1.19.0 */ public class SpringMVCUtil { + private SpringMVCUtil() { + } + /** - * 获取当前会话的 request + * 获取当前会话的 request 对象 * @return request */ public static HttpServletRequest getRequest() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null) { - throw new SaTokenException("非Web上下文无法获取Request"); + throw new NotWebContextException("非 web 上下文无法获取 HttpServletRequest"); } return servletRequestAttributes.getRequest(); } /** - * 获取当前会话的 response + * 获取当前会话的 response 对象 * @return response */ public static HttpServletResponse getResponse() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null) { - throw new SaTokenException("非Web上下文无法获取Response"); + throw new NotWebContextException("非 web 上下文无法获取 HttpServletResponse"); } return servletRequestAttributes.getResponse(); } + /** + * 判断当前是否处于 Web 上下文中 + * @return / + */ + public static boolean isWeb() { + return RequestContextHolder.getRequestAttributes() != null; + } + } diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java deleted file mode 100644 index fd0380e81..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.dev33.satoken.spring.oauth2; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; - -import cn.dev33.satoken.oauth2.SaOAuth2Manager; -import cn.dev33.satoken.oauth2.config.SaOAuth2Config; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Template; -import cn.dev33.satoken.oauth2.logic.SaOAuth2Util; - -/** - * 注入 Sa-Token-OAuth2 所需要的Bean - * - * @author kong - * - */ -@ConditionalOnClass(SaOAuth2Manager.class) -public class SaOAuth2BeanInject { - - /** - * 注入OAuth2配置Bean - * - * @param saOAuth2Config 配置对象 - */ - @Autowired(required = false) - public void setSaOAuth2Config(SaOAuth2Config saOAuth2Config) { - SaOAuth2Manager.setConfig(saOAuth2Config); - } - - /** - * 注入代码模板Bean - * - * @param saOAuth2Template 代码模板Bean - */ - @Autowired(required = false) - public void setSaOAuth2Interface(SaOAuth2Template saOAuth2Template) { - SaOAuth2Util.saOAuth2Template = saOAuth2Template; - } - -} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanRegister.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanRegister.java deleted file mode 100644 index 0122d0a25..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanRegister.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.dev33.satoken.spring.oauth2; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; - -import cn.dev33.satoken.oauth2.SaOAuth2Manager; -import cn.dev33.satoken.oauth2.config.SaOAuth2Config; - -/** - * 注册 Sa-Token-OAuth2 所需要的Bean - * @author kong - * - */ -@ConditionalOnClass(SaOAuth2Manager.class) -public class SaOAuth2BeanRegister { - - /** - * 获取OAuth2配置Bean - * @return 配置对象 - */ - @Bean - @ConfigurationProperties(prefix = "sa-token.oauth2") - public SaOAuth2Config getSaOAuth2Config() { - return new SaOAuth2Config(); - } - -} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/package-info.java b/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/package-info.java deleted file mode 100644 index 793d87dbd..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/oauth2/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Sa-Token-OAuth2 模块自动化配置(只有引入了Sa-Token-OAuth2模块后,此包下的代码才会开始工作) - */ -package cn.dev33.satoken.spring.oauth2; \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring.factories b/sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring.factories index 2e7e333a4..05745ef37 100644 --- a/sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -1,6 +1,2 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -cn.dev33.satoken.spring.SaBeanRegister,\ -cn.dev33.satoken.spring.SaBeanInject,\ -cn.dev33.satoken.spring.SaHistoryVersionInject,\ -cn.dev33.satoken.spring.oauth2.SaOAuth2BeanRegister,\ -cn.dev33.satoken.spring.oauth2.SaOAuth2BeanInject \ No newline at end of file +cn.dev33.satoken.spring.SaTokenContextRegister \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..a91d6104e --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.spring.SpringBootVersionCompatibilityChecker \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/test/java/com/pj/test/SaTokenSpringBootStarterTest.java b/sa-token-starter/sa-token-spring-boot-starter/src/test/java/com/pj/test/SaTokenSpringBootStarterTest.java deleted file mode 100644 index 041eee648..000000000 --- a/sa-token-starter/sa-token-spring-boot-starter/src/test/java/com/pj/test/SaTokenSpringBootStarterTest.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.pj.test; - -import java.util.List; - -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -import cn.dev33.satoken.exception.DisableLoginException; -import cn.dev33.satoken.session.SaSession; -import cn.dev33.satoken.stp.StpUtil; -import cn.dev33.satoken.temp.SaTempUtil; -import cn.dev33.satoken.util.SaTokenConsts; - -/** - * Sa-Token 单元测试 - * - *

    注解详解参考: https://www.cnblogs.com/flypig666/p/11505277.html - * @author Auster - * - */ -@RunWith(SpringRunner.class) -@SpringBootTest(classes = StartUpApplication.class) -public class SaTokenSpringBootStarterTest { - - // 开始 - @BeforeClass - public static void beforeClass() { - System.out.println("\n\n------------------------ Test star ..."); - } - - // 结束 - @AfterClass - public static void afterClass() { - System.out.println("\n\n------------------------ Test end ... \n"); - } - - // 测试:登录 & 注销 - @Test - public void testDoLogin() { - // 登录 - StpUtil.login(10001); - Assert.assertTrue(StpUtil.isLogin()); - Assert.assertNotNull(StpUtil.getTokenValue()); // token不为null - Assert.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 - Assert.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE); // 登录设备 - - // 注销 - StpUtil.logout(); - Assert.assertFalse(StpUtil.isLogin()); - // Assert.assertNull(StpUtil.getTokenValue()); - Assert.assertNull(StpUtil.getLoginIdDefaultNull()); - } - - // 测试:权限认证 - @Test - public void testCheckPermission() { - StpUtil.login(10001); - - // 权限认证 - Assert.assertTrue(StpUtil.hasPermission("user-add")); - Assert.assertTrue(StpUtil.hasPermission("user-list")); - Assert.assertTrue(StpUtil.hasPermission("user")); - Assert.assertTrue(StpUtil.hasPermission("art-add")); - Assert.assertFalse(StpUtil.hasPermission("get-user")); - } - - // 测试:角色认证 - @Test - public void testCheckRole() { - StpUtil.login(10001); - - // 角色认证 - Assert.assertTrue(StpUtil.hasRole("admin")); - Assert.assertFalse(StpUtil.hasRole("teacher")); - } - - // 测试:踢人下线 - @Test - public void testKickOut() { - - // 根据token踢人 - StpUtil.login(10001); - Assert.assertTrue(StpUtil.isLogin()); - StpUtil.logoutByTokenValue(StpUtil.getTokenValue()); - Assert.assertFalse(StpUtil.isLogin()); - - // 根据账号id踢人 - StpUtil.login(10001); - Assert.assertTrue(StpUtil.isLogin()); - StpUtil.logoutByLoginId(10001); - Assert.assertFalse(StpUtil.isLogin()); - } - - // 测试:账号封禁 - @Test(expected = DisableLoginException.class) - public void testDisable() { - - // 封号 - StpUtil.disable(10007, 200); - Assert.assertTrue(StpUtil.isDisable(10007)); - - // 解封 - StpUtil.untieDisable(10007); - Assert.assertFalse(StpUtil.isDisable(10007)); - - // 封号后登陆 (会抛出 DisableLoginException 异常) - StpUtil.disable(10007, 200); - StpUtil.login(10007); - } - - // 测试:Session会话 - @Test - public void testSession() { - StpUtil.login(10001); - - // Session 应该存在 - Assert.assertNotNull(StpUtil.getSession(false)); - - // 存取值 - SaSession session = StpUtil.getSession(); - session.set("name", "zhang"); - session.set("age", "18"); - Assert.assertEquals(session.get("name"), "zhang"); - Assert.assertEquals(session.getInt("age"), 18); - Assert.assertEquals((int)session.getModel("age", int.class), 18); - Assert.assertEquals((int)session.get("age", 20), 18); - Assert.assertEquals((int)session.get("name2", 20), 20); - Assert.assertEquals((int)session.get("name2", () -> 30), 30); - - } - - // 测试:身份切换 - @Test - public void testSwitch() { - // 登录 - StpUtil.login(10001); - Assert.assertFalse(StpUtil.isSwitch()); - Assert.assertEquals(StpUtil.getLoginIdAsLong(), 10001); - - // 开始身份切换 - StpUtil.switchTo(10044); - Assert.assertTrue(StpUtil.isSwitch()); - Assert.assertEquals(StpUtil.getLoginIdAsLong(), 10044); - - // 结束切换 - StpUtil.endSwitch(); - Assert.assertFalse(StpUtil.isSwitch()); - Assert.assertEquals(StpUtil.getLoginIdAsLong(), 10001); - } - - // 测试:会话管理 - @Test - public void testSearchTokenValue() { - // 登录 - StpUtil.login(10001); - StpUtil.login(10002); - StpUtil.login(10003); - StpUtil.login(10004); - StpUtil.login(10005); - - // 查询 - List list = StpUtil.searchTokenValue("", 0, 10); - Assert.assertTrue(list.size() >= 5); - } - - // 测试:临时验证模块 - @Test - public void testSaTemp() { - // 生成token - String token = SaTempUtil.createToken("group-1014", 200); - Assert.assertNotNull(token); - - // 解析token - String value = SaTempUtil.parseToken(token, String.class); - Assert.assertEquals(value, "group-1014"); - - // 过期时间 - long timeout = SaTempUtil.getTimeout(token); - Assert.assertTrue(timeout > 195); - } - - // 测试:二级认证 - @Test - public void testSafe() throws InterruptedException { - // 登录 - StpUtil.login(10001); - Assert.assertFalse(StpUtil.isSafe()); - - // 开启二级认证 - StpUtil.openSafe(2); - Assert.assertTrue(StpUtil.isSafe()); - Assert.assertTrue(StpUtil.getSafeTime() > 0); - - // 自然结束 - Thread.sleep(2500); - Assert.assertFalse(StpUtil.isSafe()); - - // 手动结束 - StpUtil.openSafe(2); - StpUtil.closeSafe(); - Assert.assertFalse(StpUtil.isSafe()); - } - -} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/pom.xml b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/pom.xml new file mode 100644 index 000000000..cc1b7a14a --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + jar + + sa-token-spring-boot-webmvc-reactor-v2v3v4-common + sa-token-spring-boot-webmvc-reactor-v2v3v4-common + sa-token springboot webmvc/reactor v2/v3/v4 common + + + + + + org.springframework.boot + spring-boot-starter + true + + + org.springframework + spring-webmvc + true + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + cn.dev33 + sa-token-sso + true + + + + + cn.dev33 + sa-token-oauth2 + true + + + + + cn.dev33 + sa-token-apikey + true + + + + + cn.dev33 + sa-token-sign + true + + + + + + + + + + + cn.dev33 + sa-token-spring-boot2-dependencies + ${revision} + pom + import + + + + + + diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/package-info.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/package-info.java new file mode 100644 index 000000000..f14171841 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * Sa-Token 集成 SpringBoot 的各个组件 + */ +package cn.dev33.satoken; \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java new file mode 100644 index 000000000..29cd03714 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java @@ -0,0 +1,273 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; +import cn.dev33.satoken.http.SaHttpTemplate; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate; +import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; +import cn.dev33.satoken.json.SaJsonTemplate; +import cn.dev33.satoken.listener.SaTokenEventCenter; +import cn.dev33.satoken.listener.SaTokenListener; +import cn.dev33.satoken.log.SaLog; +import cn.dev33.satoken.plugin.SaTokenPlugin; +import cn.dev33.satoken.plugin.SaTokenPluginHolder; +import cn.dev33.satoken.same.SaSameTemplate; +import cn.dev33.satoken.secure.totp.SaTotpTemplate; +import cn.dev33.satoken.serializer.SaSerializerTemplate; +import cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder; +import cn.dev33.satoken.stp.StpInterface; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook; +import cn.dev33.satoken.temp.SaTempTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.util.PathMatcher; + +import java.util.List; + +/** + * 注入 Sa-Token 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +public class SaBeanInject { + + /** + * 组件注入 + *

    为确保 Log 组件正常打印,必须将 SaLog 和 SaTokenConfig 率先初始化

    + * + * @param log log 对象 + * @param saTokenConfig 配置对象 + */ + public SaBeanInject( + @Autowired(required = false) SaLog log, + @Autowired(required = false) SaTokenConfig saTokenConfig, + @Autowired(required = false) SaTokenPluginHolder pluginHolder + ){ + if(log != null) { + SaManager.setLog(log); + } + if(saTokenConfig != null) { + SaManager.setConfig(saTokenConfig); + } + // 初始化 Sa-Token SPI 插件 + if (pluginHolder == null) { + pluginHolder = SaTokenPluginHolder.instance; + } + pluginHolder.init(); + SaTokenPluginHolder.instance = pluginHolder; + } + + /** + * 注入持久化Bean + * + * @param saTokenDao SaTokenDao对象 + */ + @Autowired(required = false) + public void setSaTokenDao(SaTokenDao saTokenDao) { + SaManager.setSaTokenDao(saTokenDao); + } + + /** + * 注入权限认证Bean + * + * @param stpInterface StpInterface对象 + */ + @Autowired(required = false) + public void setStpInterface(StpInterface stpInterface) { + SaManager.setStpInterface(stpInterface); + } + + /** + * 注入上下文Bean + * + * @param saTokenContext SaTokenContext对象 + */ + @Autowired(required = false) + public void setSaTokenContext(SaTokenContext saTokenContext) { + SaManager.setSaTokenContext(saTokenContext); + } + + /** + * 注入侦听器Bean + * + * @param listenerList 侦听器集合 + */ + @Autowired(required = false) + public void setSaTokenListener(List listenerList) { + SaTokenEventCenter.registerListenerList(listenerList); + } + + /** + * 注入自定义注解处理器 + * + * @param handlerList 自定义注解处理器集合 + */ + @Autowired(required = false) + public void setSaAnnotationHandler(List> handlerList) { + for (SaAnnotationHandlerInterface handler : handlerList) { + SaAnnotationStrategy.instance.registerAnnotationHandler(handler); + } + } + + /** + * 注入临时令牌验证模块 Bean + * + * @param saTempTemplate / + */ + @Autowired(required = false) + public void setSaTempTemplate(SaTempTemplate saTempTemplate) { + SaManager.setSaTempTemplate(saTempTemplate); + } + + /** + * 注入 Same-Token 模块 Bean + * + * @param saSameTemplate saSameTemplate对象 + */ + @Autowired(required = false) + public void setSaIdTemplate(SaSameTemplate saSameTemplate) { + SaManager.setSaSameTemplate(saSameTemplate); + } + + /** + * 注入 Sa-Token Http Basic 认证模块 + * + * @param saBasicTemplate saBasicTemplate对象 + */ + @Autowired(required = false) + public void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) { + SaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate; + } + + /** + * 注入 Sa-Token Http Digest 认证模块 + * + * @param saHttpDigestTemplate saHttpDigestTemplate 对象 + */ + @Autowired(required = false) + public void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) { + SaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate; + } + + /** + * 注入自定义的 JSON 转换器 Bean + * + * @param saJsonTemplate JSON 转换器 + */ + @Autowired(required = false) + public void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) { + SaManager.setSaJsonTemplate(saJsonTemplate); + } + + /** + * 注入自定义的 Http 转换器 Bean + * + * @param saHttpTemplate / + */ + @Autowired(required = false) + public void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) { + SaManager.setSaHttpTemplate(saHttpTemplate); + } + + /** + * 注入自定义的序列化器 Bean + * + * @param saSerializerTemplate 序列化器 + */ + @Autowired(required = false) + public void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) { + SaManager.setSaSerializerTemplate(saSerializerTemplate); + } + + /** + * 注入自定义的 TOTP 算法 Bean + * + * @param totpTemplate TOTP 算法类 + */ + @Autowired(required = false) + public void setSaTotpTemplate(SaTotpTemplate totpTemplate) { + SaManager.setSaTotpTemplate(totpTemplate); + } + + /** + * 注入自定义的 StpLogic + * @param stpLogic / + */ + @Autowired(required = false) + public void setStpLogic(StpLogic stpLogic) { + StpUtil.setStpLogic(stpLogic); + } + + /** + * 利用自动注入特性,获取Spring框架内部使用的路由匹配器 + * + * @param pathMatcher 要设置的 pathMatcher + */ + @Autowired(required = false) + @Qualifier("mvcPathMatcher") + public void setPathMatcher(PathMatcher pathMatcher) { + SaPathMatcherHolder.setPathMatcher(pathMatcher); + } + + /** + * 注入自定义防火墙校验 hook 集合 + * + * @param hooks / + */ + @Autowired(required = false) + public void setSaFirewallCheckHooks(List hooks) { + for (SaFirewallCheckHook hook : hooks) { + SaFirewallStrategy.instance.registerHook(hook); + } + } + + /** + * 注入CORS 策略处理函数 + * + * @param corsHandle / + */ + @Autowired(required = false) + public void setCorsHandle(SaCorsHandleFunction corsHandle) { + SaStrategy.instance.corsHandle = corsHandle; + } + + /** + * 注入自定义插件集合 + * + * @param plugins / + */ + @Autowired(required = false) + public void setSaTokenPluginList(List plugins) { + for (SaTokenPlugin plugin : plugins) { + SaTokenPluginHolder.instance.installPlugin(plugin); + } + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java new file mode 100644 index 000000000..9f9ade71f --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; + +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.spring.context.path.ApplicationContextPathLoading; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 注册Sa-Token所需要的Bean + *

    Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖 + * @author click33 + * + */ +public class SaBeanRegister { + + /** + * 获取配置Bean + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties(prefix = "sa-token") + public SaTokenConfig getSaTokenConfig() { + return new SaTokenConfig(); + } + + /** + * 应用上下文路径加载器 + * @return / + */ + @Bean + public ApplicationContextPathLoading getApplicationContextPathLoading() { + return new ApplicationContextPathLoading(); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanInject.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanInject.java new file mode 100644 index 000000000..e3a808418 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanInject.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.apikey; + +import cn.dev33.satoken.apikey.SaApiKeyManager; +import cn.dev33.satoken.apikey.config.SaApiKeyConfig; +import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; +import cn.dev33.satoken.apikey.template.SaApiKeyTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +/** + * 注入 Sa-Token API Key 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@ConditionalOnClass(SaApiKeyManager.class) +public class SaApiKeyBeanInject { + + /** + * 注入 API Key 配置对象 + * + * @param saApiKeyConfig 配置对象 + */ + @Autowired(required = false) + public void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) { + SaApiKeyManager.setConfig(saApiKeyConfig); + } + + /** + * 注入自定义的 API Key 模版方法 Bean + * + * @param apiKeyTemplate / + */ + @Autowired(required = false) + public void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) { + SaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate); + } + + /** + * 注入自定义的 API Key 数据加载器 Bean + * + * @param apiKeyDataLoader / + */ + @Autowired(required = false) + public void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) { + SaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanRegister.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanRegister.java new file mode 100644 index 000000000..5a8c7ed98 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanRegister.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.apikey; + +import cn.dev33.satoken.apikey.SaApiKeyManager; +import cn.dev33.satoken.apikey.config.SaApiKeyConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 注册 Sa-Token API Key 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@ConditionalOnClass(SaApiKeyManager.class) +public class SaApiKeyBeanRegister { + + /** + * 获取 API Key 配置对象 + * @return 配置对象 + */ + @Bean + @ConfigurationProperties(prefix = "sa-token.api-key") + public SaApiKeyConfig getSaApiKeyConfig() { + return new SaApiKeyConfig(); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/package-info.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/package-info.java new file mode 100644 index 000000000..35dc00600 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * sa-token-apikey 模块自动化配置(只有引入了 sa-token-apikey 模块后,此包下的代码才会开始工作) + */ +package cn.dev33.satoken.spring.apikey; \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/context/path/ApplicationContextPathLoading.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/context/path/ApplicationContextPathLoading.java new file mode 100644 index 000000000..aca58c179 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/context/path/ApplicationContextPathLoading.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.context.path; + +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +/** + * 应用上下文路径加载器 + * + * @author click33 + * @since 1.37.0 + */ +public class ApplicationContextPathLoading implements ApplicationRunner { + + @Value("${server.servlet.context-path:}") + String contextPath; + + @Value("${spring.mvc.servlet.path:}") + String servletPath; + + @Override + public void run(ApplicationArguments args) throws Exception { + + String routePrefix = ""; + + if(SaFoxUtil.isNotEmpty(contextPath)) { + if(! contextPath.startsWith("/")){ + contextPath = "/" + contextPath; + } + if (contextPath.endsWith("/")) { + contextPath = contextPath.substring(0, contextPath.length() - 1); + } + routePrefix += contextPath; + } + + if(SaFoxUtil.isNotEmpty(servletPath)) { + if(! servletPath.startsWith("/")){ + servletPath = "/" + servletPath; + } + if (servletPath.endsWith("/")) { + servletPath = servletPath.substring(0, servletPath.length() - 1); + } + routePrefix += servletPath; + } + + if(SaFoxUtil.isNotEmpty(routePrefix) && ! routePrefix.equals("/") ){ + ApplicationInfo.routePrefix = routePrefix; + } + } + +} \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java new file mode 100644 index 000000000..c479a7989 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java @@ -0,0 +1,153 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.oauth2; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; +import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; +import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; +import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; +import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver; +import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; +import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; +import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface; +import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; +import cn.dev33.satoken.oauth2.template.SaOAuth2Template; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +import java.util.List; + + +// 小提示:如果你在 idea 中运行源码时出现异常:java: 程序包cn.dev33.satoken.oauth2不存在。 +// 在项目根目录进入 cmd,执行 mvn package 即可解决 + + +/** + * 注入 Sa-Token-OAuth2 所需要的组件 + * + * @author click33 + * @since 1.34.0 + */ +@ConditionalOnClass(SaOAuth2Manager.class) +public class SaOAuth2BeanInject { + + /** + * 注入 OAuth2 配置对象 + * + * @param saOAuth2Config 配置对象 + */ + @Autowired(required = false) + public void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) { + SaOAuth2Manager.setServerConfig(saOAuth2Config); + } + + /** + * 注入 OAuth2 模板代码类 + * + * @param saOAuth2Template 模板代码类 + */ + @Autowired(required = false) + public void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) { + SaOAuth2Manager.setTemplate(saOAuth2Template); + } + + /** + * 注入 OAuth2 请求处理器 + * + * @param serverProcessor 请求处理器 + */ + @Autowired(required = false) + public void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) { + SaOAuth2ServerProcessor.instance = serverProcessor; + } + + /** + * 注入 OAuth2 数据加载器 + * + * @param dataLoader / + */ + @Autowired(required = false) + public void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) { + SaOAuth2Manager.setDataLoader(dataLoader); + } + + /** + * 注入 OAuth2 数据解析器 Bean + * + * @param dataResolver / + */ + @Autowired(required = false) + public void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) { + SaOAuth2Manager.setDataResolver(dataResolver); + } + + /** + * 注入 OAuth2 数据格式转换器 Bean + * + * @param dataConverter / + */ + @Autowired(required = false) + public void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) { + SaOAuth2Manager.setDataConverter(dataConverter); + } + + /** + * 注入 OAuth2 数据构建器 Bean + * + * @param dataGenerate / + */ + @Autowired(required = false) + public void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) { + SaOAuth2Manager.setDataGenerate(dataGenerate); + } + + /** + * 注入 OAuth2 数据持久 Bean + * + * @param dao / + */ + @Autowired(required = false) + public void setSaOAuth2Dao(SaOAuth2Dao dao) { + SaOAuth2Manager.setDao(dao); + } + + /** + * 注入自定义 scope 处理器 + * + * @param handlerList 自定义 scope 处理器集合 + */ + @Autowired(required = false) + public void setSaOAuth2ScopeHandler(List handlerList) { + for (SaOAuth2ScopeHandlerInterface handler : handlerList) { + SaOAuth2Strategy.instance.registerScopeHandler(handler); + } + } + + /** + * 注入自定义 grant_type 处理器 + * + * @param handlerList 自定义 grant_type 处理器集合 + */ + @Autowired(required = false) + public void setSaOAuth2GrantTypeHandlerInterface(List handlerList) { + for (SaOAuth2GrantTypeHandlerInterface handler : handlerList) { + SaOAuth2Strategy.instance.registerGrantTypeHandler(handler); + } + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanRegister.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanRegister.java new file mode 100644 index 000000000..83e696291 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanRegister.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.oauth2; + +import cn.dev33.satoken.oauth2.SaOAuth2Manager; +import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 注册 Sa-Token-OAuth2 所需要的Bean + * + * @author click33 + * @since 1.34.0 + */ +@ConditionalOnClass(SaOAuth2Manager.class) +public class SaOAuth2BeanRegister { + + /** + * 获取 OAuth2 配置 Bean + * + * @return 配置对象 + */ + @Bean + @ConfigurationProperties(prefix = "sa-token.oauth2-server") + public SaOAuth2ServerConfig getSaOAuth2Config() { + return new SaOAuth2ServerConfig(); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/package-info.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/package-info.java new file mode 100644 index 000000000..be49fffc3 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * Sa-Token-OAuth2 模块自动化配置(只有引入了Sa-Token-OAuth2模块后,此包下的代码才会开始工作) + */ +package cn.dev33.satoken.spring.oauth2; \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathMatcherHolder.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathMatcherHolder.java new file mode 100644 index 000000000..50297efdf --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathMatcherHolder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.pathmatch; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; + +/** + * 路由匹配工具类:持有 PathMatcher 全局引用,方便快捷的调用 PathMatcher 相关方法 + * + * @author click33 + * @since 1.34.0 + */ +public class SaPathMatcherHolder { + + private SaPathMatcherHolder() { + } + + /** + * 路由匹配器 + */ + public static PathMatcher pathMatcher; + + /** + * 获取路由匹配器 + * @return 路由匹配器 + */ + public static PathMatcher getPathMatcher() { + if(pathMatcher == null) { + pathMatcher = new AntPathMatcher(); + } + return pathMatcher; + } + + /** + * 写入路由匹配器 + * @param pathMatcher 路由匹配器 + */ + public static void setPathMatcher(PathMatcher pathMatcher) { + SaPathMatcherHolder.pathMatcher = pathMatcher; + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathPatternParserUtil.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathPatternParserUtil.java new file mode 100644 index 000000000..e7a42f7d1 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathPatternParserUtil.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.pathmatch; + +import org.springframework.http.server.PathContainer; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * 路由匹配工具类:使用 PathPatternParser 模式匹配 + * + * @author click33 + * @since 1.35.1 + */ +public class SaPathPatternParserUtil { + + private SaPathPatternParserUtil() { + } + + /** + * 判断:指定路由匹配符是否可以匹配成功指定路径 + * @param pattern 路由匹配符 + * @param path 要匹配的路径 + * @return 是否匹配成功 + */ + public static boolean match(String pattern, String path) { + PathPattern pathPattern = PathPatternParser.defaultInstance.parse(pattern); + PathContainer pathContainer = PathContainer.parsePath(path); + return pathPattern.matches(pathContainer); + } + + /* + 表现: + springboot 2.x SpringMVC match("/test/test", "/test/test/") // true + springboot 2.x WebFlux match("/test/test", "/test/test/") // true + springboot 3.x SpringMVC match("/test/test", "/test/test/") // false + springboot 3.x WebFlux match("/test/test", "/test/test/") // false + */ + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPatternsRequestConditionHolder.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPatternsRequestConditionHolder.java new file mode 100644 index 000000000..ef890d8e8 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPatternsRequestConditionHolder.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.pathmatch; + +import cn.dev33.satoken.exception.SaTokenException; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * 路由匹配工具类 + * + * @author click33 + * @since 1.35.1 + */ +public class SaPatternsRequestConditionHolder { + + private SaPatternsRequestConditionHolder() { + } + + public static PatternsRequestCondition patternsRequestCondition; + + public static Method matcherMethod; + + static { + try { + patternsRequestCondition = new PatternsRequestCondition(); + matcherMethod = PatternsRequestCondition.class.getDeclaredMethod("getMatchingPattern", String.class, String.class); + matcherMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new SaTokenException("路由匹配器初始化失败", e); + } + } + + /** + * 判断:指定路由匹配符是否可以匹配成功指定路径 + * @param pattern 路由匹配符 + * @param lookupPath 要匹配的路径 + * @return 是否匹配成功 + */ + public static boolean match(String pattern, String lookupPath) { + try { + return matcherMethod.invoke(patternsRequestCondition, pattern, lookupPath) != null; + } catch (IllegalAccessException | InvocationTargetException e) { + throw new SaTokenException("路由匹配器调用失败", e); + } + } + + /* + 性能测试: + 100万次 + new 对象方式,耗时:3.685s 最慢 + 反射调方法方式,耗时:1.311s 中等 + 原始方式,耗时:0.445s 最快,但有bug + */ + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanInject.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanInject.java new file mode 100644 index 000000000..1ab9c5524 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanInject.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.sign; + +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; +import cn.dev33.satoken.sign.template.SaSignTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +/** + * 注入 Sa-Token API 参数签名 所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@ConditionalOnClass(SaSignManager.class) +public class SaSignBeanInject { + + /** + * 注入 API 参数签名配置对象 + * + * @param saSignConfig 配置对象 + */ + @Autowired(required = false) + public void setSignConfig(SaSignConfig saSignConfig) { + SaSignManager.setConfig(saSignConfig); + } + + /** + * 注入 API 参数签名配置对象 + * + * @param signManyConfigWrapper 配置对象 + */ + @Autowired(required = false) + public void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) { + SaSignManager.setSignMany(signManyConfigWrapper.getSignMany()); + } + + /** + * 注入自定义的 参数签名 模版方法 Bean + * + * @param saSignTemplate 参数签名 Bean + */ + @Autowired(required = false) + public void setSaSignTemplate(SaSignTemplate saSignTemplate) { + SaSignManager.setSaSignTemplate(saSignTemplate); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanRegister.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanRegister.java new file mode 100644 index 000000000..7c15590bd --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanRegister.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.sign; + +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 注册 Sa-Token API 参数签名所需要的 Bean + * + * @author click33 + * @since 1.43.0 + */ +@ConditionalOnClass(SaSignManager.class) +public class SaSignBeanRegister { + + /** + * 获取 API 参数签名配置对象 + * @return 配置对象 + */ + @Bean + @ConfigurationProperties(prefix = "sa-token.sign") + public SaSignConfig getSaSignConfig() { + return new SaSignConfig(); + } + + /** + * 获取 API 参数签名 Many 配置对象 + * @return 配置对象 + */ + @Bean + @ConfigurationProperties(prefix = "sa-token") + public SaSignManyConfigWrapper getSaSignManyConfigWrapper() { + return new SaSignManyConfigWrapper(); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/package-info.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/package-info.java new file mode 100644 index 000000000..ba500959d --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * sa-token-sign 模块自动化配置(只有引入了 sa-token-sign 模块后,此包下的代码才会开始工作) + */ +package cn.dev33.satoken.spring.sign; \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanInject.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanInject.java new file mode 100644 index 000000000..61d262aa2 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanInject.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.sso; + +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +/** + * 注入 Sa-Token SSO 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +@ConditionalOnClass(SaSsoManager.class) +public class SaSsoBeanInject { + + /** + * 注入 Sa-Token SSO Server 端 配置类 + * + * @param serverConfig 配置对象 + */ + @Autowired(required = false) + public void setSaSsoServerConfig(SaSsoServerConfig serverConfig) { + SaSsoManager.setServerConfig(serverConfig); + } + + /** + * 注入 Sa-Token SSO Client 端 配置类 + * + * @param clientConfig 配置对象 + */ + @Autowired(required = false) + public void setSaSsoClientConfig(SaSsoClientConfig clientConfig) { + SaSsoManager.setClientConfig(clientConfig); + } + + /** + * 注入 SSO 模板代码类 (Server 端) + * + * @param ssoServerTemplate / + */ + @Autowired(required = false) + public void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) { + SaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate; + } + + /** + * 注入 SSO 模板代码类 (Client 端) + * + * @param ssoClientTemplate / + */ + @Autowired(required = false) + public void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) { + SaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate; + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanRegister.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanRegister.java new file mode 100644 index 000000000..3d35c35f8 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanRegister.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring.sso; + +import cn.dev33.satoken.sso.SaSsoManager; +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import cn.dev33.satoken.sso.config.SaSsoServerConfig; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientTemplate; +import cn.dev33.satoken.sso.template.SaSsoServerTemplate; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 注册 Sa-Token SSO 所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +@ConditionalOnClass(SaSsoManager.class) +public class SaSsoBeanRegister { + + /** + * 获取 SSO Server 端 配置对象 + * @return 配置对象 + */ + @Bean + @ConfigurationProperties(prefix = "sa-token.sso-server") + public SaSsoServerConfig getSaSsoServerConfig() { + return new SaSsoServerConfig(); + } + + /** + * 获取 SSO Client 端 配置对象 + * @return 配置对象 + */ + @Bean + @ConfigurationProperties(prefix = "sa-token.sso-client") + public SaSsoClientConfig getSaSsoClientConfig() { + return new SaSsoClientConfig(); + } + + /** + * 获取 SSO Server 端 SaSsoServerTemplate + * + * @return / + */ + @Bean + @ConditionalOnMissingBean(SaSsoServerTemplate.class) + public SaSsoServerTemplate getSaSsoServerTemplate() { + return SaSsoServerProcessor.instance.ssoServerTemplate; + } + + /** + * 获取 SSO Client 端 SaSsoClientTemplate + * + * @return / + */ + @Bean + @ConditionalOnMissingBean(SaSsoClientTemplate.class) + public SaSsoClientTemplate getSaSsoClientTemplate() { + return SaSsoClientProcessor.instance.ssoClientTemplate; + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/package-info.java b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/package-info.java new file mode 100644 index 000000000..d8ab00b35 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * Sa-Token-SSO 模块自动化配置(只有引入了 sa-token-sso 模块后,此包下的代码才会开始工作) + */ +package cn.dev33.satoken.spring.sso; \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring.factories b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..d592afc2f --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,11 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.dev33.satoken.spring.SaBeanRegister,\ +cn.dev33.satoken.spring.SaBeanInject,\ +cn.dev33.satoken.spring.sso.SaSsoBeanRegister,\ +cn.dev33.satoken.spring.sso.SaSsoBeanInject,\ +cn.dev33.satoken.spring.oauth2.SaOAuth2BeanRegister,\ +cn.dev33.satoken.spring.oauth2.SaOAuth2BeanInject,\ +cn.dev33.satoken.spring.apikey.SaApiKeyBeanRegister,\ +cn.dev33.satoken.spring.apikey.SaApiKeyBeanInject,\ +cn.dev33.satoken.spring.sign.SaSignBeanRegister,\ +cn.dev33.satoken.spring.sign.SaSignBeanInject \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..b227d7422 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,10 @@ +cn.dev33.satoken.spring.SaBeanRegister +cn.dev33.satoken.spring.SaBeanInject +cn.dev33.satoken.spring.sso.SaSsoBeanRegister +cn.dev33.satoken.spring.sso.SaSsoBeanInject +cn.dev33.satoken.spring.oauth2.SaOAuth2BeanRegister +cn.dev33.satoken.spring.oauth2.SaOAuth2BeanInject +cn.dev33.satoken.spring.apikey.SaApiKeyBeanRegister +cn.dev33.satoken.spring.apikey.SaApiKeyBeanInject +cn.dev33.satoken.spring.sign.SaSignBeanRegister +cn.dev33.satoken.spring.sign.SaSignBeanInject \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/pom.xml b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/pom.xml new file mode 100644 index 000000000..e5dcc6377 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + jar + + sa-token-spring-boot-webmvc-v3v4-common + sa-token-spring-boot-webmvc-v3v4-common + sa-token springboot webmvc v3v4 common + + + + + org.springframework.boot + spring-boot-starter-web + true + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + cn.dev33 + sa-token-jakarta-servlet + + + + + cn.dev33 + sa-token-spring-boot-webmvc-reactor-v2v3v4-common + + + + + + + + + + cn.dev33 + sa-token-spring-boot3-dependencies + ${revision} + pom + import + + + + + + diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForJakartaServlet.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForJakartaServlet.java new file mode 100644 index 000000000..926a5e3c4 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForJakartaServlet.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.FirewallCheckException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.servlet.model.SaResponseForServlet; +import cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil; +import cn.dev33.satoken.strategy.SaFirewallStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.annotation.Order; + +import java.io.IOException; + +/** + * 防火墙校验过滤器 (基于 Jakarta-Servlet) + * + * @author click33 + * @since 1.37.0 + */ +@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) +public class SaFirewallCheckFilterForJakartaServlet implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + SaRequestForServlet saRequest = new SaRequestForServlet(req); + SaResponseForServlet saResponse = new SaResponseForServlet(res); + + try { + SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaJakartaServletOperateUtil.writeResult(response, e.getMessage()); + return; + } + catch (FirewallCheckException e) { + if(SaFirewallStrategy.instance.checkFailHandle == null) { + SaJakartaServletOperateUtil.writeResult(response, e.getMessage()); + } else { + SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); + } + return; + } + // 更多异常则不处理,交由 Web 框架处理 + + // 向内执行 + chain.doFilter(request, response); + } + + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java new file mode 100644 index 000000000..cce0782ee --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java @@ -0,0 +1,146 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.*; +import org.springframework.core.annotation.Order; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 全局鉴权过滤器 (基于 Jakarta-Servlet) + *

    + * 默认优先级为 -100,尽量保证在其它过滤器之前执行 + *

    + * + * @author click33 + * @since 1.34.0 + */ +@Order(SaTokenConsts.ASSEMBLY_ORDER) +public class SaServletFilter implements SaFilter, Filter { + + // ------------------------ 设置此过滤器 拦截 & 放行 的路由 + + /** + * 拦截路由 + */ + public List includeList = new ArrayList<>(); + + /** + * 放行路由 + */ + public List excludeList = new ArrayList<>(); + + @Override + public SaServletFilter addInclude(String... paths) { + includeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaServletFilter addExclude(String... paths) { + excludeList.addAll(Arrays.asList(paths)); + return this; + } + + @Override + public SaServletFilter setIncludeList(List pathList) { + includeList = pathList; + return this; + } + + @Override + public SaServletFilter setExcludeList(List pathList) { + excludeList = pathList; + return this; + } + + + // ------------------------ 钩子函数 + + /** + * 认证函数:每次请求执行 + */ + public SaFilterAuthStrategy auth = r -> {}; + + /** + * 异常处理函数:每次[认证函数]发生异常时执行此函数 + */ + public SaFilterErrorStrategy error = e -> { + throw new SaTokenException(e); + }; + + /** + * 前置函数:在每次[认证函数]之前执行 + * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth + */ + public SaFilterAuthStrategy beforeAuth = r -> {}; + + @Override + public SaServletFilter setAuth(SaFilterAuthStrategy auth) { + this.auth = auth; + return this; + } + + @Override + public SaServletFilter setError(SaFilterErrorStrategy error) { + this.error = error; + return this; + } + + @Override + public SaServletFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { + this.beforeAuth = beforeAuth; + return this; + } + + + // ------------------------ doFilter + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + try { + // 执行全局过滤器 + beforeAuth.run(null); + SaRouter.match(includeList).notMatch(excludeList).check(r -> { + auth.run(null); + }); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaJakartaServletOperateUtil.writeResult(response, e.getMessage()); + return; + } + catch (Throwable e) { + SaJakartaServletOperateUtil.writeResult(response, String.valueOf(error.run(e))); + return; + } + + // 执行 + chain.doFilter(request, response); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForJakartaServlet.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForJakartaServlet.java new file mode 100644 index 000000000..fc938fd75 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForJakartaServlet.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; + +import cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.annotation.Order; + +import java.io.IOException; + +/** + * SaTokenContext 上下文初始化过滤器 (基于 Jakarta-Servlet) + * + * @author click33 + * @since 1.42.0 + */ +@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) +public class SaTokenContextFilterForJakartaServlet implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + SaTokenContextJakartaServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response); + chain.doFilter(request, response); + } finally { + SaTokenContextJakartaServletUtil.clearContext(); + } + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForJakartaServlet.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForJakartaServlet.java new file mode 100644 index 000000000..43f7e1e90 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForJakartaServlet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.filter; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaTokenContextModelBox; +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.*; +import org.springframework.core.annotation.Order; + +import java.io.IOException; + +/** + * CORS 跨域策略过滤器 (基于 Jakarta-Servlet) + * + * @author click33 + * @since 1.42.0 + */ +@Order(SaTokenConsts.CORS_FILTER_ORDER) +public class SaTokenCorsFilterForJakartaServlet implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + try { + SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); + SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); + } + catch (StopMatchException ignored) {} + catch (BackResultException e) { + SaJakartaServletOperateUtil.writeResult(response, e.getMessage()); + return; + } + + chain.doFilter(request, response); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java new file mode 100644 index 000000000..d8de05106 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java @@ -0,0 +1,122 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.interceptor; + +import cn.dev33.satoken.exception.BackResultException; +import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.lang.reflect.Method; + +/** + * Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力 + * + * @author click33 + * @since 1.34.0 + */ +public class SaInterceptor implements HandlerInterceptor { + + /** + * 是否打开注解鉴权 + */ + public boolean isAnnotation = true; + + /** + * 认证函数:每次请求执行 + *

    参数:路由处理函数指针 + */ + public SaParamFunction auth = handler -> {}; + + /** + * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 + */ + public SaInterceptor() { + } + + /** + * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 + * @param auth 认证函数,每次请求执行 + */ + public SaInterceptor(SaParamFunction auth) { + this.auth = auth; + } + + /** + * 设置是否打开注解鉴权 + * @param isAnnotation / + * @return 对象自身 + */ + public SaInterceptor isAnnotation(boolean isAnnotation) { + this.isAnnotation = isAnnotation; + return this; + } + + /** + * 写入[认证函数]: 每次请求执行 + * @param auth / + * @return 对象自身 + */ + public SaInterceptor setAuth(SaParamFunction auth) { + this.auth = auth; + return this; + } + + + // ----------------- 验证方法 ----------------- + + /** + * 每次请求之前触发的方法 + */ + @Override + @SuppressWarnings("all") + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + try { + + // 这里必须确保 handler 是 HandlerMethod 类型时,才能进行注解鉴权 + if(isAnnotation && handler instanceof HandlerMethod) { + Method method = ((HandlerMethod) handler).getMethod(); + SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); + } + + // Auth 校验 + auth.run(handler); + + } catch (StopMatchException e) { + // StopMatchException 异常代表:停止匹配,进入Controller + + } catch (BackResultException e) { + // BackResultException 异常代表:停止匹配,向前端输出结果 + // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 back 前自行设置 Content-Type 为 application/json + // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); + if(response.getContentType() == null) { + response.setContentType("text/plain; charset=utf-8"); + } + response.getWriter().print(e.getMessage()); + return false; + } + + // 通过验证 + return true; + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/package-info.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/package-info.java new file mode 100644 index 000000000..aa2ae7975 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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. + */ +/** + * Sa-Token 集成 SpringBoot3 的各个组件 + */ +package cn.dev33.satoken; \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpringInJakartaServlet.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpringInJakartaServlet.java new file mode 100644 index 000000000..adf142bf6 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpringInJakartaServlet.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; + +import cn.dev33.satoken.context.SaTokenContextForReadOnly; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.context.model.SaResponse; +import cn.dev33.satoken.context.model.SaStorage; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.servlet.model.SaResponseForServlet; +import cn.dev33.satoken.servlet.model.SaStorageForServlet; + +/** + *

    此为低版本(<1.42.0) 的上下文处理方案,基于 Spring 内部工具类 RequestContextHolder 读写上下文,仅做留档,如无必要请勿使用

    + * + * Sa-Token 上下文处理器 [ SpringBoot3 Jakarta Servlet 版 ],在 SpringBoot3 中使用 Sa-Token 时,必须注入此实现类,否则会出现上下文无效异常 + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenContextForSpringInJakartaServlet implements SaTokenContextForReadOnly { + + /** + * 获取当前请求的 Request 包装对象 + */ + @Override + public SaRequest getRequest() { + return new SaRequestForServlet(SpringMVCUtil.getRequest()); + } + + /** + * 获取当前请求的 Response 包装对象 + */ + @Override + public SaResponse getResponse() { + return new SaResponseForServlet(SpringMVCUtil.getResponse()); + } + + /** + * 获取当前请求的 Storage 包装对象 + */ + @Override + public SaStorage getStorage() { + return new SaStorageForServlet(SpringMVCUtil.getRequest()); + } + + /** + * 判断:在本次请求中,此上下文是否可用。 + */ + @Override + public boolean isValid() { + return SpringMVCUtil.isWeb(); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java new file mode 100644 index 000000000..c3205345f --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; + +import cn.dev33.satoken.filter.SaFirewallCheckFilterForJakartaServlet; +import cn.dev33.satoken.filter.SaTokenContextFilterForJakartaServlet; +import cn.dev33.satoken.filter.SaTokenCorsFilterForJakartaServlet; +import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil; +import cn.dev33.satoken.strategy.SaStrategy; +import org.springframework.context.annotation.Bean; + +/** + * 注册 Sa-Token 框架所需要的 Bean + * + * @author click33 + * @since 1.34.0 + */ +public class SaTokenContextRegister { + + public SaTokenContextRegister() { + // 重写路由匹配算法 + SaStrategy.instance.routeMatcher = (pattern, path) -> { + return SaPathPatternParserUtil.match(pattern, path); + }; + } + + /** + * 上下文过滤器 + * + * @return / + */ + @Bean + public SaTokenContextFilterForJakartaServlet saTokenContextFilterForServlet() { + return new SaTokenContextFilterForJakartaServlet(); + } + + /** + * CORS 跨域策略过滤器 + * + * @return / + */ + @Bean + public SaTokenCorsFilterForJakartaServlet saTokenCorsFilterForJakartaServlet() { + return new SaTokenCorsFilterForJakartaServlet(); + } + + /** + * 防火墙过滤器 + * + * @return / + */ + @Bean + public SaFirewallCheckFilterForJakartaServlet saFirewallCheckFilterForJakartaServlet() { + return new SaFirewallCheckFilterForJakartaServlet(); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java new file mode 100644 index 000000000..70c290537 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.spring; + +import cn.dev33.satoken.exception.NotWebContextException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * SpringMVC 相关操作工具类,快速获取当前会话的 HttpServletRequest、HttpServletResponse 对象 + * + * @author click33 + * @since 1.34.0 + */ +public class SpringMVCUtil { + + private SpringMVCUtil() { + } + + /** + * 获取当前会话的 request + * @return request + */ + public static HttpServletRequest getRequest() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if(servletRequestAttributes == null) { + throw new NotWebContextException("非 web 上下文无法获取 HttpServletRequest"); + } + return servletRequestAttributes.getRequest(); + } + + /** + * 获取当前会话的 response + * @return response + */ + public static HttpServletResponse getResponse() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if(servletRequestAttributes == null) { + throw new NotWebContextException("非 web 上下文无法获取 HttpServletRequest"); + } + return servletRequestAttributes.getResponse(); + } + + /** + * 判断当前是否处于 Web 上下文中 + * @return request + */ + public static boolean isWeb() { + return RequestContextHolder.getRequestAttributes() != null; + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..4f9e5fdb9 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.spring.SaTokenContextRegister diff --git a/sa-token-starter/sa-token-spring-boot3-starter/pom.xml b/sa-token-starter/sa-token-spring-boot3-starter/pom.xml new file mode 100644 index 000000000..cdd9f8300 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot3-starter/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + jar + + sa-token-spring-boot3-starter + sa-token-spring-boot3-starter + springboot3 integrate sa-token + + + + + + cn.dev33 + sa-token-spring-boot-webmvc-v3v4-common + + + + + cn.dev33 + sa-token-jackson + + + + + + + + + + cn.dev33 + sa-token-spring-boot3-dependencies + ${revision} + pom + import + + + + + + + diff --git a/sa-token-starter/sa-token-spring-boot3-starter/src/main/java/cn/dev33/satoken/Placeholder.java b/sa-token-starter/sa-token-spring-boot3-starter/src/main/java/cn/dev33/satoken/Placeholder.java new file mode 100644 index 000000000..60dace90d --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot3-starter/src/main/java/cn/dev33/satoken/Placeholder.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; + + +/** + * 占位符 + * + * @author click33 + * @since 1.45.0 + */ +public class Placeholder { + +} diff --git a/sa-token-starter/sa-token-spring-boot4-starter/pom.xml b/sa-token-starter/sa-token-spring-boot4-starter/pom.xml new file mode 100644 index 000000000..906e73b82 --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot4-starter/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-starter + ${revision} + ../pom.xml + + jar + + sa-token-spring-boot4-starter + sa-token-spring-boot4-starter + springboot4 integrate sa-token + + + + + + cn.dev33 + sa-token-spring-boot-webmvc-v3v4-common + + + + + cn.dev33 + sa-token-jackson3 + + + + + + + + + + cn.dev33 + sa-token-spring-boot4-dependencies + ${revision} + pom + import + + + + + + diff --git a/sa-token-starter/sa-token-spring-boot4-starter/src/main/java/cn/dev33/satoken/Placeholder.java b/sa-token-starter/sa-token-spring-boot4-starter/src/main/java/cn/dev33/satoken/Placeholder.java new file mode 100644 index 000000000..60dace90d --- /dev/null +++ b/sa-token-starter/sa-token-spring-boot4-starter/src/main/java/cn/dev33/satoken/Placeholder.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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; + + +/** + * 占位符 + * + * @author click33 + * @since 1.45.0 + */ +public class Placeholder { + +} diff --git a/sa-token-test/pom.xml b/sa-token-test/pom.xml new file mode 100644 index 000000000..c8471e3f7 --- /dev/null +++ b/sa-token-test/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-parent + ${revision} + ../pom.xml + + pom + + + sa-token-test + sa-token-test + sa-token-test + + + + + + sa-token-easy-test + sa-token-springboot-test + + sa-token-jwt-test + sa-token-temp-jwt-test + sa-token-json-test + sa-token-jackson3-test + sa-token-serializer-test + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + + cn.dev33 + sa-token-spring-boot2-dependencies + ${revision} + pom + import + + + + + org.springframework.boot + spring-boot-starter-test + 2.7.18 + + + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.6 + + + prepare-agent + + prepare-agent + + + + report-aggregate + test + + report-aggregate + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + ${argLine} -Xms256m -Xmx2048m -Dfile.encoding=utf-8 + 1 + random + + + + + + diff --git a/sa-token-test/sa-token-easy-test/pom.xml b/sa-token-test/sa-token-easy-test/pom.xml new file mode 100644 index 000000000..cfd9295c5 --- /dev/null +++ b/sa-token-test/sa-token-easy-test/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-test + ${revision} + ../pom.xml + + jar + + sa-token-easy-test + sa-token-easy-test + sa-token-easy-test + + + + cn.dev33 + sa-token-jackson3 + ${revision} + + + + tools.jackson.core + jackson-databind + 3.0.0 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + diff --git a/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/SaJsonTemplateTest.java b/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/SaJsonTemplateTest.java new file mode 100644 index 000000000..e426baf03 --- /dev/null +++ b/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/SaJsonTemplateTest.java @@ -0,0 +1,99 @@ +package com.pj.test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.exception.NotImplException; +import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; +import cn.dev33.satoken.json.SaJsonTemplateForJackson3; +import com.pj.test.model.SysUser; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * Sa-Token json 序列化模块测试 + * + * @author click33 + * + */ +public class SaJsonTemplateTest { + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ SaJsonTemplateTest star ..."); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ SaJsonTemplateTest end ... \n"); + } + + // 测试:DefaultImpl + @Test + public void testDefaultImpl() { + SaManager.setSaJsonTemplate(new SaJsonTemplateDefaultImpl()); + Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateDefaultImpl.class); + + // test Object -> Json + SysUser user = new SysUser(10001, "张三", 18); + Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().objectToJson(user) ); + Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject("xxx", SysUser.class) ); + Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject("xxx") ); + Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToMap("xxx") ); + } + + // 测试:Jackson3 + @Test + public void testJackson3() { + SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3()); + Assertions.assertEquals(SaJsonTemplateForJackson3.class, SaManager.getSaJsonTemplate().getClass()); + + // test Object -> Json + SysUser user = new SysUser(10001, "张三", 18); + String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); + // 与 json2 不同点:Jackson 3 默认按字母序排列属性 + Assertions.assertEquals("{\"@class\":\"com.pj.test.model.SysUser\",\"age\":18,\"id\":10001,\"name\":\"张三\",\"role\":null}", objectJson); + + // test Json -> Object + SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson); + Assertions.assertEquals(user3.toString(), user.toString()); + + // more + testNull(); + testMap(); + } + + // 测试 Map 的转换 + private void testMap() { + + // test Map -> Json + Map map = new HashMap<>(); + map.put("id", 10001); + map.put("name", "张三"); + map.put("age", 18); + String mapJson = SaManager.getSaJsonTemplate().objectToJson(map); + Assertions.assertEquals("{\"name\":\"张三\",\"id\":10001,\"age\":18}", mapJson); + + // test Json -> Map + Map map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson); + Assertions.assertEquals(map2.toString(), map.toString()); + + } + + // 测试 Null 值 + private void testNull() { + Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null)); + } + +} diff --git a/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysRole.java b/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysRole.java new file mode 100644 index 000000000..c66b66f5f --- /dev/null +++ b/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysRole.java @@ -0,0 +1,64 @@ +package com.pj.test.model; + +/** + * Role 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysRole { +// +// public SysRole() { +// } +// +// public SysRole(long id, String name) { +// super(); +// this.id = id; +// this.name = name; +// } +// +// +// /** +// * 角色id +// */ +// private long id; +// +// /** +// * 角色名称 +// */ +// private String name; +// +// /** +// * @return id +// */ +// public long getId() { +// return id; +// } +// +// /** +// * @param id 要设置的 id +// */ +// public void setId(long id) { +// this.id = id; +// } +// +// /** +// * @return name +// */ +// public String getName() { +// return name; +// } +// +// /** +// * @param name 要设置的 name +// */ +// public void setName(String name) { +// this.name = name; +// } +// +// @Override +// public String toString() { +// return "SysRole [id=" + id + ", name=" + name + "]"; +// } +// +} diff --git a/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysUser.java b/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysUser.java new file mode 100644 index 000000000..5406d82f3 --- /dev/null +++ b/sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysUser.java @@ -0,0 +1,103 @@ +package com.pj.test.model; + +/** + * User 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysUser { + + public SysUser() { + } + + public SysUser(long id, String name, int age) { + super(); + this.id = id; + this.name = name; + this.age = age; + } + + + /** + * 用户id + */ + private long id; + + /** + * 用户名称 + */ + private String name; + + /** + * 用户年龄 + */ + private int age; + + /** + * 用户角色 + */ + private SysRole role; + + /** + * @return id + */ + public long getId() { + return id; + } + + /** + * @param id 要设置的 id + */ + public void setId(long id) { + this.id = id; + } + + /** + * @return name + */ + public String getName() { + return name; + } + + /** + * @param name 要设置的 name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return age + */ + public int getAge() { + return age; + } + + /** + * @param age 要设置的 age + */ + public void setAge(int age) { + this.age = age; + } + + public SysRole getRole() { + return role; + } + + public SysUser setRole(SysRole role) { + this.role = role; + return this; + } + + @Override + public String toString() { + return "SysUser{" + + "id=" + id + + ", name='" + name + '\'' + + ", age=" + age + + ", role=" + role + + '}'; + } + +} diff --git a/sa-token-test/sa-token-jackson3-test/pom.xml b/sa-token-test/sa-token-jackson3-test/pom.xml new file mode 100644 index 000000000..cf898f48f --- /dev/null +++ b/sa-token-test/sa-token-jackson3-test/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-test + ${revision} + ../pom.xml + + jar + + sa-token-jackson3-test + sa-token-jackson3-test + sa-token-jackson3-test + + + + cn.dev33 + sa-token-jackson3 + ${revision} + + + + tools.jackson.core + jackson-databind + 3.0.0 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + diff --git a/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/SaJsonTemplateForJackson3Test.java b/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/SaJsonTemplateForJackson3Test.java new file mode 100644 index 000000000..324d3c4dc --- /dev/null +++ b/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/SaJsonTemplateForJackson3Test.java @@ -0,0 +1,103 @@ +package com.pj.test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.json.SaJsonTemplateForJackson3; +import com.pj.test.model.SysUser; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * Sa-Token-jackson3 序列化模块测试 + * + *
    + * 为什么单独写一个模块来测试 Jackson 3 ?
    + *
    + * 在同一个项目里同时引入 jackson 2 和 jackson 3 后,
    + * 执行:
    + *      SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson);
    + * 会报错:
    + *      java.lang.NoSuchFieldError: POJO
    + * 	        at tools.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:399)
    + * 	        at tools.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:361)
    + * 	        at tools.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:265)
    + * 	        at tools.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
    + * 	        at tools.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:158)
    + * 	        at tools.jackson.databind.DeserializationContext.findNonContextualValueDeserializer(DeserializationContext.java:733)
    + * 	        at tools.jackson.databind.deser.jdk.UntypedObjectDeserializer._findCustomDeser(UntypedObjectDeserializer.java:179)
    + * 	        at tools.jackson.databind.deser.jdk.UntypedObjectDeserializer.resolve(UntypedObjectDeserializer.java:152)
    + *
    + * 暂未找到解决方案,所以只能单独写一个测试类来测试 Jackson 3 的功能了。
    + *
    + * 
    + * @author click33 + * + */ +public class SaJsonTemplateForJackson3Test { + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ SaJsonTemplateForJackson3 Test star ..."); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ SaJsonTemplateForJackson3 Test end ... \n"); + } + + // 测试:Jackson3 + @Test + public void testJackson3() { + SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3()); + Assertions.assertEquals(SaJsonTemplateForJackson3.class, SaManager.getSaJsonTemplate().getClass()); + + // test Object -> Json + SysUser user = new SysUser(10001, "张三", 18); + String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); + // 与 json2 不同点:Jackson 3 默认按字母序排列属性 + Assertions.assertEquals("{\"@class\":\"com.pj.test.model.SysUser\",\"age\":18,\"id\":10001,\"name\":\"张三\",\"role\":null}", objectJson); + + // test Json -> Object + SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson); + Assertions.assertEquals(user3.toString(), user.toString()); + + // more + testNull(); + testMap(); + } + + // 测试 Map 的转换 + private void testMap() { + + // test Map -> Json + Map map = new HashMap<>(); + map.put("id", 10001); + map.put("name", "张三"); + map.put("age", 18); + String mapJson = SaManager.getSaJsonTemplate().objectToJson(map); + Assertions.assertEquals("{\"name\":\"张三\",\"id\":10001,\"age\":18}", mapJson); + + // test Json -> Map + Map map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson); + Assertions.assertEquals(map2.toString(), map.toString()); + + } + + // 测试 Null 值 + private void testNull() { + Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null)); + } + +} diff --git a/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysRole.java b/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysRole.java new file mode 100644 index 000000000..c66b66f5f --- /dev/null +++ b/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysRole.java @@ -0,0 +1,64 @@ +package com.pj.test.model; + +/** + * Role 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysRole { +// +// public SysRole() { +// } +// +// public SysRole(long id, String name) { +// super(); +// this.id = id; +// this.name = name; +// } +// +// +// /** +// * 角色id +// */ +// private long id; +// +// /** +// * 角色名称 +// */ +// private String name; +// +// /** +// * @return id +// */ +// public long getId() { +// return id; +// } +// +// /** +// * @param id 要设置的 id +// */ +// public void setId(long id) { +// this.id = id; +// } +// +// /** +// * @return name +// */ +// public String getName() { +// return name; +// } +// +// /** +// * @param name 要设置的 name +// */ +// public void setName(String name) { +// this.name = name; +// } +// +// @Override +// public String toString() { +// return "SysRole [id=" + id + ", name=" + name + "]"; +// } +// +} diff --git a/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysUser.java b/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysUser.java new file mode 100644 index 000000000..5406d82f3 --- /dev/null +++ b/sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysUser.java @@ -0,0 +1,103 @@ +package com.pj.test.model; + +/** + * User 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysUser { + + public SysUser() { + } + + public SysUser(long id, String name, int age) { + super(); + this.id = id; + this.name = name; + this.age = age; + } + + + /** + * 用户id + */ + private long id; + + /** + * 用户名称 + */ + private String name; + + /** + * 用户年龄 + */ + private int age; + + /** + * 用户角色 + */ + private SysRole role; + + /** + * @return id + */ + public long getId() { + return id; + } + + /** + * @param id 要设置的 id + */ + public void setId(long id) { + this.id = id; + } + + /** + * @return name + */ + public String getName() { + return name; + } + + /** + * @param name 要设置的 name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return age + */ + public int getAge() { + return age; + } + + /** + * @param age 要设置的 age + */ + public void setAge(int age) { + this.age = age; + } + + public SysRole getRole() { + return role; + } + + public SysUser setRole(SysRole role) { + this.role = role; + return this; + } + + @Override + public String toString() { + return "SysUser{" + + "id=" + id + + ", name='" + name + '\'' + + ", age=" + age + + ", role=" + role + + '}'; + } + +} diff --git a/sa-token-test/sa-token-json-test/pom.xml b/sa-token-test/sa-token-json-test/pom.xml new file mode 100644 index 000000000..6fc7117ac --- /dev/null +++ b/sa-token-test/sa-token-json-test/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-test + ${revision} + ../pom.xml + + jar + + sa-token-json-test + sa-token-json-test + sa-token-json-test + + + + cn.dev33 + sa-token-jackson + + + cn.dev33 + sa-token-fastjson + + + cn.dev33 + sa-token-fastjson2 + + + cn.dev33 + sa-token-snack3 + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + diff --git a/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/SaJsonTemplateTest.java b/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/SaJsonTemplateTest.java new file mode 100644 index 000000000..b7f6afe74 --- /dev/null +++ b/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/SaJsonTemplateTest.java @@ -0,0 +1,157 @@ +package com.pj.test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.exception.NotImplException; +import cn.dev33.satoken.json.*; +import com.pj.test.model.SysUser; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * Sa-Token json 序列化模块测试 + * + * @author click33 + * + */ +public class SaJsonTemplateTest { + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ SaJsonTemplateTest star ..."); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ SaJsonTemplateTest end ... \n"); + } + + // 测试:DefaultImpl + @Test + public void testDefaultImpl() { + SaManager.setSaJsonTemplate(new SaJsonTemplateDefaultImpl()); + Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateDefaultImpl.class); + + // test Object -> Json + SysUser user = new SysUser(10001, "张三", 18); + Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().objectToJson(user) ); + Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject("xxx", SysUser.class) ); + Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject("xxx") ); + Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToMap("xxx") ); + } + + // 测试:Jackson + @Test + public void testJackson() { + SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson()); + Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForJackson.class); + + // test Object -> Json + SysUser user = new SysUser(10001, "张三", 18); + String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); + Assertions.assertEquals("{\"@class\":\"com.pj.test.model.SysUser\",\"id\":10001,\"name\":\"张三\",\"age\":18,\"role\":null}", objectJson); + + // test Json -> Object + SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson); + Assertions.assertEquals(user3.toString(), user.toString()); + + // more + testNull(); + testMap(); + } + + // 测试:Fastjson + @Test + public void testFastjson() { + SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson()); + Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForFastjson.class); + + // test Object -> Json + SysUser user = new SysUser(10001, "张三", 18); + String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); + Assertions.assertEquals("{\"age\":18,\"id\":10001,\"name\":\"张三\"}", objectJson); + + // test Json -> Object + SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + testMap(); + } + + // 测试:Fastjson2 + @Test + public void testFastjson2() { + SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson2()); + Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForFastjson2.class); + + // test Object -> Json + SysUser user = new SysUser(10001, "张三", 18); + String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); + Assertions.assertEquals("{\"age\":18,\"id\":10001,\"name\":\"张三\"}", objectJson); + + // test Json -> Object + SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + testMap(); + } + + // 测试:Snack3 + @Test + public void testSnack3() { + SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack3()); + Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForSnack3.class); + + // test Object -> Json + SysUser user = new SysUser(10001, "张三", 18); + String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); + Assertions.assertEquals("{\"id\":10001,\"name\":\"张三\",\"age\":18}", objectJson); + + // test Json -> Object + SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + testMap(); + } + + // 测试 Map 的转换 + private void testMap() { + + // test Map -> Json + Map map = new HashMap<>(); + map.put("id", 10001); + map.put("name", "张三"); + map.put("age", 18); + String mapJson = SaManager.getSaJsonTemplate().objectToJson(map); + Assertions.assertEquals("{\"name\":\"张三\",\"id\":10001,\"age\":18}", mapJson); + + // test Json -> Map + Map map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson); + Assertions.assertEquals(map2.toString(), map.toString()); + + } + + // 测试 Null 值 + private void testNull() { + Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null)); + Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null)); + } + +} diff --git a/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysRole.java b/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysRole.java new file mode 100644 index 000000000..c66b66f5f --- /dev/null +++ b/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysRole.java @@ -0,0 +1,64 @@ +package com.pj.test.model; + +/** + * Role 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysRole { +// +// public SysRole() { +// } +// +// public SysRole(long id, String name) { +// super(); +// this.id = id; +// this.name = name; +// } +// +// +// /** +// * 角色id +// */ +// private long id; +// +// /** +// * 角色名称 +// */ +// private String name; +// +// /** +// * @return id +// */ +// public long getId() { +// return id; +// } +// +// /** +// * @param id 要设置的 id +// */ +// public void setId(long id) { +// this.id = id; +// } +// +// /** +// * @return name +// */ +// public String getName() { +// return name; +// } +// +// /** +// * @param name 要设置的 name +// */ +// public void setName(String name) { +// this.name = name; +// } +// +// @Override +// public String toString() { +// return "SysRole [id=" + id + ", name=" + name + "]"; +// } +// +} diff --git a/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysUser.java b/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysUser.java new file mode 100644 index 000000000..5406d82f3 --- /dev/null +++ b/sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysUser.java @@ -0,0 +1,103 @@ +package com.pj.test.model; + +/** + * User 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysUser { + + public SysUser() { + } + + public SysUser(long id, String name, int age) { + super(); + this.id = id; + this.name = name; + this.age = age; + } + + + /** + * 用户id + */ + private long id; + + /** + * 用户名称 + */ + private String name; + + /** + * 用户年龄 + */ + private int age; + + /** + * 用户角色 + */ + private SysRole role; + + /** + * @return id + */ + public long getId() { + return id; + } + + /** + * @param id 要设置的 id + */ + public void setId(long id) { + this.id = id; + } + + /** + * @return name + */ + public String getName() { + return name; + } + + /** + * @param name 要设置的 name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return age + */ + public int getAge() { + return age; + } + + /** + * @param age 要设置的 age + */ + public void setAge(int age) { + this.age = age; + } + + public SysRole getRole() { + return role; + } + + public SysUser setRole(SysRole role) { + this.role = role; + return this; + } + + @Override + public String toString() { + return "SysUser{" + + "id=" + id + + ", name='" + name + '\'' + + ", age=" + age + + ", role=" + role + + '}'; + } + +} diff --git a/sa-token-test/sa-token-jwt-test/pom.xml b/sa-token-test/sa-token-jwt-test/pom.xml new file mode 100644 index 000000000..49cd9ee54 --- /dev/null +++ b/sa-token-test/sa-token-jwt-test/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-test + ${revision} + ../pom.xml + + jar + + sa-token-jwt-test + sa-token-jwt-test + sa-token-jwt-test + + + + cn.dev33 + sa-token-spring-boot-starter + + + cn.dev33 + sa-token-jwt + + + + diff --git a/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForMixinTest.java b/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForMixinTest.java new file mode 100644 index 000000000..f02a50966 --- /dev/null +++ b/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForMixinTest.java @@ -0,0 +1,284 @@ +package com.pj.test; + +import java.util.List; + +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.exception.ApiDisabledException; +import cn.dev33.satoken.exception.DisableServiceException; +import cn.dev33.satoken.jwt.SaJwtUtil; +import cn.dev33.satoken.jwt.StpLogicJwtForMixin; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.SaLoginConfig; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; + +/** + * Sa-Token 整合 jwt:mixin 模式 测试 + * + * @author click33 + * + */ +@SpringBootTest(classes = StartUpApplication.class) +public class JwtForMixinTest { + + // 持久化Bean + @Autowired(required = false) + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ JwtForMixinTest star ..."); + StpUtil.setStpLogic(new StpLogicJwtForMixin()); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ JwtForMixinTest end ... \n"); + } + + @BeforeEach + public void beforeEach() { + SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); + } + + @AfterEach + public void afterEach() { + SaTokenContextServletUtil.clearContext(); + } + + // 测试:登录 + @Test + public void doLogin() { + // 登录 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + + // API 验证 + Assertions.assertTrue(StpUtil.isLogin()); + Assertions.assertNotNull(token); // token不为null + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 + Assertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 + + // token 验证 + JWT jwt = JWT.of(token); + JSONObject payloads = jwt.getPayloads(); + Assertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_ID), "10001"); // 账号 + Assertions.assertEquals(payloads.getStr(SaJwtUtil.DEVICE_TYPE), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 + Assertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_TYPE), StpUtil.TYPE); // 账号类型 + + // db数据 验证 + // token不存在 + Assertions.assertNull(dao.get("satoken:login:token:" + token)); + // Session 存在 + SaSession session = dao.getSession("satoken:login:session:" + 10001); + Assertions.assertNotNull(session); + Assertions.assertEquals(session.getId(), "satoken:login:session:" + 10001); + Assertions.assertTrue(session.getTerminalList().size() >= 1); + } + + // 测试:注销 + @Test + public void logout() { + // 登录 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + Assertions.assertEquals(JWT.of(token).getPayloads().getStr("loginId"), "10001"); + + // 注销 + StpUtil.logout(); + // token 应该被清除 + Assertions.assertNull(StpUtil.getTokenValue()); + Assertions.assertFalse(StpUtil.isLogin()); + } + + // 测试:Session会话 + @Test + public void testSession() { + StpUtil.login(10001); + + // API 应该可以获取 Session + Assertions.assertNotNull(StpUtil.getSession(false)); + + // db中应该存在 Session + SaSession session = dao.getSession("satoken:login:session:" + 10001); + Assertions.assertNotNull(session); + + // 存取值 + session.set("name", "zhang"); + session.set("age", "18"); + Assertions.assertEquals(session.get("name"), "zhang"); + Assertions.assertEquals(session.getInt("age"), 18); + Assertions.assertEquals((int)session.getModel("age", int.class), 18); + Assertions.assertEquals((int)session.get("age", 20), 18); + Assertions.assertEquals((int)session.get("name2", 20), 20); + Assertions.assertEquals((int)session.get("name2", () -> 30), 30); + session.clear(); + Assertions.assertEquals(session.get("name"), null); + } + + // 测试:权限认证 + @Test + public void testCheckPermission() { + StpUtil.login(10001); + + // 权限认证 + Assertions.assertTrue(StpUtil.hasPermission("user-add")); + Assertions.assertTrue(StpUtil.hasPermission("user-list")); + Assertions.assertTrue(StpUtil.hasPermission("user")); + Assertions.assertTrue(StpUtil.hasPermission("art-add")); + Assertions.assertFalse(StpUtil.hasPermission("get-user")); + // and + Assertions.assertTrue(StpUtil.hasPermissionAnd("art-add", "art-get")); + Assertions.assertFalse(StpUtil.hasPermissionAnd("art-add", "comment-add")); + // or + Assertions.assertTrue(StpUtil.hasPermissionOr("art-add", "comment-add")); + Assertions.assertFalse(StpUtil.hasPermissionOr("comment-add", "comment-delete")); + } + + // 测试:角色认证 + @Test + public void testCheckRole() { + StpUtil.login(10001); + + // 角色认证 + Assertions.assertTrue(StpUtil.hasRole("admin")); + Assertions.assertFalse(StpUtil.hasRole("teacher")); + // and + Assertions.assertTrue(StpUtil.hasRoleAnd("admin", "super-admin")); + Assertions.assertFalse(StpUtil.hasRoleAnd("admin", "ceo")); + // or + Assertions.assertTrue(StpUtil.hasRoleOr("admin", "ceo")); + Assertions.assertFalse(StpUtil.hasRoleOr("ceo", "cto")); + } + + // 测试:根据token强制注销 + @Test + public void testLogoutByToken() { + Assertions.assertThrows(ApiDisabledException.class, () -> { + // 先登录上 + StpUtil.login(10001); + Assertions.assertTrue(StpUtil.isLogin()); + String token = StpUtil.getTokenValue(); + + // 根据token注销 + StpUtil.logoutByTokenValue(token); + }); + } + + // 测试:根据账号id强制注销 + @Test + public void testLogoutByLoginId() { + Assertions.assertThrows(ApiDisabledException.class, () -> { + // 先登录上 + StpUtil.login(10001); + Assertions.assertTrue(StpUtil.isLogin()); + + // 根据账号id注销 + StpUtil.logout(10001); + }); + } + + // 测试Token-Session + @Test + public void testTokenSession() { + + // 先登录上 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + + // 刚开始不存在 + Assertions.assertNull(StpUtil.stpLogic.getTokenSession(false)); + SaSession session = dao.getSession("satoken:login:token-session:" + token); + Assertions.assertNull(session); + + // 调用一次就存在了 + StpUtil.getTokenSession(); + Assertions.assertNotNull(StpUtil.stpLogic.getTokenSession(false)); + SaSession session2 = dao.getSession("satoken:login:token-session:" + token); + Assertions.assertNotNull(session2); + } + + // 测试:账号封禁 + @Test + public void testDisable() { + Assertions.assertThrows(DisableServiceException.class, () -> { + // 封号 + StpUtil.disable(10007, 200); + Assertions.assertTrue(StpUtil.isDisable(10007)); + Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10007), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); + + // 解封 + StpUtil.untieDisable(10007); + Assertions.assertFalse(StpUtil.isDisable(10007)); + Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10007), null); + + // 封号后校验 (会抛出 DisableLoginException 异常) + StpUtil.disable(10007, 200); + StpUtil.checkDisable(10007); + StpUtil.login(10007); + }); + } + + // 测试:身份切换 + @Test + public void testSwitch() { + // 登录 + StpUtil.login(10001); + Assertions.assertFalse(StpUtil.isSwitch()); + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); + + // 开始身份切换 + StpUtil.switchTo(10044); + Assertions.assertTrue(StpUtil.isSwitch()); + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10044); + + // 结束切换 + StpUtil.endSwitch(); + Assertions.assertFalse(StpUtil.isSwitch()); + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); + } + + // 测试:会话管理 + @Test + public void testSearchTokenValue() { + Assertions.assertThrows(ApiDisabledException.class, () -> { + // 登录 + StpUtil.login(10001); + StpUtil.login(10002); + StpUtil.login(10003); + StpUtil.login(10004); + StpUtil.login(10005); + + // 查询 + List list = StpUtil.searchTokenValue("", 0, 10, true); + Assertions.assertTrue(list.size() >= 5); + }); + } + + // 测试:getExtra + @Test + public void getExtra() { + // 登录 + StpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan")); + String tokenValue = StpUtil.getTokenValue(); + + // 可以取到 + Assertions.assertEquals(StpUtil.getExtra("name"), "zhangsan"); + Assertions.assertEquals(StpUtil.getExtra(tokenValue, "name"), "zhangsan"); + + // 取不到 + Assertions.assertEquals(StpUtil.getExtra("name2"), null); + } + +} diff --git a/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForSimpleTest.java b/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForSimpleTest.java new file mode 100644 index 000000000..97a465b5c --- /dev/null +++ b/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForSimpleTest.java @@ -0,0 +1,97 @@ +package com.pj.test; + +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import org.junit.jupiter.api.*; +import org.springframework.boot.test.context.SpringBootTest; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.jwt.StpLogicJwtForSimple; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.SaLoginConfig; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; + +/** + * Sa-Token 整合 jwt:Simple 模式 测试 + * + * @author click33 + * + */ +//@RunWith(SpringRunner.class) +@SpringBootTest(classes = StartUpApplication.class) +public class JwtForSimpleTest { + + // 持久化Bean + static SaTokenDao dao; + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ JwtForSimpleTest star ..."); + dao = SaManager.getSaTokenDao(); + StpUtil.setStpLogic(new StpLogicJwtForSimple()); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ JwtForSimpleTest end ... \n"); + } + + @BeforeEach + public void beforeEach() { + SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); + } + + @AfterEach + public void afterEach() { + SaTokenContextServletUtil.clearContext(); + } + + // 测试:登录 + @Test + public void doLogin() { + // 登录 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + + // API 验证 + Assertions.assertTrue(StpUtil.isLogin()); + Assertions.assertNotNull(token); // token不为null + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 + Assertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 + + // token 验证 + JWT jwt = JWT.of(token); + JSONObject payloads = jwt.getPayloads(); + Assertions.assertEquals(payloads.getStr("loginId"), "10001"); + + // db数据 验证 + // token存在 + Assertions.assertEquals(dao.get("satoken:login:token:" + token), "10001"); + // Session 存在 + SaSession session = dao.getSession("satoken:login:session:" + 10001); + Assertions.assertNotNull(session); + Assertions.assertEquals(session.getId(), "satoken:login:session:" + 10001); + Assertions.assertTrue(session.getTerminalList().size() >= 1); + } + + // 测试:getExtra + @Test + public void getExtra() { + // 登录 + StpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan")); + String tokenValue = StpUtil.getTokenValue(); + + // 可以取到 + Assertions.assertEquals(StpUtil.getExtra("name"), "zhangsan"); + Assertions.assertEquals(StpUtil.getExtra(tokenValue, "name"), "zhangsan"); + // 取不到 + Assertions.assertEquals(StpUtil.getExtra("name2"), null); + } + +} diff --git a/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForStatelessTest.java b/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForStatelessTest.java new file mode 100644 index 000000000..aa70a4290 --- /dev/null +++ b/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForStatelessTest.java @@ -0,0 +1,192 @@ +package com.pj.test; + +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.exception.ApiDisabledException; +import cn.dev33.satoken.jwt.SaJwtUtil; +import cn.dev33.satoken.jwt.StpLogicJwtForStateless; +import cn.dev33.satoken.stp.SaLoginConfig; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaTokenConsts; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; + +/** + * Sa-Token 整合 jwt:stateless 模式 测试 + * + * @author click33 + * + */ +@SpringBootTest(classes = StartUpApplication.class) +public class JwtForStatelessTest { + + // 持久化Bean + @Autowired(required = false) + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ JwtForStatelessTest star ..."); + StpUtil.setStpLogic(new StpLogicJwtForStateless()); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ JwtForStatelessTest end ... \n"); + } + + @BeforeEach + public void beforeEach() { + SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); + } + + @AfterEach + public void afterEach() { + SaTokenContextServletUtil.clearContext(); + } + + // 测试:登录 + @Test + public void doLogin() { + // 登录 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + + // API 验证 + Assertions.assertTrue(StpUtil.isLogin()); + Assertions.assertNotNull(token); // token不为null + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 + Assertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 + + // token 验证 + JWT jwt = JWT.of(token); + JSONObject payloads = jwt.getPayloads(); + Assertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_ID), "10001"); // 账号 + Assertions.assertEquals(payloads.getStr(SaJwtUtil.DEVICE_TYPE), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 + Assertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_TYPE), StpUtil.TYPE); // 账号类型 + + // 时间 + Assertions.assertTrue(StpUtil.getTokenTimeout() <= SaManager.getConfig().getTimeout()); + Assertions.assertTrue(StpUtil.getTokenTimeout() > SaManager.getConfig().getTimeout() - 10000); + + try { + // 尝试获取Session会抛出异常 + StpUtil.getSession(); + Assertions.assertTrue(false); + } catch (Exception e) { + } + } + + // 测试:注销 + @Test + public void logout() { + // 登录 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + Assertions.assertEquals(JWT.of(token).getPayloads().getStr("loginId"), "10001"); + + // 注销 + StpUtil.logout(); + + // token 应该被清除 + Assertions.assertNull(StpUtil.getTokenValue()); + Assertions.assertFalse(StpUtil.isLogin()); + } + + // 测试:Session会话 + @Test + public void testSession() { + Assertions.assertThrows(ApiDisabledException.class, () -> { + StpUtil.login(10001); + + // 会抛异常 + StpUtil.getSession(); + }); + } + + // 测试:权限认证 + @Test + public void testCheckPermission() { + StpUtil.login(10001); + + // 权限认证 + Assertions.assertTrue(StpUtil.hasPermission("user-add")); + Assertions.assertTrue(StpUtil.hasPermission("user-list")); + Assertions.assertTrue(StpUtil.hasPermission("user")); + Assertions.assertTrue(StpUtil.hasPermission("art-add")); + Assertions.assertFalse(StpUtil.hasPermission("get-user")); + // and + Assertions.assertTrue(StpUtil.hasPermissionAnd("art-add", "art-get")); + Assertions.assertFalse(StpUtil.hasPermissionAnd("art-add", "comment-add")); + // or + Assertions.assertTrue(StpUtil.hasPermissionOr("art-add", "comment-add")); + Assertions.assertFalse(StpUtil.hasPermissionOr("comment-add", "comment-delete")); + } + + // 测试:角色认证 + @Test + public void testCheckRole() { + StpUtil.login(10001); + + // 角色认证 + Assertions.assertTrue(StpUtil.hasRole("admin")); + Assertions.assertFalse(StpUtil.hasRole("teacher")); + // and + Assertions.assertTrue(StpUtil.hasRoleAnd("admin", "super-admin")); + Assertions.assertFalse(StpUtil.hasRoleAnd("admin", "ceo")); + // or + Assertions.assertTrue(StpUtil.hasRoleOr("admin", "ceo")); + Assertions.assertFalse(StpUtil.hasRoleOr("ceo", "cto")); + } + + // 测试:根据token强制注销 + @Test + public void testLogoutByToken() { + Assertions.assertThrows(ApiDisabledException.class, () -> { + // 先登录上 + StpUtil.login(10001); + Assertions.assertTrue(StpUtil.isLogin()); + String token = StpUtil.getTokenValue(); + + // 根据token注销 + StpUtil.logoutByTokenValue(token); + }); + } + + // 测试:根据账号id强制注销 + @Test + public void testLogoutByLoginId() { + Assertions.assertThrows(ApiDisabledException.class, () -> { + // 先登录上 + StpUtil.login(10001); + Assertions.assertTrue(StpUtil.isLogin()); + + // 根据账号id注销 + StpUtil.logout(10001); + }); + } + + // 测试:getExtra + @Test + public void getExtra() { + // 登录 + StpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan")); + String tokenValue = StpUtil.getTokenValue(); + + // 可以取到 + Assertions.assertEquals(StpUtil.getExtra("name"), "zhangsan"); + Assertions.assertEquals(StpUtil.getExtra(tokenValue, "name"), "zhangsan"); + + // 取不到 + Assertions.assertEquals(StpUtil.getExtra("name2"), null); + } + +} diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/test/java/com/pj/test/StartUpApplication.java b/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/StartUpApplication.java similarity index 100% rename from sa-token-starter/sa-token-spring-boot-starter/src/test/java/com/pj/test/StartUpApplication.java rename to sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/StartUpApplication.java diff --git a/sa-token-starter/sa-token-spring-boot-starter/src/test/java/com/pj/test/satoken/StpInterfaceImpl.java b/sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/satoken/StpInterfaceImpl.java similarity index 100% rename from sa-token-starter/sa-token-spring-boot-starter/src/test/java/com/pj/test/satoken/StpInterfaceImpl.java rename to sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/satoken/StpInterfaceImpl.java diff --git a/sa-token-test/sa-token-jwt-test/src/test/resources/application.yml b/sa-token-test/sa-token-jwt-test/src/test/resources/application.yml new file mode 100644 index 000000000..1b25a92c2 --- /dev/null +++ b/sa-token-test/sa-token-jwt-test/src/test/resources/application.yml @@ -0,0 +1,46 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # jwt秘钥 + jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10000ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + \ No newline at end of file diff --git a/sa-token-test/sa-token-serializer-test/pom.xml b/sa-token-test/sa-token-serializer-test/pom.xml new file mode 100644 index 000000000..bd64dda91 --- /dev/null +++ b/sa-token-test/sa-token-serializer-test/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-test + ${revision} + ../pom.xml + + jar + + sa-token-serializer-test + sa-token-serializer-test + sa-token-serializer-test + + + + cn.dev33 + sa-token-serializer-features + + + + diff --git a/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/SaSerializerTemplateTest.java b/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/SaSerializerTemplateTest.java new file mode 100644 index 000000000..2ff23687f --- /dev/null +++ b/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/SaSerializerTemplateTest.java @@ -0,0 +1,177 @@ +package com.pj.test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.serializer.SaSerializerForBase64UseEmoji; +import cn.dev33.satoken.serializer.SaSerializerForBase64UsePeriodicTable; +import cn.dev33.satoken.serializer.SaSerializerForBase64UseSpecialSymbols; +import cn.dev33.satoken.serializer.SaSerializerForBase64UseTianGan; +import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseBase64; +import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseHex; +import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseISO_8859_1; +import com.pj.test.model.SysUser; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Sa-Token Serializer 序列化模块测试 + * + * @author click33 + * + */ +public class SaSerializerTemplateTest { + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ SaSerializerTemplateTest star ..."); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ SaSerializerTemplateTest end ... \n"); + } + + // 测试:SaSerializerTemplateForJdkUseBase64 + @Test + public void testSaSerializerTemplateForJdkUseBase64() { + SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseBase64()); + Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseBase64.class); + + // test Object -> String + SysUser user = new SysUser(10001, "张三", 18); + String objectString = SaManager.getSaSerializerTemplate().objectToString(user); + Assertions.assertEquals("rO0ABXNyABljb20ucGoudGVzdC5tb2RlbC5TeXNVc2Vy0MeZoPBtVUwCAARJAANhZ2VKAAJpZEwABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztMAARyb2xldAAbTGNvbS9wai90ZXN0L21vZGVsL1N5c1JvbGU7eHAAAAASAAAAAAAAJxF0AAblvKDkuIlw", objectString); + + // test String -> Object + SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + } + + // 测试:SaSerializerTemplateForJdkUseHex + @Test + public void testSaSerializerTemplateForJdkUseHex() { + SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseHex()); + Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseHex.class); + + // test Object -> String + SysUser user = new SysUser(10001, "张三", 18); + String objectString = SaManager.getSaSerializerTemplate().objectToString(user); + Assertions.assertEquals("ACED000573720019636F6D2E706A2E746573742E6D6F64656C2E53797355736572D0C799A0F06D554C0200044900036167654A000269644C00046E616D657400124C6A6176612F6C616E672F537472696E673B4C0004726F6C6574001B4C636F6D2F706A2F746573742F6D6F64656C2F537973526F6C653B7870000000120000000000002711740006E5BCA0E4B88970", objectString); + + // test String -> Object + SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + } + + // 测试:SaSerializerTemplateForJdkUseISO_8859_1 + @Test + public void testSaSerializerTemplateForJdkUseISO_8859_1() { + SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseISO_8859_1()); + Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseISO_8859_1.class); + + // test Object -> String + SysUser user = new SysUser(10001, "张三", 18); + String objectString = SaManager.getSaSerializerTemplate().objectToString(user); + // Assertions.assertEquals("xxxx", objectString); // 太过奇形怪状,无法直接断言 + + // test String -> Object + SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + } + + // 测试:SaSerializerForBase64UseTianGan + @Test + public void testSaSerializerForBase64UseTianGan() { + SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan()); + Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseTianGan.class); + + // test Object -> String + SysUser user = new SysUser(10001, "张三", 18); + String objectString = SaManager.getSaSerializerTemplate().objectToString(user); + Assertions.assertEquals("雷辰中甲乙坤卯西甲乙日天离谷中雾艮庚石雾兑庚亥北兑丙宙霜离谷未日离丙宙酉金坤卯亥艮谷亥西中寅金巽石巳乙霜亥戌东丙甲甲未癸甲甲卯火巽谷亥子甲甲癸田巽戊东甲乙庚宙火离乾亥中甲乙癸寅坎月己谷震申安电震乾宙山丑信卯中艮月日雾巽北霜寅甲甲未西离谷南日兑甲甲离酉庚卯露离申安东坎土安中巽坤卯中丑谷信露巽庚亥电丑信卯宙艮信癸露离庚戌泰金辛甲甲甲甲甲申甲甲甲甲甲甲甲甲癸南己中甲甲离日露子丁地雾壬日东", objectString); + + // test String -> Object + SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + } + + // 测试:SaSerializerForBase64UsePeriodicTable + @Test + public void testSaSerializerForBase64UsePeriodicTable() { + SaManager.setSaSerializerTemplate(new SaSerializerForBase64UsePeriodicTable()); + Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UsePeriodicTable.class); + + // test Object -> String + SysUser user = new SysUser(10001, "张三", 18); + String objectString = SaManager.getSaSerializerTemplate().objectToString(user); + Assertions.assertEquals("钌磷碘氢氦铬硅锑氢氦锶氪镍铯碘银铜氮铌银锌氮钛碲锌锂铈钯镍铯氩锶镍锂铈钙镓铬硅钛铜铯钛锑碘铝镓铁铌硫氦钯钛钪铟锂氢氢氩氖氢氢硅硒铁铯钛钠氢氢氖钼铁硼铟氢氦氮铈硒镍钒钛碘氢氦氖铝钴钇碳铯锰钾钐铑锰钒铈锆镁氙硅碘铜钇锶银铁碲钯铝氢氢氩锑镍铯锡锶锌氢氢镍钙氮硅镉镍钾钐铟钴溴钐碘铁铬硅碘镁铯氙镉铁氮钛铑镁氙硅铈铜氙氖镉镍氮钪钕镓氧氢氢氢氢氢钾氢氢氢氢氢氢氢氢氖锡碳碘氢氢镍锶镉钠铍铷银氟锶铟", objectString); + + // test String -> Object + SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + } + + // 测试:SaSerializerForBase64UseSpecialSymbols + @Test + public void testSaSerializerForBase64UseSpecialSymbols() { + SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseSpecialSymbols()); + Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseSpecialSymbols.class); + + // test Object -> String + SysUser user = new SysUser(10001, "张三", 18); + String objectString = SaManager.getSaSerializerTemplate().objectToString(user); + Assertions.assertEquals("→▃☶▲▼▌▂☳▲▼§♫▬☰☶↘〓▶↑↘◤▶▎☱◤●☀↓▬☰▆§▬●☀█◥▌▂▎〓☰▎☳☶▁◥▊↑▄▼↓▎▏☲●▲▲▆♥▲▲▂♩▊☰▎♦▲▲♥↗▊■☲▲▼▶☀♩▬▍▎☶▲▼♥▁▉〼★☰▋▇‥↙▋▍☀↖♣☵▂☶〓〼§↘▊☱↓▁▲▲▆☳▬☰☷§◤▲▲▬█▶▂☴▬▇‥☲▉♪‥☶▊▌▂☶♣☰☵☴▊▶▎↙♣☵▂☀〓☵♥☴▬▶▏▪◥◀▲▲▲▲▲▇▲▲▲▲▲▲▲▲♥☷★☶▲▲▬§☴♦◆♬↘♠§☲", objectString); + + // test String -> Object + SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + } + + // 测试:SaSerializerForBase64UseEmoji + @Test + public void testSaSerializerForBase64UseEmoji() { + SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseEmoji()); + Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseEmoji.class); + + // test Object -> String + SysUser user = new SysUser(10001, "张三", 18); + String objectString = SaManager.getSaSerializerTemplate().objectToString(user); + Assertions.assertEquals("😫😎😴😀😁😗😍😲😀😁😥😣😛😶😴😮😜😆😨😮😝😆😕😳😝😂😹😭😛😶😑😥😛😂😹😓😞😗😍😕😜😶😕😲😴😌😞😙😨😏😁😭😕😔😰😂😀😀😑😉😀😀😍😡😙😶😕😊😀😀😉😩😙😄😰😀😁😆😹😡😛😖😕😴😀😁😉😌😚😦😅😶😘😒😽😬😘😖😹😧😋😵😍😴😜😦😥😮😙😳😭😌😀😀😑😲😛😶😱😥😝😀😀😛😓😆😍😯😛😒😽😰😚😢😽😴😙😗😍😴😋😶😵😯😙😆😕😬😋😵😍😹😜😵😉😯😛😆😔😻😞😇😀😀😀😀😀😒😀😀😀😀😀😀😀😀😉😱😅😴😀😀😛😥😯😊😃😤😮😈😥😰", objectString); + + // test String -> Object + SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); + Assertions.assertEquals(user2.toString(), user.toString()); + + // more + testNull(); + } + + // 测试 Null 值 + private void testNull() { + Assertions.assertNull(SaManager.getSaSerializerTemplate().objectToString(null)); + Assertions.assertNull(SaManager.getSaSerializerTemplate().stringToObject(null, SysUser.class)); + Assertions.assertNull(SaManager.getSaSerializerTemplate().stringToObject(null)); + } + +} diff --git a/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysRole.java b/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysRole.java new file mode 100644 index 000000000..f86ba4081 --- /dev/null +++ b/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysRole.java @@ -0,0 +1,66 @@ +package com.pj.test.model; + +import java.io.Serializable; + +/** + * Role 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysRole implements Serializable { +// +// public SysRole() { +// } +// +// public SysRole(long id, String name) { +// super(); +// this.id = id; +// this.name = name; +// } +// +// +// /** +// * 角色id +// */ +// private long id; +// +// /** +// * 角色名称 +// */ +// private String name; +// +// /** +// * @return id +// */ +// public long getId() { +// return id; +// } +// +// /** +// * @param id 要设置的 id +// */ +// public void setId(long id) { +// this.id = id; +// } +// +// /** +// * @return name +// */ +// public String getName() { +// return name; +// } +// +// /** +// * @param name 要设置的 name +// */ +// public void setName(String name) { +// this.name = name; +// } +// +// @Override +// public String toString() { +// return "SysRole [id=" + id + ", name=" + name + "]"; +// } +// +} diff --git a/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysUser.java b/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysUser.java new file mode 100644 index 000000000..bf07fb6b6 --- /dev/null +++ b/sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysUser.java @@ -0,0 +1,105 @@ +package com.pj.test.model; + +import java.io.Serializable; + +/** + * User 实体类 + * + * @author click33 + * @since 2022-10-15 + */ +public class SysUser implements Serializable { + + public SysUser() { + } + + public SysUser(long id, String name, int age) { + super(); + this.id = id; + this.name = name; + this.age = age; + } + + + /** + * 用户id + */ + private long id; + + /** + * 用户名称 + */ + private String name; + + /** + * 用户年龄 + */ + private int age; + + /** + * 用户角色 + */ + private SysRole role; + + /** + * @return id + */ + public long getId() { + return id; + } + + /** + * @param id 要设置的 id + */ + public void setId(long id) { + this.id = id; + } + + /** + * @return name + */ + public String getName() { + return name; + } + + /** + * @param name 要设置的 name + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return age + */ + public int getAge() { + return age; + } + + /** + * @param age 要设置的 age + */ + public void setAge(int age) { + this.age = age; + } + + public SysRole getRole() { + return role; + } + + public SysUser setRole(SysRole role) { + this.role = role; + return this; + } + + @Override + public String toString() { + return "SysUser{" + + "id=" + id + + ", name='" + name + '\'' + + ", age=" + age + + ", role=" + role + + '}'; + } + +} diff --git a/sa-token-test/sa-token-springboot-test/pom.xml b/sa-token-test/sa-token-springboot-test/pom.xml new file mode 100644 index 000000000..8dd4aa5dd --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-test + ${revision} + ../pom.xml + + jar + + sa-token-springboot-test + sa-token-springboot-test + sa-token-springboot-test + + + + cn.dev33 + sa-token-spring-boot-starter + + + cn.dev33 + sa-token-sso + test + + + cn.dev33 + sa-token-oauth2 + test + + + cn.dev33 + sa-token-sign + test + + + + cn.dev33 + sa-token-servlet + + + cn.dev33 + sa-token-core + + + + diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/application/SaApplicationTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/application/SaApplicationTest.java new file mode 100644 index 000000000..c18c63383 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/application/SaApplicationTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.application; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.application.SaApplication; +import cn.dev33.satoken.context.SaHolder; + +/** + * SaApplication 存取值测试 + * + * @author click33 + * @since 2022-9-4 + */ +public class SaApplicationTest { + + // 测试 + @Test + public void testSaApplication() { + SaApplication application = SaHolder.getApplication(); + + // 取值 + application.set("age", "18"); + Assertions.assertEquals(application.get("age").toString(), "18"); + Assertions.assertEquals(application.getInt("age"), 18); + Assertions.assertEquals(application.getLong("age"), 18L); + Assertions.assertEquals(application.getFloat("age"), 18f); + Assertions.assertEquals(application.getDouble("age"), 18.0); + Assertions.assertEquals(application.getString("age"), "18"); + Assertions.assertEquals(application.get("age", 20), 18); + Assertions.assertEquals(application.get("age2", 20), 20); + Assertions.assertEquals(application.getString("age2"), null); + // lambda 取值,有值时依然是原值 + Assertions.assertEquals(application.get("age", () -> "23"), "18"); + Assertions.assertEquals(application.getInt("age"), 18); + // lambda 取值,无值时被写入新值 + Assertions.assertEquals(application.get("age2", () -> "23"), "23"); + Assertions.assertEquals(application.getInt("age2"), 23); + + // getModel取值 + Assertions.assertEquals(application.getModel("age", int.class), 18); + Assertions.assertEquals(application.getModel("age", int.class, 30), 18); + Assertions.assertEquals(application.getModel("age3", int.class, 30), 30); + + // 删除值 + application.delete("age"); + Assertions.assertNull(application.get("age")); + + // 是否为空 + Assertions.assertTrue(application.valueIsNull(null)); + Assertions.assertTrue(application.valueIsNull("")); + Assertions.assertFalse(application.valueIsNull("abc")); + + // 为空时才能写入 + application.setByNull("age4", "18"); + Assertions.assertEquals(application.getInt("age4"), 18); + application.setByNull("age4", "20"); + Assertions.assertEquals(application.getInt("age4"), 18); + + // 清空 + application.clear(); + Assertions.assertEquals(application.keys().size(), 0); + + // 获取所有值 + application.set("key1", "value1"); + application.set("key2", "value2"); + application.set("key3", "value3"); + Assertions.assertEquals(application.keys().size(), 3); + + // 空列表 + application.clear(); + Assertions.assertEquals(application.keys().size(), 0); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/config/SaTokenConfigTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/config/SaTokenConfigTest.java new file mode 100644 index 000000000..d506daea1 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/config/SaTokenConfigTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.config; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.config.SaCookieConfig; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.config.SaTokenConfigFactory; + +/** + * 配置类测试 + * + * @author click33 + * @since 2022-9-4 + */ +public class SaTokenConfigTest { + + // 基本 get set 测试 + @Test + public void testProp() { + SaTokenConfig config = new SaTokenConfig(); + + config.setTokenName("nav-token"); + Assertions.assertEquals(config.getTokenName(), "nav-token"); + + config.setTimeout(100204); + Assertions.assertEquals(config.getTimeout(), 100204); + + config.setActiveTimeout(1804); + Assertions.assertEquals(config.getActiveTimeout(), 1804); + + config.setIsConcurrent(false); + Assertions.assertEquals(config.getIsConcurrent(), false); + + config.setIsShare(false); + Assertions.assertEquals(config.getIsShare(), false); + + config.setMaxLoginCount(11); + Assertions.assertEquals(config.getMaxLoginCount(), 11); + + config.setIsReadBody(false); + Assertions.assertEquals(config.getIsReadBody(), false); + + config.setIsReadHeader(false); + Assertions.assertEquals(config.getIsReadHeader(), false); + + config.setIsReadCookie(false); + Assertions.assertEquals(config.getIsReadCookie(), false); + + config.setTokenStyle("tik"); + Assertions.assertEquals(config.getTokenStyle(), "tik"); + + config.setDataRefreshPeriod(111); + Assertions.assertEquals(config.getDataRefreshPeriod(), 111); + + config.setTokenSessionCheckLogin(false); + Assertions.assertEquals(config.getTokenSessionCheckLogin(), false); + + config.setAutoRenew(false); + Assertions.assertEquals(config.getAutoRenew(), false); + + config.setTokenPrefix("token"); + Assertions.assertEquals(config.getTokenPrefix(), "token"); + + config.setIsPrint(false); + Assertions.assertEquals(config.getIsPrint(), false); + + config.setIsLog(false); + Assertions.assertEquals(config.getIsLog(), false); + + config.setJwtSecretKey("NgdfaXasARggr"); + Assertions.assertEquals(config.getJwtSecretKey(), "NgdfaXasARggr"); + + config.setSameTokenTimeout(1004); + Assertions.assertEquals(config.getSameTokenTimeout(), 1004); + + config.setHttpBasic("sa:123456"); + Assertions.assertEquals(config.getHttpBasic(), "sa:123456"); + + config.setCurrDomain("http://127.0.0.1:8084"); + Assertions.assertEquals(config.getCurrDomain(), "http://127.0.0.1:8084"); + + config.setCheckSameToken(false); + Assertions.assertEquals(config.getCheckSameToken(), false); + + SaCookieConfig scc = new SaCookieConfig(); + config.setCookie(scc); + Assertions.assertEquals(config.getCookie(), scc); + + config.toString(); + } + + // 从文件读取 + @Test + public void testSaTokenConfigFactory() { + SaTokenConfig config = SaTokenConfigFactory.createConfig("sa-token2.properties"); + Assertions.assertEquals(config.getTokenName(), "use-token"); + Assertions.assertEquals(config.getTimeout(), 9000); + Assertions.assertEquals(config.getActiveTimeout(), 240); + Assertions.assertEquals(config.getIsConcurrent(), false); + Assertions.assertEquals(config.getIsShare(), false); + Assertions.assertEquals(config.getIsLog(), true); + } + + // 测试 SaCookieConfig + @Test + public void testSaCookieConfig() { + SaCookieConfig config = new SaCookieConfig(); + + config.setDomain("stp.cn"); + Assertions.assertEquals(config.getDomain(), "stp.cn"); + + config.setPath("/pro/"); + Assertions.assertEquals(config.getPath(), "/pro/"); + + config.setSecure(true); + Assertions.assertEquals(config.getSecure(), true); + + config.setHttpOnly(false); + Assertions.assertEquals(config.getHttpOnly(), false); + + config.setSameSite("lax"); + Assertions.assertEquals(config.getSameSite(), "lax"); + + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaCookieTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaCookieTest.java new file mode 100644 index 000000000..1a26b7eb9 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaCookieTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.context.model; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.context.model.SaCookie; + +/** + * SaFoxUtil 工具类测试 + * + * @author click33 + * @since 2022-2-8 22:14:25 + */ +public class SaCookieTest { + + @Test + public void test() { + SaCookie cookie = new SaCookie("satoken", "xxxx-xxxx-xxxx-xxxx") + .setDomain("https://sa-token.com/") + .setMaxAge(-1) + .setPath("/") + .setSameSite("Lax") + .setHttpOnly(true) + .setSecure(true); + + Assertions.assertEquals(cookie.getName(), "satoken"); + Assertions.assertEquals(cookie.getValue(), "xxxx-xxxx-xxxx-xxxx"); + Assertions.assertEquals(cookie.getDomain(), "https://sa-token.com/"); + Assertions.assertEquals(cookie.getMaxAge(), -1); + Assertions.assertEquals(cookie.getPath(), "/"); + Assertions.assertEquals(cookie.getSameSite(), "Lax"); + Assertions.assertEquals(cookie.getHttpOnly(), true); + Assertions.assertEquals(cookie.getSecure(), true); + Assertions.assertEquals(cookie.toHeaderValue(), "satoken=xxxx-xxxx-xxxx-xxxx; Domain=https://sa-token.com/; Path=/; Secure; HttpOnly; SameSite=Lax"); + + Assertions.assertNotNull(cookie.toString()); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaTokenContextDefaultImplTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaTokenContextDefaultImplTest.java new file mode 100644 index 000000000..2dce565fc --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaTokenContextDefaultImplTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.context.model; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.context.SaTokenContextDefaultImpl; +import cn.dev33.satoken.exception.SaTokenException; + +/** + * 默认上下文测试 + * + * @author click33 + * @since 2022-9-5 + */ +public class SaTokenContextDefaultImplTest { + + @Test + public void testSaTokenContextDefaultImpl() { + SaTokenContextDefaultImpl context = new SaTokenContextDefaultImpl(); + Assertions.assertThrows(SaTokenException.class, () -> context.getStorage()); + Assertions.assertThrows(SaTokenException.class, () -> context.getRequest()); + Assertions.assertThrows(SaTokenException.class, () -> context.getResponse()); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/dao/SaTokenDaoTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/dao/SaTokenDaoTest.java new file mode 100644 index 000000000..4244c8812 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/dao/SaTokenDaoTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.dao; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; +import cn.dev33.satoken.session.SaSession; + +/** + * SaTokenDao 持久层 测试 + * + * @author click33 + * @since 2022-2-9 15:39:38 + */ +public class SaTokenDaoTest { + + SaTokenDao dao = new SaTokenDaoDefaultImpl(); + + // 字符串存取 + @Test + public void get() { + dao.set("name", "zhangsan", 60); + Assertions.assertEquals(dao.get("name"), "zhangsan"); + Assertions.assertTrue(dao.getTimeout("name") <= 60); + Assertions.assertEquals(dao.getTimeout("name2"), -2); + + dao.update("name", "lisi"); + Assertions.assertEquals(dao.get("name"), "lisi"); + + dao.updateTimeout("name", 100); + Assertions.assertTrue(dao.getTimeout("name") <= 100); + + dao.delete("name"); + Assertions.assertEquals(dao.get("name"), null); + } + + // 对象存取 + @Test + public void getObject() { + dao.setObject("name", "zhangsan", 60); + Assertions.assertEquals(dao.getObject("name"), "zhangsan"); + Assertions.assertTrue(dao.getObjectTimeout("name") <= 60); + + dao.updateObject("name", "lisi"); + Assertions.assertEquals(dao.getObject("name"), "lisi"); + + dao.updateObjectTimeout("name", 100); + Assertions.assertTrue(dao.getObjectTimeout("name") <= 100); + + dao.deleteObject("name"); + Assertions.assertEquals(dao.getObject("name"), null); + } + + // SaSession 存取 + @Test + public void getSession() { + SaSession session = new SaSession("session-1001"); + + dao.setSession(session, 60); + Assertions.assertEquals(dao.getSession("session-1001").getId(), session.getId()); + Assertions.assertTrue(dao.getSessionTimeout("session-1001") <= 60); + + SaSession session2 = new SaSession("session-1001"); + dao.updateSession(session2); + Assertions.assertEquals(dao.getSession("session-1001").getId(), session2.getId()); + + dao.updateSessionTimeout("session-1001", 100); + Assertions.assertTrue(dao.getSessionTimeout("session-1001") <= 100); + + dao.deleteSession("session-1001"); + Assertions.assertEquals(dao.getSession("session-1001"), null); + } + + // 测试永久有效期的写值改值 + @Test + public void testUpdate() { + + // ----------- 字符串 相关 + + // 永久有效 + dao.set("age", "20", -1); + Assertions.assertEquals(dao.get("age"), "20"); + Assertions.assertEquals(dao.getTimeout("age"), SaTokenDao.NEVER_EXPIRE); + + // 修改值 + dao.update("age", "22"); + Assertions.assertEquals(dao.get("age"), "22"); + // 有效期应该不变,还是永久 + Assertions.assertEquals(dao.getTimeout("age"), SaTokenDao.NEVER_EXPIRE); + + + // ----------- Session 相关 + + // 永久有效 + SaSession session = new SaSession("session-1001"); + dao.setSession(session, -1); + Assertions.assertEquals(dao.getSession("session-1001").getId(), session.getId()); + Assertions.assertEquals(dao.getSessionTimeout("session-1001"), SaTokenDao.NEVER_EXPIRE); + + // 修改值 + dao.updateSession(session); + Assertions.assertEquals(dao.getSession("session-1001").getId(), session.getId()); + // 有效期应该不变,还是永久 + Assertions.assertEquals(dao.getSessionTimeout("session-1001"), SaTokenDao.NEVER_EXPIRE); + + + // ----------- 无效update + dao.update("mid", "zhang"); + Assertions.assertNull(dao.get("mid")); + } + + // timeout为0或者小于等于-2时,不写入 + @Test + public void test0Timeout() { + + // ----------- 字符串 相关 + + // 字符串 0 和 <-2 + dao.set("avatar", "1.jpg", 0); + Assertions.assertNull(dao.get("avatar")); + + dao.set("avatar", "1.jpg", -9); + Assertions.assertNull(dao.get("avatar")); + + // ----------- Session 相关 + + // Session 0 和 <-2 + SaSession session = new SaSession("session-1001"); + dao.setSession(session, 0); + Assertions.assertNull(dao.getSession("session-1001")); + + dao.setSession(session, -9); + Assertions.assertNull(dao.getSession("session-1001")); + } + + // TO-DO 和时间相关的测试 + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/fun/IsRunFunctionTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/fun/IsRunFunctionTest.java new file mode 100644 index 000000000..1f18919c7 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/fun/IsRunFunctionTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.fun; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.fun.IsRunFunction; + +/** + * IsRunFunction 测试 + * + * @author click33 + * @since 2022-2-9 16:11:10 + */ +public class IsRunFunctionTest { + + @Test + public void test() { + + class TempClass{ + int count = 1; + } + TempClass obj = new TempClass(); + + IsRunFunction fun = new IsRunFunction(true); + fun.exe(()->{ + obj.count = 2; + }).noExe(()->{ + obj.count = 3; + }); + + Assertions.assertEquals(obj.count, 2); + } + + @Test + public void test2() { + + class TempClass{ + int count = 1; + } + TempClass obj = new TempClass(); + + IsRunFunction fun = new IsRunFunction(false); + fun.exe(()->{ + obj.count = 2; + }).noExe(()->{ + obj.count = 3; + }); + + Assertions.assertEquals(obj.count, 3); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/json/SaJsonTemplateDefaultImplTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/json/SaJsonTemplateDefaultImplTest.java new file mode 100644 index 000000000..5347c358a --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/json/SaJsonTemplateDefaultImplTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.json; + +import cn.dev33.satoken.exception.NotImplException; +import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; +import cn.dev33.satoken.util.SoMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * json默认实现类测试 + * + * @author click33 + * @since 2022-9-1 + */ +public class SaJsonTemplateDefaultImplTest { + + @Test + public void testSaJsonTemplateDefaultImpl() { + SaJsonTemplateDefaultImpl saJsonTemplate = new SaJsonTemplateDefaultImpl(); + // 组件未实现 + Assertions.assertThrows(NotImplException.class, () -> { + saJsonTemplate.jsonToMap("{}"); + }); + // 组件未实现 + Assertions.assertThrows(NotImplException.class, () -> { + saJsonTemplate.objectToJson(SoMap.getSoMap("name", "zhangsan")); + }); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/package-info.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/package-info.java new file mode 100644 index 000000000..a6c2d71c9 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/package-info.java @@ -0,0 +1,19 @@ +/** + * 核心包测试 + */ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core; \ No newline at end of file diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/BCryptTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/BCryptTest.java new file mode 100644 index 000000000..d10947787 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/BCryptTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.secure; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.secure.BCrypt; + +/** + * BCrypt 加密测试 + * + * @author dream. + * @since 2022/1/20 + */ +public class BCryptTest { + + @Test + public void testCheckpw() { + final String hashed = BCrypt.hashpw("12345"); +// System.out.println(hashed); + Assertions.assertTrue(BCrypt.checkpw("12345", hashed)); + Assertions.assertFalse(BCrypt.checkpw("123456", hashed)); + } + +} \ No newline at end of file diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaBase64UtilTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaBase64UtilTest.java new file mode 100644 index 000000000..ebf89b80e --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaBase64UtilTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.secure; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.secure.SaBase64Util; + +/** + * SaBase64Util 测试 + * + * @author click33 + * @since 2022-2-9 + */ +public class SaBase64UtilTest { + + @Test + public void test() { + // 文本 + String text = "Sa-Token 一个轻量级java权限认证框架"; + + // 使用Base64编码 + String base64Text = SaBase64Util.encode(text); + Assertions.assertEquals(base64Text, "U2EtVG9rZW4g5LiA5Liq6L276YeP57qnamF2Yeadg+mZkOiupOivgeahhuaetg=="); + + // 使用Base64解码 + String text2 = SaBase64Util.decode(base64Text); + Assertions.assertEquals(text2, "Sa-Token 一个轻量级java权限认证框架"); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaSecureUtilTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaSecureUtilTest.java new file mode 100644 index 000000000..b6a23af9e --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaSecureUtilTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.secure; + +import java.util.HashMap; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.secure.SaSecureUtil; + +/** + * SaSecureUtil 加密工具类 测试 + * + * @author click33 + * @since 2022-2-9 + */ +public class SaSecureUtilTest { + + @Test + public void test() { + + // md5加密 + Assertions.assertEquals(SaSecureUtil.md5("123456"), "e10adc3949ba59abbe56e057f20f883e"); + + // sha1加密 + Assertions.assertEquals(SaSecureUtil.sha1("123456"), "7c4a8d09ca3762af61e59520943dc26494f8941b"); + + // sha256加密 + Assertions.assertEquals(SaSecureUtil.sha256("123456"), "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92"); + + // md5加盐加密: md5(md5(str) + md5(salt)) + Assertions.assertEquals(SaSecureUtil.md5BySalt("123456", "salt"), "f52020dca765fd3943ed40a615dc2c5c"); + + } + + @Test + public void aesEncrypt() { + // 定义秘钥和明文 + String key = "123456"; + String text = "Sa-Token 一个轻量级java权限认证框架"; + + // 加密 + String ciphertext = SaSecureUtil.aesEncrypt(key, text); + Assertions.assertEquals(ciphertext, "KmSqfwxY5BRuWoHMWJqtebcOZ2lEEZaj2OSi1Ei8pRx4zdi24wsnwsTQVjbXRQ0M"); + + // 解密 + String text2 = SaSecureUtil.aesDecrypt(key, ciphertext); + Assertions.assertEquals(text2, "Sa-Token 一个轻量级java权限认证框架"); + } + + @Test + public void rsaEncryptByPublic() { + // 定义私钥和公钥 + 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); +// Assert.assertEquals(ciphertext, "d9e01fd105b059e975c524a1f4dccbe10dfc3a23b931a9e168ecb0a5758a29c45532254679f86cf83a63e5cc21ef631802fe70ea47e7519f5d96e0d1fab38a6f6dbebdb34b106ce7f27c341838e4e88a8ff3298c519c29a3f0944cf8f668bfecd9394f16945d85d84c4d813d12ecadf34bfb21850c383977b5b2de848fa40995"); + + // 使用私钥解密 + String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext); + Assertions.assertEquals(text2, "Sa-Token 一个轻量级java权限认证框架"); + } + + @Test + public void rsaEncryptByPrivate() throws Exception { + + // 生成私钥和公钥 + HashMap map = SaSecureUtil.rsaGenerateKeyPair(); + String privateKey = map.get("private"); + String publicKey = map.get("public"); + + // 文本 + String text = "Sa-Token 一个轻量级java权限认证框架"; + + // 使用公钥加密 + String ciphertext = SaSecureUtil.rsaEncryptByPrivate(privateKey, text); + + // 使用私钥解密 + String text2 = SaSecureUtil.rsaDecryptByPublic(publicKey, ciphertext); + Assertions.assertEquals(text2, "Sa-Token 一个轻量级java权限认证框架"); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionCustomUtilTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionCustomUtilTest.java new file mode 100644 index 000000000..e758cbdb9 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionCustomUtilTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.session; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.SaSessionCustomUtil; + +/** + * SaSession 测试 + * + * @author click33 + * @since 2022-2-9 + */ +public class SaSessionCustomUtilTest { + + // 测试自定义Session + @Test + public void testCustomSession() { + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 刚开始不存在 + Assertions.assertFalse(SaSessionCustomUtil.isExists("art-1")); + SaSession session = dao.getSession("satoken:custom:session:" + "art-1"); + Assertions.assertNull(session); + + // 调用一下 + SaSessionCustomUtil.getSessionById("art-1"); + SaSessionCustomUtil.getSessionById("art-1", false); + + // 就存在了 + Assertions.assertTrue(SaSessionCustomUtil.isExists("art-1")); + SaSession session2 = dao.getSession("satoken:custom:session:" + "art-1"); + Assertions.assertNotNull(session2); + + // 给删除掉 + SaSessionCustomUtil.deleteSessionById("art-1"); + + // 就又不存在了 + Assertions.assertFalse(SaSessionCustomUtil.isExists("art-1")); + SaSession session3 = dao.getSession("satoken:custom:session:" + "art-1"); + Assertions.assertNull(session3); + + // 调用了也不会存在 + SaSessionCustomUtil.getSessionById("art-4", false); + SaSession session4 = dao.getSession("satoken:custom:session:" + "art-2"); + Assertions.assertNull(session4); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionTest.java new file mode 100644 index 000000000..451b1a6ea --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.session; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.SaTerminalInfo; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * SaSession 测试 + * + * @author click33 + * @since 2022-2-9 + */ +public class SaSessionTest { + + // 基础属性 + @Test + public void testProp() { + SaSession session = new SaSession("session-1001"); + Assertions.assertEquals(session.getId(), "session-1001"); + + // 属性读取 + session = new SaSession(); + session.setId("session-1009"); + Assertions.assertEquals(session.getId(), "session-1009"); + + session.setCreateTime(1662241013902L); + Assertions.assertEquals(session.getCreateTime(), 1662241013902L); + } + + // 基础存取值 + @Test + public void testSetGet() { + + // 基础取值 + SaSession session = new SaSession("session-1002"); + session.set("name", "zhangsan"); + session.set("age", 18); + Assertions.assertEquals(session.get("name"), "zhangsan"); + Assertions.assertEquals((int)session.get("age", 20), 18); + Assertions.assertEquals((int)session.get("age2", 20), 20); + Assertions.assertEquals(session.getModel("age", Double.class).getClass(), Double.class); + + // 原本无值时才会写入 + session.setByNull("name", "lisi"); + Assertions.assertEquals(session.get("name"), "zhangsan"); + session.setByNull("name2", "lisi"); + Assertions.assertEquals(session.get("name2"), "lisi"); + + // 复杂取值 + class User { + String name; + int age; + User(String name, int age) { + this.name = name; + this.age = age; + } + } + User user = new User("zhangsan", 18); + session.set("user", user); + + User user2 = session.getModel("user", User.class); + Assertions.assertNotNull(user2); + Assertions.assertEquals(user2.name, "zhangsan"); + Assertions.assertEquals(user2.age, 18); + } + + // 测试有效期 + @Test + public void testSessionTimeout() { + // 修改剩余有效期 + SaSession session = new SaSession("session-1005"); + SaManager.getSaTokenDao().setSession(session, 20000); + session.updateMaxTimeout(100); + Assertions.assertTrue(session.timeout() <= 100); + System.out.println(session.timeout()); + // 仍然是 <=100 + session.updateMaxTimeout(1000); + Assertions.assertTrue(session.timeout() <= 100); + System.out.println(session.timeout()); + // Min 修改 + session.updateMinTimeout(-1); + System.out.println(session.timeout()); + Assertions.assertTrue(session.timeout() == -1); + } + + // 测试token 签名 + @Test + public void testSaTerminalInfo() { + SaSession session = new SaSession("session-1002"); + + // 添加 Token 签名 + session.addTerminal(new SaTerminalInfo(1, "xxxx-xxxx-xxxx-xxxx-1", "PC", null)); + session.addTerminal(new SaTerminalInfo(2, "xxxx-xxxx-xxxx-xxxx-2", "APP", null)); + + // 查询 + Assertions.assertEquals(session.getTerminalList().size(), 2); + Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-1").getDeviceType(), "PC"); + Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-2").getDeviceType(), "APP"); + + // 删除一个 + session.removeTerminal("xxxx-xxxx-xxxx-xxxx-1"); + Assertions.assertEquals(session.getTerminalList().size(), 1); + + // 删除一个不存在的,则不影响 SaTerminalInfo 列表 + session.removeTerminal("xxxx-xxxx-xxxx-xxxx-999"); + Assertions.assertEquals(session.getTerminalList().size(), 1); + + // 重置整个签名列表 + List list = Arrays.asList( + new SaTerminalInfo(1, "xxxx-xxxx-xxxx-xxxx-1", "WEB", null), + new SaTerminalInfo(2, "xxxx-xxxx-xxxx-xxxx-2", "phone", null), + new SaTerminalInfo(3, "xxxx-xxxx-xxxx-xxxx-3", "ipad", null) + ); + session.setTerminalList(list); + Assertions.assertEquals(session.getTerminalList().size(), 3); + Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-1").getDeviceType(), "WEB"); + Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-2").getDeviceType(), "phone"); + Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-3").getDeviceType(), "ipad"); + } + + // 测试重置 DataMap + @Test + public void testDataMap() { + SaSession session = new SaSession("session-1003"); + session.set("key1", "value1"); + session.set("key2", "value2"); + session.set("key3", "value3"); + + // 所有数据 + Assertions.assertEquals(session.keys().size(), 3); + Assertions.assertEquals(session.getDataMap().size(), 3); + + // 重置所有数据 + Map dataMap = new ConcurrentHashMap<>(); + dataMap.put("aaa", "111"); + dataMap.put("bbb", "222"); + session.refreshDataMap(dataMap); + Assertions.assertEquals(session.keys().size(), 2); + + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaTerminalInfoTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaTerminalInfoTest.java new file mode 100644 index 000000000..6db702f82 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaTerminalInfoTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.session; + +import cn.dev33.satoken.session.SaTerminalInfo; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * SaTerminalInfo 相关测试 + * + * @author click33 + * @since 2022-9-4 + */ +public class SaTerminalInfoTest { + + // 测试 + @Test + public void testSaTerminalInfo() { + SaTerminalInfo terminal = new SaTerminalInfo(); + terminal.setDeviceType("PC"); + terminal.setTokenValue("ttt-value"); + + Assertions.assertEquals(terminal.getDeviceType(), "PC"); + Assertions.assertEquals(terminal.getTokenValue(), "ttt-value"); + + Assertions.assertNotNull(terminal.toString()); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/sign/SaSignTemplateTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/sign/SaSignTemplateTest.java new file mode 100644 index 000000000..4351a83fe --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/sign/SaSignTemplateTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.sign; + +import cn.dev33.satoken.sign.SaSignManager; +import cn.dev33.satoken.sign.config.SaSignConfig; +import cn.dev33.satoken.util.SoMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * API 接口签名测试 + * + * @author click33 + * @since 2022-9-2 + */ +public class SaSignTemplateTest { + + String key = "SwqFmsKxcbq23"; + + // 连接参数列表 + @Test + public void testJoinParamsDictSort() { + SoMap map = SoMap.getSoMap() + .set("name", "zhang") + .set("age", 18) + .set("sex", "女"); + String str = SaSignManager.getSaSignTemplate().joinParamsDictSort(map); + + // 按照音序排列 + Assertions.assertEquals(str, "age=18&name=zhang&sex=女"); + } + + // 给参数签名 + @Test + public void testCreateSign() { + SoMap map = SoMap.getSoMap() + .set("name", "zhang") + .set("age", 18) + .set("sex", "女"); + SaSignManager.getSaSignTemplate().setSignConfig(new SaSignConfig().setSecretKey(key)); + String sign = SaSignManager.getSaSignTemplate().createSign(map); + Assertions.assertEquals(sign, "6f5e844a53e74363c2f6b24f64c4f0ff"); + + // 多次签名,结果一致 + String sign2 = SaSignManager.getSaSignTemplate().createSign(map); + Assertions.assertEquals(sign, sign2); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/stp/TokenInfoTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/stp/TokenInfoTest.java new file mode 100644 index 000000000..d6b29895f --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/stp/TokenInfoTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.stp; + +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.SaTokenInfo; +import cn.dev33.satoken.util.SaTokenConsts; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * Token 参数扩展 + * + * @author click33 + * @since 2022-9-5 + */ +public class TokenInfoTest { + + @Test + public void test() { + SaTokenInfo info = new SaTokenInfo(); + info.setTokenName("satoken"); + info.setTokenValue("xxxxx-xxxxx-xxxxx-xxxxx"); + info.setIsLogin(true); + info.setLoginId(10001); + info.setLoginType("login"); + info.setTokenTimeout(1800); + info.setSessionTimeout(120); + info.setTokenSessionTimeout(1800); + info.setTokenActiveTimeout(120); + info.setLoginDeviceType("PC"); + info.setTag("xxx"); + + Assertions.assertEquals(info.getTokenName(), "satoken"); + Assertions.assertEquals(info.getTokenValue(), "xxxxx-xxxxx-xxxxx-xxxxx"); + Assertions.assertEquals(info.getIsLogin(), true); + Assertions.assertEquals(info.getLoginId(), 10001); + Assertions.assertEquals(info.getLoginType(), "login"); + Assertions.assertEquals(info.getTokenTimeout(), 1800); + Assertions.assertEquals(info.getSessionTimeout(), 120); + Assertions.assertEquals(info.getTokenSessionTimeout(), 1800); + Assertions.assertEquals(info.getTokenActiveTimeout(), 120); + Assertions.assertEquals(info.getLoginDeviceType(), "PC"); + Assertions.assertEquals(info.getTag(), "xxx"); + + Assertions.assertNotNull(info.toString()); + } + + @Test + public void testLoginParameter() { + Assertions.assertEquals(new SaLoginParameter().setDeviceType("PC").getDeviceType(), "PC"); + Assertions.assertEquals(new SaLoginParameter().setIsLastingCookie(false).getIsLastingCookie(), false); + Assertions.assertEquals(new SaLoginParameter().setTimeout(1600).getTimeout(), 1600); + Assertions.assertEquals(new SaLoginParameter().setToken("token-xxx").getToken(), "token-xxx"); + Assertions.assertEquals(new SaLoginParameter().setExtra("age", 18).getExtra("age"), 18); + + Map extraData = new HashMap<>(); + extraData.put("age", 20); + SaLoginParameter lm = new SaLoginParameter().setExtraData(extraData); + Assertions.assertEquals(lm.getExtraData(), extraData); + Assertions.assertEquals(lm.getExtra("age"), 20); + Assertions.assertTrue(lm.haveExtraData()); + Assertions.assertNotNull(lm.toString()); + + // 计算 CookieTimeout + SaLoginParameter loginParameter = SaLoginParameter + .create() + .setTimeout(-1); + Assertions.assertEquals(loginParameter.getCookieTimeout(), Integer.MAX_VALUE); + Assertions.assertEquals(loginParameter.getDeviceType(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTokenTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTokenTest.java new file mode 100644 index 000000000..65d7fa41c --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTokenTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.temp; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.temp.SaTempUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * 临时Token模块测试 + * + * @author click33 + * @since 2022-9-1 + */ +public class SaTempTokenTest { + + // 测试:临时Token认证模块 + @Test + public void testSaTemp() { + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 生成token + String token = SaTempUtil.createToken("group-1014", 200); +// System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).timedCache.dataMap.keySet()); +// System.out.println("satoken:temp-token:" + ":" + token); + Assertions.assertNotNull(token); + Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token), "group-1014"); + + // 解析token + String value = SaTempUtil.parseToken(token, String.class); + Assertions.assertEquals(value, "group-1014"); + + // 解析 token 并裁剪前缀 + long value2 = SaTempUtil.parseToken(token, "group-", Long.class); + Assertions.assertEquals(value2, 1014); + + // 默认类型 + Object value3 = SaTempUtil.parseToken(token); + Assertions.assertEquals(value3, "group-1014"); + + // 转换类型 + String value4 = SaTempUtil.parseToken(token, String.class); + Assertions.assertEquals(value4, "group-1014"); + + // 过期时间 + long timeout = SaTempUtil.getTimeout(token); + Assertions.assertTrue(timeout > 195); + Assertions.assertTrue(timeout < 201); + + // 回收token + SaTempUtil.deleteToken(token); + String value5 = SaTempUtil.parseToken(token, String.class); + Assertions.assertNull(value5); + Assertions.assertNull(dao.getObject("satoken:temp-token:" + ":" + token)); + } + + // 测试:临时Token认证模块索引 + @Test + public void testSaTempIndex() { + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 生成token + String token1 = SaTempUtil.createToken("1001", 200, true); + String token2 = SaTempUtil.createToken("1001", 300, true); + String token3 = SaTempUtil.createToken("1001", 400, true); + + Assertions.assertNotNull(token1); + Assertions.assertNotNull(token2); + Assertions.assertNotNull(token3); + // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap); + + // 解析token + Assertions.assertEquals(SaTempUtil.parseToken(token1, String.class), "1001"); + Assertions.assertEquals(SaTempUtil.parseToken(token2, String.class), "1001"); + Assertions.assertEquals(SaTempUtil.parseToken(token3, String.class), "1001"); + + // 缓存数据比对 + Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token1), "1001"); + Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token2), "1001"); + Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token3), "1001"); + + // 索引 + List tempTokenList = SaTempUtil.getTempTokenList("1001"); + Assertions.assertEquals(tempTokenList.size(), 3); + Assertions.assertTrue(tempTokenList.contains(token1)); + Assertions.assertTrue(tempTokenList.contains(token2)); + Assertions.assertTrue(tempTokenList.contains(token3)); + + long sessionTimeout = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); + Assertions.assertTrue(sessionTimeout > 395); + Assertions.assertTrue(sessionTimeout < 401); + + // 移除一个 token + SaTempUtil.deleteToken(token3); + Assertions.assertNull(SaTempUtil.parseToken(token3, String.class)); + Assertions.assertNull(dao.getObject("satoken:temp-token:" + token3)); + + List tempTokenList2 = SaTempUtil.getTempTokenList("1001"); + Assertions.assertEquals(tempTokenList2.size(), 2); + Assertions.assertFalse(tempTokenList2.contains(token3)); + + long sessionTimeout2 = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); + Assertions.assertTrue(sessionTimeout2 > 295); + Assertions.assertTrue(sessionTimeout2 < 301); + + // 新增一个 token + String token4 = SaTempUtil.createToken("1001", -1, true); + Assertions.assertEquals(SaTempUtil.parseToken(token4, String.class), "1001"); + + List tempTokenList3 = SaTempUtil.getTempTokenList("1001"); + Assertions.assertEquals(tempTokenList3.size(), 3); + Assertions.assertTrue(tempTokenList3.contains(token4)); + + long sessionTimeout4 = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); + Assertions.assertEquals(-1, sessionTimeout4); + } + + @Test + public void testGetJwtSecretKey() { + // 秘钥默认为null + String jwtSecretKey = SaManager.getSaTempTemplate().getJwtSecretKey(); + Assertions.assertNull(jwtSecretKey); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaFoxUtilTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaFoxUtilTest.java new file mode 100644 index 000000000..950c25691 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaFoxUtilTest.java @@ -0,0 +1,411 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.util; + +import cn.dev33.satoken.util.SaFoxUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * SaFoxUtil 工具类测试 + * + * @author click33 + * @since 2022-2-8 22:14:25 + */ +public class SaFoxUtilTest { + + @Test + public void getRandomString() { + String randomString = SaFoxUtil.getRandomString(8); + Assertions.assertEquals(randomString.length(), 8); + } + + @Test + public void isEmpty() { + Assertions.assertTrue(SaFoxUtil.isEmpty("")); + Assertions.assertTrue(SaFoxUtil.isEmpty(null)); + Assertions.assertFalse(SaFoxUtil.isEmpty("abc")); + + Assertions.assertTrue(SaFoxUtil.isNotEmpty("abc")); + Assertions.assertFalse(SaFoxUtil.isNotEmpty(null)); + Assertions.assertFalse(SaFoxUtil.isNotEmpty("")); + } + + @Test + public void equals() { + Assertions.assertTrue(SaFoxUtil.equals(null, null)); + Assertions.assertTrue(SaFoxUtil.equals("a", "a")); + Assertions.assertFalse(SaFoxUtil.equals("1", 1)); + Assertions.assertFalse(SaFoxUtil.equals("1", null)); + Assertions.assertFalse(SaFoxUtil.equals(null, "1")); + } + + @Test + public void getMarking28() { + Assertions.assertNotEquals(SaFoxUtil.getMarking28(), SaFoxUtil.getMarking28()); + } + + @Test + public void formatDate() { + Instant instant = Instant.ofEpochMilli(1644328600364L); + ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai")); + String formatDate = SaFoxUtil.formatDate(zonedDateTime); + Assertions.assertEquals(formatDate, "2022-02-08 21:56:40"); + } + + @Test + public void searchList() { + // 原始数据 + List dataList = Arrays.asList("token1", "token2", "token3", "token4", "token5", "aaa1"); + + // 分页 + List list1 = SaFoxUtil.searchList(dataList, 1, 2, true); + Assertions.assertEquals(list1.size(), 2); + Assertions.assertEquals(list1.get(0), "token2"); + Assertions.assertEquals(list1.get(1), "token3"); + + // 前缀筛选 + List list2 = SaFoxUtil.searchList(dataList, "token", "", 0, 10, true); + Assertions.assertEquals(list2.size(), 5); + + // 关键字筛选 + List list3 = SaFoxUtil.searchList(dataList, "", "1", 0, 10, true); + Assertions.assertEquals(list3.size(), 2); + + // 综合筛选 + List list4 = SaFoxUtil.searchList(dataList, "token", "1", 0, 10, true); + Assertions.assertEquals(list4.size(), 1); + + // 关键字为null时,效果和 "" 等同 + List list4_2 = SaFoxUtil.searchList(dataList, null, null, 0, 10, true); + List list4_3 = SaFoxUtil.searchList(dataList, "", "", 0, 10, true); + Assertions.assertEquals(list4_2.get(0), list4_3.get(0)); + + + // 不做分页 + List list5 = SaFoxUtil.searchList(dataList, "", "", 0, -1, true); + Assertions.assertEquals(list5.size(), dataList.size()); + + // 反序排列 list6的第一个元素 == dataList最后一个元素 + List list6 = SaFoxUtil.searchList(dataList, "", "", 0, -1, false); + Assertions.assertEquals(list6.get(0), dataList.get(dataList.size() - 1)); + } + + @Test + public void vagueMatch() { + // 不模糊 + Assertions.assertTrue(SaFoxUtil.vagueMatch("hello", "hello")); + + // 正常模糊 + Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello world")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello*")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("hello*", "he")); + + // 带 - + Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-add")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-*")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*", "user")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*-add-*", "user-xx-add-1")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*-add-*", "user-add-1")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*", "usermgt-list")); + + // 带 / + Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/add")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/*")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*", "user")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*/add/*", "user/xx/add/1")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*/add/*", "user/add/1")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*", "usermgt/list")); + + // 带 : + Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:add")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:*")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*", "user")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*:add:*", "user:xx:add:1")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*:add:*", "user:add:1")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*", "usermgt:list")); + + // 带 . + Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user.")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user.add")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user.*")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*", "user")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*.add.*", "user.xx.add.1")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*.add.*", "user.add.1")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*", "usermgt.list")); + + // 极端情况 + Assertions.assertTrue(SaFoxUtil.vagueMatch(null, null)); + Assertions.assertFalse(SaFoxUtil.vagueMatch(null, "hello")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("hello*", null)); + + // url 匹配 + Assertions.assertTrue(SaFoxUtil.vagueMatch("*", "http://sa-sso-client1.com:9001/sso/login")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("http://sa-sso-client1.com:9001/*", "http://sa-sso-client1.com:9001/sso/login")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("http://sa-sso-client1.com:9001/*", "http://sa-sso-client1.com:9001/sso/login?name=1")); + Assertions.assertTrue(SaFoxUtil.vagueMatch("http://sa-sso-client1.com:9001/*", "http://sa-sso-client1.com:9001/sso/login?name=1&age=2")); + Assertions.assertFalse(SaFoxUtil.vagueMatch("http://sa-sso-client1.com:9001/*", "http://sa-sso-client1.com:9002")); + } + + @Test + public void isWrapperType() { + Assertions.assertTrue(SaFoxUtil.isWrapperType(Integer.class)); + Assertions.assertTrue(SaFoxUtil.isWrapperType(Short.class)); + Assertions.assertTrue(SaFoxUtil.isWrapperType(Long.class)); + Assertions.assertTrue(SaFoxUtil.isWrapperType(Byte.class)); + Assertions.assertTrue(SaFoxUtil.isWrapperType(Float.class)); + Assertions.assertTrue(SaFoxUtil.isWrapperType(Double.class)); + Assertions.assertTrue(SaFoxUtil.isWrapperType(Boolean.class)); + Assertions.assertTrue(SaFoxUtil.isWrapperType(Character.class)); + + Assertions.assertFalse(SaFoxUtil.isWrapperType(int.class)); + Assertions.assertFalse(SaFoxUtil.isWrapperType(long.class)); + Assertions.assertFalse(SaFoxUtil.isWrapperType(Object.class)); + } + + @Test + public void isBasicType() { + Assertions.assertTrue(SaFoxUtil.isBasicType(int.class)); + Assertions.assertTrue(SaFoxUtil.isBasicType(Integer.class)); + Assertions.assertTrue(SaFoxUtil.isBasicType(long.class)); + Assertions.assertTrue(SaFoxUtil.isBasicType(Long.class)); + Assertions.assertTrue(SaFoxUtil.isBasicType(String.class)); + + Assertions.assertFalse(SaFoxUtil.isBasicType(List.class)); + Assertions.assertFalse(SaFoxUtil.isBasicType(Map.class)); + } + + @Test + public void getValueByType() { + // 基础类型,转换 + Assertions.assertEquals(SaFoxUtil.getValueByType("1", int.class), 1); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", long.class), 1L); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", Long.class), 1L); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", String.class), "1"); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", short.class), (short)1); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", Short.class), (short)1); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", byte.class), (byte)1); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", Byte.class), (byte)1); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", float.class), 1f); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", Float.class), 1f); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", double.class), 1.0); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", Double.class), 1.0); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", boolean.class), false); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", Boolean.class), false); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", char.class), '1'); + Assertions.assertEquals(SaFoxUtil.getValueByType("1", Character.class), '1'); + Assertions.assertEquals(SaFoxUtil.getValueByType(1, String.class), "1"); + + // 复杂类型,还原 + Object obj = new ArrayList<>(); + Assertions.assertEquals(SaFoxUtil.getValueByType(obj, List.class).getClass(), ArrayList.class); + } + + @Test + public void joinParam() { + // 参数为空时,返回原url + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com", null), "https://sa-token.com"); + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com", ""), "https://sa-token.com"); + // url为空时,视为空字符串 + Assertions.assertEquals(SaFoxUtil.joinParam(null, "id=1"), "?id=1"); + Assertions.assertEquals(SaFoxUtil.joinParam("", "id=1"), "?id=1"); + + // 各种情况的测试 + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com", "id=1"), "https://sa-token.com?id=1"); + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com?", "id=1"), "https://sa-token.com?id=1"); + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com?name=zhang", "id=1"), "https://sa-token.com?name=zhang&id=1"); + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com?name=zhang&", "id=1"), "https://sa-token.com?name=zhang&id=1"); + + // 重载方法测试 + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com?name=zhang&", "id", 1), "https://sa-token.com?name=zhang&id=1"); + // url或key为null时,不拼接 + Assertions.assertEquals(SaFoxUtil.joinParam(null, "id", 1), null); + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com", null, 1), "https://sa-token.com"); + // value为null时,会拼接出一个null字符串 + Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.com", "id", null), "https://sa-token.com?id=null"); + } + + @Test + public void joinSharpParam() { + // 参数为空时,返回原url + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com", null), "https://sa-token.com"); + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com", ""), "https://sa-token.com"); + // url为空时,视为空字符串 + Assertions.assertEquals(SaFoxUtil.joinSharpParam(null, "id=1"), "#id=1"); + Assertions.assertEquals(SaFoxUtil.joinSharpParam("", "id=1"), "#id=1"); + + // 各种情况的测试 + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com", "id=1"), "https://sa-token.com#id=1"); + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com#", "id=1"), "https://sa-token.com#id=1"); + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com#name=zhang", "id=1"), "https://sa-token.com#name=zhang&id=1"); + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com#name=zhang&", "id=1"), "https://sa-token.com#name=zhang&id=1"); + + // 重载方法测试 + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com#name=zhang&", "id", 1), "https://sa-token.com#name=zhang&id=1"); + // url或key为null时,不拼接 + Assertions.assertEquals(SaFoxUtil.joinSharpParam(null, "id", 1), null); + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com", null, 1), "https://sa-token.com"); + // value为null时,会拼接出一个null字符串 + Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.com", "id", null), "https://sa-token.com#id=null"); + } + + @Test + public void spliceTwoUrl() { + // 其中一个为null时,直接返回另一个 + Assertions.assertEquals(SaFoxUtil.spliceTwoUrl("https://sa-sso-server.com/sso/auth", null), "https://sa-sso-server.com/sso/auth"); + Assertions.assertEquals(SaFoxUtil.spliceTwoUrl(null, "https://sa-sso-server.com/sso/auth"), "https://sa-sso-server.com/sso/auth"); + + // 正常情况,拼接 + Assertions.assertEquals(SaFoxUtil.spliceTwoUrl("https://sa-sso-server.com", "/sso/auth"), "https://sa-sso-server.com/sso/auth"); + + // url2以http开头时,直接返回url2 + Assertions.assertEquals(SaFoxUtil.spliceTwoUrl("https://sa-sso-server2.com", "https://sa-sso-server.com/sso/auth2"), "https://sa-sso-server.com/sso/auth2"); + } + + @Test + public void arrayJoin() { + Assertions.assertEquals(SaFoxUtil.arrayJoin(new String[] {"a", "b", "c"}), "a,b,c"); + Assertions.assertEquals(SaFoxUtil.arrayJoin(new String[] {}), ""); + Assertions.assertEquals(SaFoxUtil.arrayJoin(null), ""); + } + + @Test + public void isUrl() { + Assertions.assertTrue(SaFoxUtil.isUrl("https://sa-token.com")); + Assertions.assertTrue(SaFoxUtil.isUrl("https://www.baidu.com/")); + + Assertions.assertFalse(SaFoxUtil.isUrl(null)); + Assertions.assertFalse(SaFoxUtil.isUrl("")); + Assertions.assertFalse(SaFoxUtil.isUrl("htt://www.baidu.com/")); + Assertions.assertFalse(SaFoxUtil.isUrl("https:www.baidu.com/")); + Assertions.assertFalse(SaFoxUtil.isUrl("httpswwwbaiducom/")); + Assertions.assertFalse(SaFoxUtil.isUrl("https://www.baidu.com/,")); + } + + @Test + public void encodeUrl() { + Assertions.assertEquals(SaFoxUtil.encodeUrl("https://sa-token.com"), "https%3A%2F%2Fsa-token.com"); + Assertions.assertEquals(SaFoxUtil.decoderUrl("https%3A%2F%2Fsa-token.com"), "https://sa-token.com"); + } + + @Test + public void convertStringToList() { + List list = SaFoxUtil.convertStringToList("a,b,,c"); + Assertions.assertEquals(list.size(), 3); + Assertions.assertEquals(list.get(0), "a"); + Assertions.assertEquals(list.get(1), "b"); + Assertions.assertEquals(list.get(2), "c"); + + List list2 = SaFoxUtil.convertStringToList("a,"); + Assertions.assertEquals(list2.size(), 1); + + List list3 = SaFoxUtil.convertStringToList(","); + Assertions.assertEquals(list3.size(), 0); + + List list4 = SaFoxUtil.convertStringToList(""); + Assertions.assertEquals(list4.size(), 0); + + List list5 = SaFoxUtil.convertStringToList(null); + Assertions.assertEquals(list5.size(), 0); + } + + @Test + public void convertListToString() { + // 正常 + List list = Arrays.asList("a", "b", "c"); + Assertions.assertEquals(SaFoxUtil.convertListToString(list), "a,b,c"); + + // 空数组 + List list2 = Arrays.asList(); + Assertions.assertEquals(SaFoxUtil.convertListToString(list2), ""); + + // 空 + List list3 = null; + Assertions.assertEquals(SaFoxUtil.convertListToString(list3), ""); + } + + @Test + public void convertStringToArray() { + String[] array = SaFoxUtil.convertStringToArray("a,b,c"); + Assertions.assertEquals(array.length, 3); + Assertions.assertEquals(array[0], "a"); + Assertions.assertEquals(array[1], "b"); + Assertions.assertEquals(array[2], "c"); + + String[] array2 = SaFoxUtil.convertStringToArray("a,"); + Assertions.assertEquals(array2.length, 1); + + String[] array3 = SaFoxUtil.convertStringToArray(","); + Assertions.assertEquals(array3.length, 0); + + String[] array4 = SaFoxUtil.convertStringToArray(""); + Assertions.assertEquals(array4.length, 0); + + String[] array5 = SaFoxUtil.convertStringToArray(null); + Assertions.assertEquals(array5.length, 0); + } + + @Test + public void convertArrayToString() { + // 正常 + String[] array = new String[] {"a", "b", "c"}; + Assertions.assertEquals(SaFoxUtil.convertArrayToString(array), "a,b,c"); + + // null + String[] array2 = null; + Assertions.assertEquals(SaFoxUtil.convertArrayToString(array2), ""); + + // 空数组 + String[] array3 = new String[] {}; + Assertions.assertEquals(SaFoxUtil.convertArrayToString(array3), ""); + } + + @Test + public void emptyList() { + List list = SaFoxUtil.emptyList(); + Assertions.assertEquals(list.size(), 0); + } + + @Test + public void toList() { + List list = SaFoxUtil.toList("a","b", "c"); + Assertions.assertEquals(list.size(), 3); + Assertions.assertEquals(list.get(0), "a"); + Assertions.assertEquals(list.get(1), "b"); + Assertions.assertEquals(list.get(2), "c"); + } + + @Test + public void hasNonPrintableASCII() { + Assertions.assertFalse(SaFoxUtil.hasNonPrintableASCII("Hello World!")); + Assertions.assertTrue(SaFoxUtil.hasNonPrintableASCII("Hello\u0007World")); + Assertions.assertTrue(SaFoxUtil.hasNonPrintableASCII("Hello\tWorld")); + Assertions.assertTrue(SaFoxUtil.hasNonPrintableASCII("Hello\nWorld")); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaResultTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaResultTest.java new file mode 100644 index 000000000..6e0bccd20 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaResultTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.core.util; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.util.SaResult; + +/** + * SaResult 结果集 测试 + * + * @author click33 + * @since 2022-2-8 22:14:25 + */ +public class SaResultTest { + + // 构造函数构建 + @Test + public void test() { + // 无参构造时,默认所有参数为null + SaResult res = new SaResult(); + Assertions.assertEquals(res.getCode(), null); + Assertions.assertEquals(res.getMsg(), null); + Assertions.assertEquals(res.getData(), null); + + // 全参数构造 + SaResult res2 = new SaResult(200, "ok", "zhangsan"); + Assertions.assertEquals((int)res2.getCode(), 200); + Assertions.assertEquals(res2.getMsg(), "ok"); + Assertions.assertEquals(res2.getData(), "zhangsan"); + + // 自定义写值取值 + res.set("age", 18); + Assertions.assertEquals(res.get("age"), 18); + Assertions.assertEquals(res.get("age", String.class), "18"); + Assertions.assertEquals(res.getOrDefault("age", 20), 18); + Assertions.assertEquals(res.getOrDefault("age2", 20), 20); + } + + // 静态函数快速构建 + @Test + public void test2() { + // ok 和 error + Assertions.assertEquals((int)SaResult.ok().getCode(), 200); + Assertions.assertEquals((int)SaResult.error().getCode(), 500); + Assertions.assertEquals(SaResult.error("错误").getMsg(), "错误"); + + // 指定code + SaResult res = SaResult.code(201); + Assertions.assertEquals((int)res.getCode(), 201); + + // + // 全参数构造 + SaResult res2 = SaResult.get(200, "ok", "zhangsan"); + Assertions.assertEquals((int)res2.getCode(), 200); + Assertions.assertEquals(res2.getMsg(), "ok"); + Assertions.assertEquals(res2.getData(), "zhangsan"); + // 序列化 + Assertions.assertEquals(res2.toString(), "{\"code\": 200, \"msg\": \"ok\", \"data\": \"zhangsan\"}"); + // data 为 int 时的序列化 + res2.setData(1); + Assertions.assertEquals(res2.toString(), "{\"code\": 200, \"msg\": \"ok\", \"data\": 1}"); + + // Map 构造 + Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", "value2"); + SaResult res4 = new SaResult(map); + Assertions.assertEquals(res4.get("key1"), "value1"); + Assertions.assertEquals(res4.get("key2"), "value2"); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/StartUpApplication.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/StartUpApplication.java new file mode 100644 index 000000000..c8f22cbf0 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/StartUpApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 启动类 + * @author Auster + * + */ +@SpringBootApplication +public class StartUpApplication { + public static void main(String[] args) { + SpringApplication.run(StartUpApplication.class, args); + } +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationController.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationController.java new file mode 100644 index 000000000..04666a6f5 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationController.java @@ -0,0 +1,104 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.annotation; + +import cn.dev33.satoken.annotation.*; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试注解用的Controller + * + * @author click33 + * @since 2022-9-2 + */ +@RestController +@RequestMapping("/at/") +public class SaAnnotationController { + + // 登录 + @RequestMapping("login") + public SaResult login(long id) { + StpUtil.login(id); + return SaResult.ok().set("token", StpUtil.getTokenValue()); + } + + // 登录校验 + @SaCheckLogin + @RequestMapping("checkLogin") + public SaResult checkLogin() { + return SaResult.ok(); + } + + // 角色校验 + @SaCheckRole("admin") + @RequestMapping("checkRole") + public SaResult checkRole() { + return SaResult.ok(); + } + + // 权限校验 + @SaCheckPermission("art-add") + @RequestMapping("checkPermission") + public SaResult checkPermission() { + return SaResult.ok(); + } + + // 权限校验 or 角色校验 + @SaCheckPermission(value = "art-add2", orRole = "admin") + @RequestMapping("checkPermission2") + public SaResult checkPermission2() { + return SaResult.ok(); + } + + // 开启二级认证 + @RequestMapping("openSafe") + public SaResult openSafe() { + StpUtil.openSafe(120); + return SaResult.ok(); + } + + // 二级认证校验 + @SaCheckSafe + @RequestMapping("checkSafe") + public SaResult checkSafe() { + return SaResult.ok(); + } + + // 封禁账号 + @RequestMapping("disable") + public SaResult disable(long id) { + StpUtil.disable(id, "comment", 200); + return SaResult.ok(); + } + + // 服务封禁校验 + @SaCheckDisable("comment") + @RequestMapping("checkDisable") + public SaResult checkDisable() { + return SaResult.ok(); + } + + // 解封账号 + @RequestMapping("untieDisable") + public SaResult untieDisable(long id) { + StpUtil.untieDisable(id, "comment"); + return SaResult.ok(); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationControllerTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationControllerTest.java new file mode 100644 index 000000000..a8eade997 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationControllerTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.annotation; + +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.integrate.StartUpApplication; +import cn.dev33.satoken.util.SaResult; + +/** + * 注解鉴权测试 + * + * @author Auster + * + */ +@SpringBootTest(classes = StartUpApplication.class) +public class SaAnnotationControllerTest { + + @Autowired + private WebApplicationContext wac; + + private MockMvc mvc; + + // 每个方法前执行 + @BeforeEach + public void before() { + mvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + // 校验通过的情况 + @Test + public void testPassing() { + // 登录拿到Token + SaResult res = request("/at/login?id=10001"); + String satoken = res.get("token", String.class); + Assertions.assertNotNull(satoken); + + // 登录校验,通过 + SaResult res2 = request("/at/checkLogin?satoken=" + satoken); + Assertions.assertEquals(res2.getCode(), 200); + + // 角色校验,通过 + SaResult res3 = request("/at/checkRole?satoken=" + satoken); + Assertions.assertEquals(res3.getCode(), 200); + + // 权限校验,通过 + SaResult res4 = request("/at/checkPermission?satoken=" + satoken); + Assertions.assertEquals(res4.getCode(), 200); + + // 权限校验or角色校验,通过 + SaResult res5 = request("/at/checkPermission2?satoken=" + satoken); + Assertions.assertEquals(res5.getCode(), 200); + + // 开启二级认证 + SaResult res6 = request("/at/openSafe?satoken=" + satoken); + Assertions.assertEquals(res6.getCode(), 200); + + // 校验二级认证,通过 + SaResult res7 = request("/at/checkSafe?satoken=" + satoken); + Assertions.assertEquals(res7.getCode(), 200); + + // 访问校验封禁的接口 ,通过 + SaResult res9 = request("/at/checkDisable?satoken=" + satoken); + Assertions.assertEquals(res9.getCode(), 200); + } + + // 校验不通过的情况 + @Test + public void testNotPassing() { + // 登录拿到Token + SaResult res = request("/at/login?id=10002"); + String satoken = res.get("token", String.class); + Assertions.assertNotNull(satoken); + + // 登录校验,不通过 + SaResult res2 = request("/at/checkLogin"); + Assertions.assertEquals(res2.getCode(), 401); + + // 角色校验,不通过 + SaResult res3 = request("/at/checkRole?satoken=" + satoken); + Assertions.assertEquals(res3.getCode(), 402); + + // 权限校验,不通过 + SaResult res4 = request("/at/checkPermission?satoken=" + satoken); + Assertions.assertEquals(res4.getCode(), 403); + + // 权限校验or角色校验,不通过 + SaResult res5 = request("/at/checkPermission2?satoken=" + satoken); + Assertions.assertEquals(res5.getCode(), 403); + + // 校验二级认证,不通过 + SaResult res7 = request("/at/checkSafe?satoken=" + satoken); + Assertions.assertEquals(res7.getCode(), 901); + + // -------- 登录拿到Token + String satoken10042 = request("/at/login?id=10042").get("token", String.class); + Assertions.assertNotNull(satoken10042); + + // 校验账号封禁 ,通过 + SaResult res8 = request("/at/disable?id=10042"); + Assertions.assertEquals(res8.getCode(), 200); + + // 访问校验封禁的接口 ,不通过 + SaResult res9 = request("/at/checkDisable?satoken=" + satoken10042); + Assertions.assertEquals(res9.getCode(), 904); + + // 解封后就能访问了 + request("/at/untieDisable?id=10042"); + SaResult res10 = request("/at/checkDisable?satoken=" + satoken10042); + Assertions.assertEquals(res10.getCode(), 200); + } + + // 测试忽略认证 + @Test + public void testIgnore() { + // 必须登录才能访问的 + SaResult res1 = request("/ig/show1"); + Assertions.assertEquals(res1.getCode(), 401); + + // 不登录也可以访问的 + SaResult res2 = request("/ig/show2"); + Assertions.assertEquals(res2.getCode(), 200); + } + + // 封装请求 + private SaResult request(String path) { + try { + // 发请求 + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post(path) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + // 转 Map + String content = mvcResult.getResponse().getContentAsString(); + Map map = SaManager.getSaJsonTemplate().jsonToMap(content); + + // 转 SaResult 对象 + return new SaResult().setMap(map); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationIgnoreController.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationIgnoreController.java new file mode 100644 index 000000000..6c4d2b016 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationIgnoreController.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.annotation; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.util.SaResult; + +/** + * 测试注解用的Controller + * + * @author click33 + * @since 2022-9-2 + */ +@SaCheckLogin +@RestController +@RequestMapping("/ig/") +public class SaAnnotationIgnoreController { + + // 需要登录后访问 + @RequestMapping("show1") + public SaResult show1() { + return SaResult.ok(); + } + + // 不登录也可访问 + @SaIgnore + @RequestMapping("show2") + public SaResult show2() { + return SaResult.ok(); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/HandlerException.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/HandlerException.java new file mode 100644 index 000000000..e69832732 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/HandlerException.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure; + +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.NotHttpBasicAuthException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import cn.dev33.satoken.exception.NotSafeException; +import cn.dev33.satoken.exception.SameTokenInvalidException; +import cn.dev33.satoken.util.SaResult; + +/** + * 全局异常处理 + * @author click33 + * + */ +@RestControllerAdvice +public class HandlerException { + + // 未登录异常,code=401 + @ExceptionHandler(NotLoginException.class) + public SaResult handlerNotLoginException(NotLoginException e) { + return SaResult.error().setCode(401); + } + + // 缺少角色异常,code=402 + @ExceptionHandler(NotRoleException.class) + public SaResult handlerNotRoleException(NotRoleException e) { + return SaResult.error().setCode(402); + } + + // 缺少权限异常,code=403 + @ExceptionHandler(NotPermissionException.class) + public SaResult handlerNotPermissionException(NotPermissionException e) { + return SaResult.error().setCode(403); + } + + // 二级认证失败,code=901 + @ExceptionHandler(NotSafeException.class) + public SaResult handlerNotSafeException(NotSafeException e) { + return SaResult.error().setCode(901); + } + + // same-token 校验失败,code=902 + @ExceptionHandler(SameTokenInvalidException.class) + public SaResult handlerSameTokenInvalidException(SameTokenInvalidException e) { + return SaResult.error().setCode(902); + } + + // Http Basic 校验失败,code=903 + @ExceptionHandler(NotHttpBasicAuthException.class) + public SaResult handlerNotBasicAuthException(NotHttpBasicAuthException e) { + return SaResult.error().setCode(903); + } + + // 服务被封禁 ,code=904 + @ExceptionHandler(DisableServiceException.class) + public SaResult handlerDisableServiceException(DisableServiceException e) { + return SaResult.error().setCode(904); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/SaTokenConfigure.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/SaTokenConfigure.java new file mode 100644 index 000000000..8299f7814 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/SaTokenConfigure.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure; + +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import cn.dev33.satoken.interceptor.SaInterceptor; + +/** + * Sa-Token 相关配置类 + * + * @author click33 + * @since 2022-9-2 + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + + // 注册 Sa-Token 拦截器,打开注解式鉴权功能 + @Override + public void addInterceptors(InterceptorRegistry registry) { + + // 测试环境下上下文过滤器不生效,所以此处从拦截器需要补充上下文 + registry.addInterceptor(new SaInterceptor(handle -> { + SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); + }).isAnnotation(false)).addPathPatterns("/**"); + + // 注册 Sa-Token 拦截器,打开注解式鉴权功能 + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); + } + +} + diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/StpInterfaceImpl.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/StpInterfaceImpl.java new file mode 100644 index 000000000..b7b9bad5d --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/StpInterfaceImpl.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 自定义权限验证接口扩展 + * + * @author click33 + * + */ +@Component +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + int id = SaFoxUtil.getValueByType(loginId, int.class); + if(id == 10001) { + return Arrays.asList("user*", "art-add", "art-delete", "art-update", "art-get"); + } else { + return null; + } + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + int id = SaFoxUtil.getValueByType(loginId, int.class); + if(id == 10001) { + return Arrays.asList("admin", "super-admin"); + } else { + return null; + } + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaBasicTemplate.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaBasicTemplate.java new file mode 100644 index 000000000..9192f6169 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaBasicTemplate.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; + +@Component +public class MySaBasicTemplate extends SaHttpBasicTemplate { + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaOAuth2Template.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaOAuth2Template.java new file mode 100644 index 000000000..1d5189958 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaOAuth2Template.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.oauth2.template.SaOAuth2Template; + +/** + * 自定义 Sa-OAuth2 模板方法 + * + * @author click33 + * @since 2022-9-5 + */ +@Component +public class MySaOAuth2Template extends SaOAuth2Template { + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSameTemplate.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSameTemplate.java new file mode 100644 index 000000000..c25991d07 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSameTemplate.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.same.SaSameTemplate; + +@Component +public class MySaSameTemplate extends SaSameTemplate { + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSignTemplate.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSignTemplate.java new file mode 100644 index 000000000..b8a96afa6 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSignTemplate.java @@ -0,0 +1,24 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import cn.dev33.satoken.sign.template.SaSignTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MySaSignTemplate extends SaSignTemplate { + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSsoTemplate.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSsoTemplate.java new file mode 100644 index 000000000..392e5ce42 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSsoTemplate.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.sso.template.SaSsoTemplate; + +/** + * 自定义 Sa-SSO 模板方法 + * + * @author click33 + * @since 2022-9-5 + */ +@Component +public class MySaSsoTemplate extends SaSsoTemplate { + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTempTemplate.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTempTemplate.java new file mode 100644 index 000000000..50d14a6db --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTempTemplate.java @@ -0,0 +1,24 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import cn.dev33.satoken.temp.SaTempTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MySaTempTemplate extends SaTempTemplate { + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenDao.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenDao.java new file mode 100644 index 000000000..a2dc15a8d --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenDao.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; + +@Component +public class MySaTokenDao extends SaTokenDaoDefaultImpl { + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenListener.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenListener.java new file mode 100644 index 000000000..7f8d653e4 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenListener.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.listener.SaTokenListenerForSimple; + +@Component +public class MySaTokenListener extends SaTokenListenerForSimple { + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MyStpLogic.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MyStpLogic.java new file mode 100644 index 000000000..0f84cd97e --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MyStpLogic.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.configure.inject; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; + +@Component +public class MyStpLogic extends StpLogic { + + public MyStpLogic() { + super(StpUtil.TYPE); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginController.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginController.java new file mode 100644 index 000000000..8c37f7231 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginController.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.login; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * + * @author click33 + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.data(StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginControllerTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginControllerTest.java new file mode 100644 index 000000000..408859a67 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginControllerTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.login; + +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import cn.dev33.satoken.integrate.StartUpApplication; +import cn.dev33.satoken.util.SoMap; + +/** + * Sa-Token 登录API测试 + * + * @author click33 + * + */ +@SpringBootTest(classes = StartUpApplication.class) +@SuppressWarnings("deprecation") +public class LoginControllerTest { + + @Autowired + private WebApplicationContext wac; + + private MockMvc mvc; + + // 开始 + @BeforeEach + public void before() { + mvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testLogin() throws Exception{ + // 请求 + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post("/acc/doLogin") + .param("name", "zhang") + .param("pwd", "123456") + .contentType(MediaType.APPLICATION_JSON_UTF8) + .accept(MediaType.APPLICATION_JSON_UTF8) + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + // 拿到结果 + SoMap so = SoMap.getSoMap().setJsonString( + mvcResult.getResponse().getContentAsString() + ); + String token = so.getString("token"); + + // 断言 + Assertions.assertTrue(mvcResult.getResponse().getHeader("Set-Cookie") != null); + Assertions.assertEquals(so.getInt("code"), 200); + Assertions.assertNotNull(token); + } + + @Test + @SuppressWarnings("unchecked") + public void testLogin2() throws Exception{ + // 获取token + SoMap so = request("/acc/doLogin?name=zhang&pwd=123456"); + Assertions.assertNotNull(so.getString("token")); + + String token = so.getString("token"); + + // 是否登录 + SoMap so2 = request("/acc/isLogin?satoken=" + token); + Assertions.assertTrue(so2.getBoolean("data")); + + // tokenInfo + SoMap so3 = request("/acc/tokenInfo?satoken=" + token); + SoMap so4 = SoMap.getSoMap((Map)so3.get("data")); + Assertions.assertEquals(so4.getString("tokenName"), "satoken"); + Assertions.assertEquals(so4.getString("tokenValue"), token); + + // 注销 + request("/acc/logout?satoken=" + token); + + // 是否登录 + SoMap so5 = request("/acc/isLogin?satoken=" + token); + Assertions.assertFalse(so5.getBoolean("data")); + } + + // 封装请求 + private SoMap request(String path) throws Exception { + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post(path) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + SoMap so = SoMap.getSoMap().setJsonString( + mvcResult.getResponse().getContentAsString() + ); + + return so; + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreController.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreController.java new file mode 100644 index 000000000..fd5b9e160 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreController.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.more; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.util.SaFoxUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 其它测试 + * + * @author click33 + * + */ +@RestController +@RequestMapping("/more/") +public class MoreController { + + // 一些基本的测试 + @RequestMapping("getInfo") + public SaResult getInfo() { + SaRequest req = SaHolder.getRequest(); + boolean flag = + SaFoxUtil.equals(req.getParam("name"), "zhang") + && SaFoxUtil.equals(req.getParam("name2", "li"), "li") + && SaFoxUtil.equals(req.getParamNotNull("name"), "zhang") + && req.isParam("name", "zhang") + && req.isPath("/more/getInfo") + && req.hasParam("name") + && SaFoxUtil.equals(req.getHeader("div"), "val") + && SaFoxUtil.equals(req.getHeader("div", "zhang"), "val") + && SaFoxUtil.equals(req.getHeader("div2", "zhang"), "zhang") + ; + + SaHolder.getResponse().setServer("sa-server"); + return SaResult.data(flag); + } + + // Http Basic 认证 + @RequestMapping("basicAuth") + public SaResult basicAuth() { + SaHttpBasicUtil.check("sa:123456"); + return SaResult.ok(); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreControllerTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreControllerTest.java new file mode 100644 index 000000000..605989ad5 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreControllerTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.more; + +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.integrate.StartUpApplication; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.spring.SaTokenContextForSpring; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.util.SaResult; + + +/** + * 其它测试 + * + * @author click33 + * + */ +@SpringBootTest(classes = StartUpApplication.class) +public class MoreControllerTest { + + @Autowired + private WebApplicationContext wac; + + private MockMvc mvc; + + // 开始 + @BeforeEach + public void before() { + mvc = MockMvcBuilders.webAppContextSetup(wac).build(); + + // 在单元测试时,通过 request.getServletPath() 获取到的请求路径为空,导致路由拦截不正确 + // 虽然不知道为什么会这样,但是暂时可以通过以下方式来解决 + SaManager.setSaTokenContext(new SaTokenContextForSpring() { + @Override + public SaRequest getRequest() { + return new SaRequestForServlet(SpringMVCUtil.getRequest()) { + @Override + public String getRequestPath() { + return request.getRequestURI(); + } + }; + } + }); + + } + + // 基础API测试 + @Test + public void testApi() { + SaResult res = request("/more/getInfo?name=zhang"); + Assertions.assertEquals(res.getData(), true); + } + + // Http Basic 认证 + @Test + public void testBasic() throws Exception { + + // ---------------- 认证不通过 + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post("/more/basicAuth") + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + ) + .andExpect(MockMvcResultMatchers.status().is(401)) + .andReturn(); + + // 转 Map + String content = mvcResult.getResponse().getContentAsString(); + Map map = SaManager.getSaJsonTemplate().jsonToMap(content); + // 转 SaResult 对象 + SaResult res = new SaResult().setMap(map); + Assertions.assertEquals(res.getCode(), 903); + // 会有一个特殊响应头 + String header = mvcResult.getResponse().getHeader("WWW-Authenticate"); + Assertions.assertEquals(header, "Basic Realm=Sa-Token"); + + + // ---------------- 认证通过 + MvcResult mvcResult2 = mvc.perform( + MockMvcRequestBuilders.post("/more/basicAuth") + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + .header("Authorization", "Basic c2E6MTIzNDU2") + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + // 转 Map + String content2 = mvcResult2.getResponse().getContentAsString(); + Map map2 = SaManager.getSaJsonTemplate().jsonToMap(content2); + // 转 SaResult 对象 + SaResult res2 = new SaResult().setMap(map2); + Assertions.assertEquals(res2.getCode(), 200); + } + + + // 封装请求 + private SaResult request(String path) { + try { + // 发请求 + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post(path) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + .header("div", "val") + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + // 转 Map + String content = mvcResult.getResponse().getContentAsString(); + Map map = SaManager.getSaJsonTemplate().jsonToMap(content); + + // 转 SaResult 对象 + return new SaResult().setMap(map); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterController.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterController.java new file mode 100644 index 000000000..98f364d32 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterController.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.router; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 路由鉴权测试 + * + * @author click33 + * + */ +@RestController +@RequestMapping("/rt/") +public class RouterController { + + @RequestMapping("getInfo") + public SaResult getInfo() { + return SaResult.ok(); + } + + @RequestMapping("getInfo*") + public SaResult getInfo2() { + return SaResult.ok(); + } + + // 读url + @RequestMapping("getInfo_101") + public SaResult getInfo_101() { + return SaResult.data(SaHolder.getRequest().getUrl()); + } + + // 读Cookie + @RequestMapping("getInfo_102") + public SaResult getInfo_102() { + return SaResult.data(SaHolder.getRequest().getCookieValue("x-token")); + } + + // 测试转发 + @RequestMapping("getInfo_103") + public SaResult getInfo_103() { + SaHolder.getRequest().forward("/rt/getInfo_102"); + return SaResult.ok(); + } + + // 空接口 + @RequestMapping("getInfo_200") + public SaResult getInfo_200() { + return SaResult.ok(); + } + @RequestMapping("getInfo_201") + public SaResult getInfo_201() { + return SaResult.ok(); + } + @RequestMapping("getInfo_202") + public SaResult getInfo_202() { + return SaResult.ok(); + } + @RequestMapping("login") + public SaResult login(long id) { + StpUtil.login(id); + return SaResult.ok().set("token", StpUtil.getTokenValue()); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterControllerTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterControllerTest.java new file mode 100644 index 000000000..46b3a0b0c --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterControllerTest.java @@ -0,0 +1,274 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.router; + +import java.util.Arrays; +import java.util.Map; + +import javax.servlet.http.Cookie; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.integrate.StartUpApplication; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.router.SaRouterStaff; +import cn.dev33.satoken.servlet.model.SaRequestForServlet; +import cn.dev33.satoken.spring.SaTokenContextForSpring; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.util.SaResult; + + +/** + * C Controller 测试 + * + * @author click33 + * + */ +@SpringBootTest(classes = StartUpApplication.class) +public class RouterControllerTest { + + @Autowired + private WebApplicationContext wac; + + private MockMvc mvc; + + // 开始 + @BeforeEach + public void before() { + mvc = MockMvcBuilders.webAppContextSetup(wac).build(); + + // 在单元测试时,通过 request.getServletPath() 获取到的请求路径为空,导致路由拦截不正确 + // 虽然不知道为什么会这样,但是暂时可以通过以下方式来解决 + SaManager.setSaTokenContext(new SaTokenContextForSpring() { + @Override + public SaRequest getRequest() { + return new SaRequestForServlet(SpringMVCUtil.getRequest()) { + @Override + public String getRequestPath() { + return request.getRequestURI(); + } + }; + } + }); + + } + + // 基础API测试 + @Test + public void testApi() { + // 是否命中 + SaRouterStaff staff = SaRouter.match(false); + Assertions.assertFalse(staff.isHit()); + + // 重置 + staff.reset(); + Assertions.assertTrue(staff.isHit()); + + // lambda 形式 + SaRouterStaff staff2 = SaRouter.match(r -> false); + Assertions.assertFalse(staff2.isHit()); + + // 匹配 + Assertions.assertTrue(SaRouter.isMatch("/user/**", "/user/add")); + Assertions.assertTrue(SaRouter.isMatch(new String[] {"/user/**", "/art/**", "/goods/**"}, "/art/delete")); + Assertions.assertTrue(SaRouter.isMatch(Arrays.asList("/user/**", "/art/**", "/goods/**"), "/art/delete")); + Assertions.assertTrue(SaRouter.isMatch(new String[] {"POST", "GET", "PUT"}, "GET")); + + // 不匹配的 + Assertions.assertTrue(SaRouter.notMatch(false).isHit()); + Assertions.assertTrue(SaRouter.notMatch(r -> false).isHit()); + } + + // 各种路由测试 + @Test + public void testRouter() { + // getInfo + SaResult res = request("/rt/getInfo?name=zhang"); + Assertions.assertEquals(res.getCode(), 201); + + // getInfo2 + SaResult res2 = request("/rt/getInfo2"); + Assertions.assertEquals(res2.getCode(), 202); + + // getInfo3 + SaResult res3 = request("/rt/getInfo3"); + Assertions.assertEquals(res3.getCode(), 203); + + // getInfo4 + SaResult res4 = request("/rt/getInfo4"); + Assertions.assertEquals(res4.getCode(), 204); + + // getInfo5 + SaResult res5 = request("/rt/getInfo5"); + Assertions.assertEquals(res5.getCode(), 205); + + // getInfo6 + SaResult res6 = request("/rt/getInfo6"); + Assertions.assertEquals(res6.getCode(), 206); + + // getInfo7 + SaResult res7 = request("/rt/getInfo7"); + Assertions.assertEquals(res7.getCode(), 200); + + // getInfo8 + SaResult res8 = request("/rt/getInfo8"); + Assertions.assertEquals(res8.getCode(), 200); + + // getInfo9 + SaResult res9 = request("/rt/getInfo9"); + Assertions.assertEquals(res9.getCode(), 209); + + // getInfo10 + SaResult res10 = request("/rt/getInfo10"); + Assertions.assertEquals(res10.getCode(), 200); + + // getInfo11 + SaResult res11 = request("/rt/getInfo11"); + Assertions.assertEquals(res11.getCode(), 211); + + // getInfo12 + SaResult res12 = request("/rt/getInfo12"); + Assertions.assertEquals(res12.getCode(), 212); + + // getInfo13 + SaResult res13 = request("/rt/getInfo13"); + Assertions.assertEquals(res13.getCode(), 213); + + // getInfo14 + SaResult res14 = request("/rt/getInfo14"); + Assertions.assertEquals(res14.getCode(), 214); + + // getInfo15 + SaResult res15 = request("/rt/getInfo15"); + Assertions.assertEquals(res15.getCode(), 215); + + } + + // 测试 getUrl() + @Test + public void testGetUrl() { + // getInfo_101 + SaResult res = request("/rt/getInfo_101"); + Assertions.assertTrue(res.getData().toString().endsWith("/rt/getInfo_101")); + + // getInfo_101,不包括后面的参数 + SaResult res2 = request("/rt/getInfo_101?id=1"); + Assertions.assertTrue(res2.getData().toString().endsWith("/rt/getInfo_101")); + + // 自定义当前域名 + SaManager.getConfig().setCurrDomain("http://xxx.com"); + SaResult res3 = request("/rt/getInfo_101?id=1"); + Assertions.assertEquals(res3.getData().toString(), "http://xxx.com/rt/getInfo_101"); + SaManager.getConfig().setCurrDomain(null); + } + + // 测试读取Cookie + @Test + public void testGetCookie() throws Exception { + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post("/rt/getInfo_102") + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + .cookie(new Cookie("x-token", "token-111")) + ) + .andExpect(MockMvcResultMatchers.status().is(200)) + .andReturn(); + + // 转 Map + String content = mvcResult.getResponse().getContentAsString(); + Map map = SaManager.getSaJsonTemplate().jsonToMap(content); + + // 转 SaResult 对象 + SaResult res = new SaResult().setMap(map); + Assertions.assertEquals(res.getData(), "token-111"); + } + + // 测试重定向 + @Test + public void testRedirect() throws Exception { + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post("/rt/getInfo16") + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + ) + .andExpect(MockMvcResultMatchers.status().is(302)) + .andReturn(); + + Assertions.assertEquals(mvcResult.getResponse().getHeader("Location"), "/rt/getInfo3"); + } + + // 空接口 + @Test + public void testGetInfo200() { +// SaResult res = request("/rt/getInfo_200"); +// Assertions.assertEquals(res.getCode(), 200); +// SaResult res1 = request("/rt/getInfo_201"); +// Assertions.assertEquals(res1.getCode(), 201); +// SaResult res2 = request("/rt/getInfo_202"); +// Assertions.assertEquals(res2.getCode(), 401); + + // 登录拿到Token + SaResult resLogin = request("/rt/login?id=10001"); + String satoken = resLogin.get("token", String.class); + SaResult res3 = request("/rt/getInfo_202?satoken=" + satoken); + Assertions.assertEquals(res3.getCode(), 200); + } + + // 测试转发 + @Test + public void testForward() { + SaResult res = request("/rt/getInfo_103"); + Assertions.assertEquals(res.getCode(), 200); + } + + // 封装请求 + private SaResult request(String path) { + try { + // 发请求 + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post(path) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + // 转 Map + String content = mvcResult.getResponse().getContentAsString(); + Map map = SaManager.getSaJsonTemplate().jsonToMap(content); + + // 转 SaResult 对象 + return new SaResult().setMap(map); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/SaTokenConfigure2.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/SaTokenConfigure2.java new file mode 100644 index 000000000..f19feb133 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/SaTokenConfigure2.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.router; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaHttpMethod; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.util.SaResult; +import org.junit.jupiter.api.Assertions; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; + +/** + * Sa-Token 相关配置类 + * + * @author click33 + * @since 2022-9-2 + */ +@Configuration +public class SaTokenConfigure2 implements WebMvcConfigurer { + + // 路由鉴权 + @Override + public void addInterceptors(InterceptorRegistry registry) { + + // 测试环境下上下文过滤器不生效,所以此处从拦截器需要补充上下文 + registry.addInterceptor(new SaInterceptor(handle -> { + SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); + }).isAnnotation(false)).addPathPatterns("/**"); + + // 路由鉴权 + registry.addInterceptor(new SaInterceptor(handle -> {}) + .isAnnotation(true) + .setAuth(handle -> { + + // 匹配 getInfo ,返回code=201 + SaRouter.match("/**") + .match(SaHttpMethod.POST) + .matchMethod("POST") + .match(SaHolder.getRequest().getMethod().equals("POST")) + .match(r -> SaHolder.getRequest().isPath("/rt/getInfo")) + .match(r -> SaHolder.getRequest().isParam("name", "zhang")) + .back(SaResult.code(201)); + + // 匹配 getInfo2 ,返回code=202 + SaRouter.match("/rt/getInfo2") + .match(Arrays.asList("/rt/getInfo2", "/rt/*")) + .notMatch("/rt/getInfo3") + .notMatch(false) + .notMatch(r -> false) + .notMatch(SaHttpMethod.GET) + .notMatchMethod("PUT") + .notMatch(Arrays.asList("/rt/getInfo4", "/rt/getInfo5")) + .back(SaResult.code(202)); + + // 匹配 getInfo3 ,返回code=203 + SaRouter.match("/rt/getInfo3", "/rt/getInfo4", () -> SaRouter.back(SaResult.code(203))); + SaRouter.match("/rt/getInfo4", "/rt/getInfo5", r -> SaRouter.back(SaResult.code(204))); + SaRouter.match("/rt/getInfo5", () -> SaRouter.back(SaResult.code(205))); + SaRouter.match("/rt/getInfo6", r -> SaRouter.back(SaResult.code(206))); + + // 通往 Controller + SaRouter.match(Arrays.asList("/rt/getInfo7")).stop(); + + // 通往 Controller + SaRouter.match("/rt/getInfo8", () -> SaRouter.stop()); + + SaRouter.matchMethod("POST").match("/rt/getInfo9").free(r -> SaRouter.back(SaResult.code(209))); + SaRouter.match(SaHttpMethod.POST).match("/rt/getInfo10").setHit(false).back(); + + // 11 + SaRouter.notMatch("/rt/getInfo11").reset().match("/rt/getInfo11").back(SaResult.code(211)); + SaRouter.notMatch(SaHttpMethod.GET).match("/rt/getInfo12").back(SaResult.code(212)); + SaRouter.notMatch(Arrays.asList("/rt/getInfo12", "/rt/getInfo14")).match("/rt/getInfo13").back(SaResult.code(213)); + SaRouter.notMatchMethod("GET", "PUT").match("/rt/getInfo14").back(SaResult.code(214)); + +// SaRouter.match(Arrays.asList("/rt/getInfo15", "/rt/getInfo16")) + if(SaRouter.isMatchCurrURI("/rt/getInfo15")) { + if(SaHolder.getRequest().getCookieValue("ddd") == null + && SaHolder.getStorage().getSource() == SpringMVCUtil.getRequest() + && SaHolder.getRequest().getSource() == SpringMVCUtil.getRequest() + && SaHolder.getResponse().getSource() == SpringMVCUtil.getResponse() + ) { + SaRouter.newMatch().free(r -> SaRouter.back(SaResult.code(215))); + } + } + + SaRouter.match("/rt/getInfo16", () -> { + Assertions.assertThrows(Exception.class, () -> SaHolder.getResponse().redirect(null)); + SaHolder.getResponse().redirect("/rt/getInfo3"); + }); + + })).addPathPatterns("/**"); + } + +} + diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenController.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenController.java new file mode 100644 index 000000000..86466ac54 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenController.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.same; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * same-token Controller + * + * @author click33 + * + */ +@RestController +@RequestMapping("/same/") +public class SaSameTokenController { + + // 获取信息 + @RequestMapping("getInfo") + public SaResult getInfo() { + // 获取并校验same-token + String sameToken = SpringMVCUtil.getRequest().getHeader(SaSameUtil.SAME_TOKEN); + SaSameUtil.checkToken(sameToken); + // 返回信息 + return SaResult.data("info=zhangsan"); + } + + // 获取信息2 + @RequestMapping("getInfo2") + public SaResult getInfo2() { + // 获取并校验same-token + SaSameUtil.checkCurrentRequestToken(); + // 返回信息 + return SaResult.data("info=zhangsan2"); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenControllerTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenControllerTest.java new file mode 100644 index 000000000..0a48f51dc --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenControllerTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.integrate.same; + +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.exception.SameTokenInvalidException; +import cn.dev33.satoken.integrate.StartUpApplication; +import cn.dev33.satoken.same.SaSameUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * same-token Controller 测试 + * + * @author click33 + * + */ +@SpringBootTest(classes = StartUpApplication.class) +public class SaSameTokenControllerTest { + + @Autowired + private WebApplicationContext wac; + + private MockMvc mvc; + + // 开始 + @BeforeEach + public void before() { + mvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + // 获取信息 + @Test + public void testGetInfo() { + String token = SaSameUtil.getToken(); + // 加token,能调通 + SaResult res = request("/same/getInfo", token); + Assertions.assertEquals(res.getCode(), 200); + // 不加token,不能调通 + SaResult res2 = request("/same/getInfo", "xxx"); + Assertions.assertEquals(res2.getCode(), 902); + + // 获取信息2 + token = SaSameUtil.getTokenNh(); + // 加token,能调通 + SaResult res3 = request("/same/getInfo2", token); + Assertions.assertEquals(res3.getCode(), 200); + // 不加token,不能调通 + SaResult res4 = request("/same/getInfo2", "xxx"); + Assertions.assertEquals(res4.getCode(), 902); + } + + // 基础测试 + @Test + public void testApi() { + String token = SaSameUtil.getToken(); + + // 刷新一下,会有变化 + SaSameUtil.refreshToken(); + String token2 = SaSameUtil.getToken(); + Assertions.assertNotEquals(token, token2); + + // 旧token,变为次级token + String pastToken = SaSameUtil.getPastTokenNh(); + Assertions.assertEquals(token, pastToken); + + // dao中应该有值 + String daoToken = SaManager.getSaTokenDao().get("satoken:var:same-token"); + String daoToken2 = SaManager.getSaTokenDao().get("satoken:var:past-same-token"); + Assertions.assertEquals(token2, daoToken); + Assertions.assertEquals(token, daoToken2); + + // 新旧都有效 + Assertions.assertTrue(SaSameUtil.isValid(token)); + Assertions.assertTrue(SaSameUtil.isValid(token2)); + + // 空的不行 + Assertions.assertFalse(SaSameUtil.isValid(null)); + Assertions.assertFalse(SaSameUtil.isValid("")); + + // 不抛出异常 + Assertions.assertDoesNotThrow(() -> SaSameUtil.checkToken(token)); + Assertions.assertDoesNotThrow(() -> SaSameUtil.checkToken(token2)); + + // 抛出异常 + Assertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken(null)); + Assertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken("")); + Assertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken("aaa")); + } + + + + // 封装请求 + private SaResult request(String path, String sameToken) { + try { + // 发请求 + MvcResult mvcResult = mvc.perform( + MockMvcRequestBuilders.post(path) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .accept(MediaType.APPLICATION_PROBLEM_JSON) + .header(SaSameUtil.SAME_TOKEN, sameToken) + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + // 转 Map + String content = mvcResult.getResponse().getContentAsString(); + Map map = SaManager.getSaJsonTemplate().jsonToMap(content); + + // 转 SaResult 对象 + return new SaResult().setMap(map); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/BasicsTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/BasicsTest.java new file mode 100644 index 000000000..bfbf47ab7 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/BasicsTest.java @@ -0,0 +1,795 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.springboot; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.exception.*; +import cn.dev33.satoken.filter.SaServletFilter; +import cn.dev33.satoken.json.SaJsonTemplate; +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder; +import cn.dev33.satoken.stp.SaLoginConfig; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.util.SaTokenConsts; +import cn.dev33.satoken.util.SoMap; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.util.PathMatcher; + +import javax.servlet.ServletException; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Sa-Token 基础API测试 + * + *

    注解详解参考: https://www.cnblogs.com/flypig666/p/11505277.html + * @author Auster + * + */ +@SpringBootTest(classes = StartUpApplication.class) +public class BasicsTest { + + // 持久化Bean + @Autowired(required = false) + SaTokenDao dao = SaManager.getSaTokenDao(); + + @Autowired + PathMatcher pathMatcher; + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ 基础测试 start ..."); + SaManager.getConfig().setActiveTimeout(180); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ 基础测试 end ... \n"); + } + + @BeforeEach + public void beforeEach() { + SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); + } + + // 结束 + @AfterEach + public void afterEach() { + SaTokenContextServletUtil.clearContext(); + } + + // 测试:基础API + @Test + public void testBasicsApi() { + // 基本API + Assertions.assertEquals(StpUtil.getLoginType(), "login"); + Assertions.assertEquals(StpUtil.getStpLogic(), SaManager.getStpLogic("login")); + Assertions.assertEquals(StpUtil.getTokenName(), "satoken"); + + // 安全的更新 StpUtil 的 StpLogic 对象 + StpLogic loginStpLogic = new StpLogic("login"); + StpUtil.setStpLogic(loginStpLogic); + Assertions.assertEquals(StpUtil.getStpLogic(), loginStpLogic); + Assertions.assertEquals(SaManager.getStpLogic("login"), loginStpLogic); + } + + // 测试:登录 + @Test + public void testDoLogin() { + // 登录 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + + // token 存在 + Assertions.assertNotNull(token); + Assertions.assertEquals(token, StpUtil.getTokenValueNotCut()); + Assertions.assertEquals(token, StpUtil.getTokenValueByLoginId(10001)); + Assertions.assertEquals(token, StpUtil.getTokenValueByLoginId(10001, SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE)); + + // token 队列 + List tokenList = StpUtil.getTokenValueListByLoginId(10001); + List tokenList2 = StpUtil.getTokenValueListByLoginId(10001, SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); + Assertions.assertEquals(token, tokenList.get(tokenList.size() - 1)); + Assertions.assertEquals(token, tokenList2.get(tokenList.size() - 1)); + + // API 验证 + Assertions.assertTrue(StpUtil.isLogin()); + Assertions.assertDoesNotThrow(() -> StpUtil.checkLogin()); + Assertions.assertNotNull(token); // token不为null + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 + Assertions.assertEquals(StpUtil.getLoginIdAsInt(), 10001); // loginId=10001 + Assertions.assertEquals(StpUtil.getLoginIdAsString(), "10001"); // loginId=10001 + Assertions.assertEquals(StpUtil.getLoginId(), "10001"); // loginId=10001 + Assertions.assertEquals(StpUtil.getLoginIdDefaultNull(), "10001"); // loginId=10001 + Assertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 + + // db数据 验证 + // token存在 + Assertions.assertEquals(dao.get("satoken:login:token:" + token), "10001"); + // Session 存在 + SaSession session = dao.getSession("satoken:login:session:" + 10001); + Assertions.assertNotNull(session); + Assertions.assertEquals(session.getId(), "satoken:login:session:" + 10001); + Assertions.assertTrue(session.getTerminalList().size() >= 1); + } + + // 测试:注销 + @Test + public void testLogout() { + // 登录 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + Assertions.assertEquals(dao.get("satoken:login:token:" + token), "10001"); + + // 注销 + StpUtil.logout(); + // token 应该被清除 + Assertions.assertNull(StpUtil.getTokenValue()); + Assertions.assertFalse(StpUtil.isLogin()); + Assertions.assertNull(dao.get("satoken:login:token:" + token)); + + // 全部客户端注销掉 + StpUtil.logout(10001); + // Session 应该被清除 + SaSession session = dao.getSession("satoken:login:session:" + 10001); + Assertions.assertNull(session); + + // 在调用 getLoginId() 就会抛出异常 + Assertions.assertEquals(StpUtil.getLoginId("无值"), "无值"); + Assertions.assertThrows(NotLoginException.class, () -> StpUtil.getLoginId()); + } + + // 测试:Session会话 + @Test + public void testSession() { + StpUtil.login(10001); + + // API 应该可以获取 Session + Assertions.assertNotNull(StpUtil.getSession()); + Assertions.assertNotNull(StpUtil.getSession(false)); + + // db中应该存在 Session + SaSession session = dao.getSession("satoken:login:session:" + 10001); + Assertions.assertNotNull(session); + + // 存取值 + session.set("name", "zhang"); + session.set("age", "18"); + Assertions.assertEquals(session.get("name"), "zhang"); + Assertions.assertEquals(session.getInt("age"), 18); + Assertions.assertEquals((int)session.getModel("age", int.class), 18); + Assertions.assertEquals((int)session.get("age", 20), 18); + Assertions.assertEquals((int)session.get("name2", 20), 20); + Assertions.assertEquals((int)session.get("name2", () -> 30), 30); + session.clear(); + Assertions.assertEquals(session.get("name"), null); + } + + // 测试:权限认证 + @Test + public void testCheckPermission() { + StpUtil.login(10001); + + // 获取权限 + List permissionList = StpUtil.getPermissionList(); + List permissionList2 = StpUtil.getPermissionList(10001); + Assertions.assertEquals(permissionList.size(), permissionList2.size()); + + // 权限校验 + Assertions.assertTrue(StpUtil.hasPermission("user-add")); + Assertions.assertTrue(StpUtil.hasPermission("user-list")); + Assertions.assertTrue(StpUtil.hasPermission("user")); + Assertions.assertTrue(StpUtil.hasPermission("art-add")); + Assertions.assertFalse(StpUtil.hasPermission("get-user")); + // and + Assertions.assertTrue(StpUtil.hasPermissionAnd("art-add", "art-get")); + Assertions.assertFalse(StpUtil.hasPermissionAnd("art-add", "comment-add")); + // or + Assertions.assertTrue(StpUtil.hasPermissionOr("art-add", "comment-add")); + Assertions.assertFalse(StpUtil.hasPermissionOr("comment-add", "comment-delete")); + // more + Assertions.assertTrue(StpUtil.hasPermission(10001, "user-add")); + Assertions.assertFalse(StpUtil.hasPermission(10002, "user-add")); + + // 抛异常 + Assertions.assertThrows(NotPermissionException.class, () -> StpUtil.checkPermission("goods-add")); + Assertions.assertThrows(NotPermissionException.class, () -> StpUtil.checkPermissionAnd("goods-add", "art-add")); + // 不抛异常 + Assertions.assertDoesNotThrow(() -> StpUtil.checkPermission("user-add")); + Assertions.assertDoesNotThrow(() -> StpUtil.checkPermissionAnd("art-get", "art-add")); + Assertions.assertDoesNotThrow(() -> StpUtil.checkPermissionOr("goods-add", "art-add")); + } + + // 测试:角色认证 + @Test + public void testCheckRole() { + StpUtil.login(10001); + + // 获取角色 + List roleList = StpUtil.getRoleList(); + List roleList2 = StpUtil.getRoleList(10001); + Assertions.assertEquals(roleList.size(), roleList2.size()); + + // 角色校验 + Assertions.assertTrue(StpUtil.hasRole("admin")); + Assertions.assertFalse(StpUtil.hasRole("teacher")); + // and + Assertions.assertTrue(StpUtil.hasRoleAnd("admin", "super-admin")); + Assertions.assertFalse(StpUtil.hasRoleAnd("admin", "ceo")); + // or + Assertions.assertTrue(StpUtil.hasRoleOr("admin", "ceo")); + Assertions.assertFalse(StpUtil.hasRoleOr("ceo", "cto")); + // more + Assertions.assertTrue(StpUtil.hasRole(10001, "admin")); + Assertions.assertFalse(StpUtil.hasRole(10002, "admin2")); + + // 抛异常 + Assertions.assertThrows(NotRoleException.class, () -> StpUtil.checkRole("ceo")); + Assertions.assertThrows(NotRoleException.class, () -> StpUtil.checkRoleAnd("ceo", "admin")); + // 不抛异常 + Assertions.assertDoesNotThrow(() -> StpUtil.checkRole("admin")); + Assertions.assertDoesNotThrow(() -> StpUtil.checkRoleAnd("admin", "super-admin")); + Assertions.assertDoesNotThrow(() -> StpUtil.checkRoleOr("ceo", "admin")); + } + + // 测试:根据token强制注销 + @Test + public void testLogoutByToken() { + StpUtil.logout(10001); + + // 先登录上 + StpUtil.login(10001); + Assertions.assertTrue(StpUtil.isLogin()); + String token = StpUtil.getTokenValue(); + + // 根据token注销 + StpUtil.logoutByTokenValue(token); + Assertions.assertFalse(StpUtil.isLogin()); + + // token 应该被清除 + Assertions.assertNull(dao.get("satoken:login:token:" + token)); + // Session 应该被清除 + SaSession session = dao.getSession("satoken:login:session:" + 10001); + Assertions.assertNull(session); + + // 场景值应该是token无效 + try { + StpUtil.checkLogin(); + } catch (NotLoginException e) { + Assertions.assertEquals(e.getType(), NotLoginException.INVALID_TOKEN); + } + + // 根据token踢下线 + StpUtil.login(10001); + StpUtil.kickoutByTokenValue(StpUtil.getTokenValue()); + + // 场景值应该是被踢下线 + try { + StpUtil.checkLogin(); + } catch (NotLoginException e) { + Assertions.assertEquals(e.getType(), NotLoginException.KICK_OUT); + } + } + + // 测试:根据账号id强制注销 + @Test + public void testLogoutByLoginId() { + + // 先登录上 + StpUtil.login(10001); + Assertions.assertTrue(StpUtil.isLogin()); + String token = StpUtil.getTokenValue(); + + // 根据账号id注销 + StpUtil.logout(10001); + Assertions.assertFalse(StpUtil.isLogin()); + + // token 应该被清除 + Assertions.assertNull(dao.get("satoken:login:token:" + token)); + // Session 应该被清除 + SaSession session = dao.getSession("satoken:login:session:" + 10001); + Assertions.assertNull(session); + + // 场景值应该是token无效 + try { + StpUtil.checkLogin(); + } catch (NotLoginException e) { + Assertions.assertEquals(e.getType(), NotLoginException.INVALID_TOKEN); + } + } + + // 测试Token-Session + @Test + public void testTokenSession() { + + // 先登录上 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + + // 刚开始不存在 + Assertions.assertNull(StpUtil.stpLogic.getTokenSession(false)); + SaSession session = dao.getSession("satoken:login:token-session:" + token); + Assertions.assertNull(session); + + // 调用一次就存在了 + StpUtil.getTokenSession(); + Assertions.assertNotNull(StpUtil.stpLogic.getTokenSession(false)); + SaSession session2 = dao.getSession("satoken:login:token-session:" + token); + Assertions.assertNotNull(session2); + + // + SaSession tokenSession = StpUtil.getTokenSession(); + SaSession tokenSession2 = StpUtil.getTokenSessionByToken(token); + Assertions.assertEquals(tokenSession.getId(), tokenSession2.getId()); + } + + // 测试:根据账号id踢人 + @Test + public void kickoutByLoginId() { + + // 踢人下线 + StpUtil.login(10001); + String token = StpUtil.getTokenValue(); + StpUtil.kickout(10001); + + // token 应该被打标记 + Assertions.assertEquals(dao.get("satoken:login:token:" + token), NotLoginException.KICK_OUT); + + // 场景值应该是token已被踢下线 + try { + StpUtil.checkLogin(); + } catch (NotLoginException e) { + Assertions.assertEquals(e.getType(), NotLoginException.KICK_OUT); + } + } + + // 测试:账号封禁 + @Test + public void testDisable() { + // 封号 + StpUtil.disable(10007, 200); + Assertions.assertTrue(StpUtil.isDisable(10007)); + Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10007), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); + + // 封号后检测一下 (会抛出 DisableLoginException 异常) + Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10007)); + + // 封号时间 + long disableTime = StpUtil.getDisableTime(10007); + Assertions.assertTrue(disableTime <= 200 && disableTime >= 199); + + // 解封 + StpUtil.untieDisable(10007); + Assertions.assertFalse(StpUtil.isDisable(10007)); + Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10007), null); + Assertions.assertDoesNotThrow(() -> StpUtil.checkDisable(10007)); + } + + // 测试:分类封禁 + @Test + public void testDisableService() { + // 封掉评论功能 + StpUtil.disable(10008, "comment", 200); + Assertions.assertTrue(StpUtil.isDisable(10008, "comment")); + Assertions.assertEquals(dao.get("satoken:login:disable:comment:" + 10008), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); + Assertions.assertNull(dao.get("satoken:login:disable:login:" + 10008)); + + // 封号后检测一下 + Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10008, "comment")); + // 检查多个,有一个不通过就报异常 + Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10008, "comment", "login")); + + // 封号时间 + long disableTime = StpUtil.getDisableTime(10008, "comment"); + Assertions.assertTrue(disableTime <= 200 && disableTime >= 199); + + // 解封 (不加服务名不会成功) + StpUtil.untieDisable(10008); + Assertions.assertTrue(StpUtil.isDisable(10008, "comment")); + Assertions.assertNotNull(dao.get("satoken:login:disable:comment:" + 10008)); + + // 解封 (加服务名才会成功) + StpUtil.untieDisable(10008, "comment"); + Assertions.assertFalse(StpUtil.isDisable(10008, "comment")); + Assertions.assertEquals(dao.get("satoken:login:disable:comment:" + 10008), null); + Assertions.assertDoesNotThrow(() -> StpUtil.checkDisable(10007, "comment")); + } + + // 测试:阶梯封禁 + @Test + public void testDisableLevel() { + // 封禁等级5 + StpUtil.disableLevel(10009, 5, 200); + Assertions.assertTrue(StpUtil.isDisableLevel(10009, 3)); + Assertions.assertTrue(StpUtil.isDisableLevel(10009, 5)); + // 未达到7级 + Assertions.assertFalse(StpUtil.isDisableLevel(10009, 7)); + // 账号未封禁 + Assertions.assertFalse(StpUtil.isDisableLevel(20009, 3)); + + // dao中应该有值 + Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10009), String.valueOf(5)); + + // 封号后检测一下 + Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10009, 3)); + Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10009, 5)); + // 未达到等级,不抛出异常 + Assertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(10009, 7)); + // 账号未被封禁,不抛出异常 + Assertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(20009, 3)); + + // 封号等级 + Assertions.assertEquals(StpUtil.getDisableLevel(10009), 5); + Assertions.assertEquals(StpUtil.getDisableLevel(20009), -2); + + // 解封 + StpUtil.untieDisable(10009); + Assertions.assertFalse(StpUtil.isDisable(10009)); + Assertions.assertFalse(StpUtil.isDisableLevel(10009, 5)); + Assertions.assertNull(dao.get("satoken:login:disable:login:" + 10009)); + } + + // 测试:分类封禁 + 阶梯封禁 + @Test + public void testDisableServiceLevel() { + // 封禁服务 shop,等级5 + StpUtil.disableLevel(10010, "shop", 5, 200); + Assertions.assertTrue(StpUtil.isDisableLevel(10010, "shop", 3)); + Assertions.assertTrue(StpUtil.isDisableLevel(10010, "shop", 5)); + // 未达到7级 + Assertions.assertFalse(StpUtil.isDisableLevel(10010, "shop", 7)); + // 账号未封禁 + Assertions.assertFalse(StpUtil.isDisableLevel(20010, "shop", 3)); + // 服务名不对 + Assertions.assertFalse(StpUtil.isDisableLevel(10010, "shop2", 5)); + + // dao中应该有值 + Assertions.assertEquals(dao.get("satoken:login:disable:shop:" + 10010), String.valueOf(5)); + + // 封号后检测一下 + Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10010, "shop", 3)); + Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10010, "shop", 5)); + // 未达到等级,不抛出异常 + Assertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(10010, "shop", 7)); + // 账号未被封禁,不抛出异常 + Assertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(20010, "shop", 3)); + + // 封号等级 + Assertions.assertEquals(StpUtil.getDisableLevel(10010, "shop"), 5); + Assertions.assertEquals(StpUtil.getDisableLevel(10010, "shop2"), -2); + Assertions.assertEquals(StpUtil.getDisableLevel(20010, "shop"), -2); + + // 解封 + StpUtil.untieDisable(10010, "shop"); + Assertions.assertFalse(StpUtil.isDisable(10010, "shop")); + Assertions.assertFalse(StpUtil.isDisableLevel(10010, "shop", 5)); + Assertions.assertNull(dao.get("satoken:login:disable:shop:" + 10010)); + } + + // 测试:身份切换 + @Test + public void testSwitch() { + // 登录 + StpUtil.login(10001); + Assertions.assertFalse(StpUtil.isSwitch()); + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); + + // 开始身份切换 + StpUtil.switchTo(10044); + Assertions.assertTrue(StpUtil.isSwitch()); + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10044); + + // 开始身份切换 Lambda 方式 + StpUtil.switchTo(10045, () -> { + Assertions.assertTrue(StpUtil.isSwitch()); + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10045); + }); + + // 结束切换 + StpUtil.endSwitch(); + Assertions.assertFalse(StpUtil.isSwitch()); + Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); + } + + // 测试:会话管理 + @Test + public void testSearchTokenValue() { + // 登录 + StpUtil.login(10001); + StpUtil.login(10002); + StpUtil.login(10003); + StpUtil.login(10004); + StpUtil.login(10005); + + // 查询 Token 列表 + List list = StpUtil.searchTokenValue("", 0, 10, true); + Assertions.assertTrue(list.size() >= 5); + + // 查询 Session 列表 + List list2 = StpUtil.searchSessionId("", 0, 10, true); + Assertions.assertTrue(list2.size() >= 5); + list2.stream().forEach(sessionId -> { + Assertions.assertNotNull(StpUtil.getSessionBySessionId(sessionId)); + }); + } + + // 测试:会话管理(Token-Session) + @Test + public void testSearchTokenSession() { + // 登录 + StpUtil.login(10001); + StpUtil.getTokenSession(); + StpUtil.login(10002); + StpUtil.getTokenSession(); + StpUtil.login(10003); + StpUtil.getTokenSession(); + StpUtil.login(10004); + StpUtil.getTokenSession(); + StpUtil.login(10005); + StpUtil.getTokenSession(); + + // 查询 Token-Session 列表 + List list2 = StpUtil.searchTokenSessionId("", 0, 10, true); + Assertions.assertTrue(list2.size() >= 5); + list2.stream().forEach(sessionId -> { + Assertions.assertNotNull(StpUtil.getSessionBySessionId(sessionId)); + }); + } + + // 测试:二级认证 + @Test + public void testSafe() { + // 登录 + StpUtil.login(10001); + Assertions.assertFalse(StpUtil.isSafe()); + + // 开启二级认证 + StpUtil.openSafe(2); + Assertions.assertTrue(StpUtil.isSafe()); + Assertions.assertTrue(StpUtil.getSafeTime() > 0); + StpUtil.checkSafe(); + + // 自然结束 +// Thread.sleep(2500); +// Assertions.assertFalse(StpUtil.isSafe()); + + // 手动结束 +// StpUtil.openSafe(2); + StpUtil.closeSafe(); + Assertions.assertFalse(StpUtil.isSafe()); + + // 抛异常 + Assertions.assertThrows(NotSafeException.class, () -> StpUtil.checkSafe()); + } + + + // ------------- 复杂点的 + + // 测试:指定设备登录 + @Test + public void testDoLoginByDevice() { + StpUtil.login(10001, "PC"); + Assertions.assertEquals(StpUtil.getLoginDevice(), "PC"); + + // 指定一个其它的设备注销,应该注销不掉 + StpUtil.logout(10001, "APP"); + Assertions.assertTrue(StpUtil.isLogin()); + + // 指定当前设备踢掉,则能够踢掉 + StpUtil.kickout(10001, "PC"); + Assertions.assertFalse(StpUtil.isLogin()); + + // 顶掉 + StpUtil.login(10001, "PC"); + StpUtil.replaced(10001, "PC"); + Assertions.assertFalse(StpUtil.isLogin()); + try { + StpUtil.checkLogin(); + } catch (NotLoginException e) { + // 场景值应该为-4 + Assertions.assertEquals(e.getType(), NotLoginException.BE_REPLACED); + } + } + + // 测试:指定 timeout 登录 + @Test + public void testDoLoginByTimeout() { + + // 指定timeout 登录 + StpUtil.login(10001, 100); + long timeout = StpUtil.getTokenTimeout(); + Assertions.assertTrue(timeout <= 100 && timeout >= 99); + + // 续期一下 + StpUtil.renewTimeout(200); + timeout = StpUtil.getTokenTimeout(); + Assertions.assertTrue(timeout <= 200 && timeout >= 199); + + // 续期一下 + StpUtil.renewTimeout(StpUtil.getTokenValue(), 300); + timeout = StpUtil.getTokenTimeout(); + Assertions.assertTrue(timeout <= 300 && timeout >= 299); + + // Session 也会续期 + timeout = StpUtil.getSessionTimeout(); + Assertions.assertTrue(timeout >= 299); + + StpUtil.getTokenSession(); + timeout = StpUtil.getTokenSessionTimeout(); + Assertions.assertTrue(timeout >= 299); + + + // 注销后,就是-2 + StpUtil.logout(); + timeout = StpUtil.getTokenTimeout(); + Assertions.assertTrue(timeout == SaTokenDao.NOT_VALUE_EXPIRE); + } + + // 测试:预定 Token 登录 + @Test + public void testDoLoginBySetToken() { + // 预定 Token 登录 + StpUtil.login(10001, new SaLoginParameter().setToken("qwer-qwer-qwer-qwer")); + Assertions.assertEquals(StpUtil.getTokenValue(), "qwer-qwer-qwer-qwer"); + + // 注销后,应该清除Token + StpUtil.logout(); + Assertions.assertNull(StpUtil.getTokenValue()); + } + + // 测试:无上下文注入的登录 + @Test + public void testCreateLoginSession() { + + // 无上下文注入的登录 + StpUtil.createLoginSession(10001); + Assertions.assertNull(StpUtil.getTokenValue()); + + // 无上下文注入的登录 + String token = StpUtil.createLoginSession(10001, new SaLoginParameter()); + Assertions.assertNull(StpUtil.getTokenValue()); + + // 手动写入 + StpUtil.setTokenValue(token); + Assertions.assertNotNull(StpUtil.getTokenValue()); + + // 手动写入到Cookie + StpUtil.setTokenValue(token, 10); + Assertions.assertNotNull(StpUtil.getTokenValue()); + } + + // 测试,匿名 Token-Session + @Test + public void testAnonTokenSession() { + // token 不存在 + StpUtil.logout(); + Assertions.assertNull(StpUtil.getTokenValue()); + + // token 存在 + SaSession anonTokenSession = StpUtil.getAnonTokenSession(); + String token = StpUtil.getTokenValue(); + Assertions.assertNotNull(token); + // 写个值 + anonTokenSession.set("code", "123456"); + + // 登录时,预定上 + StpUtil.login(10001, SaLoginConfig.setToken(token)); + // token不变 + Assertions.assertEquals(token, StpUtil.getTokenValue()); + + // Token-Session 存在,且不变 + SaSession tokenSession = StpUtil.getTokenSession(); + Assertions.assertEquals(anonTokenSession.getId(), tokenSession.getId()); + + // 刚才写的值,仍然在 + Assertions.assertEquals(tokenSession.get("code"), "123456"); + } + + // 测试,token 最低活跃频率 + @Test + public void testActiveTimeout() { + // 登录 + StpUtil.login(10001); + Assertions.assertNotNull(StpUtil.getTokenValue()); + + // 默认跟随全局 timeout + StpUtil.updateLastActiveToNow(); + long activeTimeout = StpUtil.getTokenActiveTimeout(); + Assertions.assertTrue(activeTimeout <=180 || activeTimeout >=179); + + // 不会抛出异常 + Assertions.assertDoesNotThrow(() -> StpUtil.checkActiveTimeout()); + } + + // 测试,上下文 API + @Test + public void testSaTokenContext() { + SaTokenContext context = SaHolder.getContext(); + // path 匹配 + // Assertions.assertTrue(context.matchPath("/user/**", "/user/add")); + // context 是否有效 + Assertions.assertTrue(context.isValid()); + // 是否为web环境 + Assertions.assertTrue(SpringMVCUtil.isWeb()); + // pathMatcher + // Assertions.assertEquals(pathMatcher, SaPathMatcherHolder.getPathMatcher()); + // 自创建 + SaPathMatcherHolder.pathMatcher = null; + Assertions.assertNotNull(SaPathMatcherHolder.getPathMatcher()); + SaPathMatcherHolder.pathMatcher = pathMatcher; + } + + // 测试json转换 + @Test + public void testSaJsonTemplate() { + SaJsonTemplate saJsonTemplate = SaManager.getSaJsonTemplate(); + + // map 转 json + SoMap map = SoMap.getSoMap("name", "zhangsan"); + String jsonString = saJsonTemplate.objectToJson(map); + Assertions.assertEquals(jsonString, "{\"name\":\"zhangsan\"}"); + + // 抛异常 + // Assertions.assertThrows(SaJsonConvertException.class, () -> saJsonTemplate.objectToJson(new Object())); + + // json 转 map + Map map2 = saJsonTemplate.jsonToMap("{\"name\":\"zhangsan\"}"); + Assertions.assertEquals(map2.get("name"), "zhangsan"); + + // 抛异常 + Assertions.assertThrows(SaJsonConvertException.class, () -> saJsonTemplate.jsonToMap("x")); + } + + // 测试过滤器、拦截器 基础API + @Test + public void testFilter() throws IOException, ServletException { + // 过滤器 + SaServletFilter filter = new SaServletFilter() + .addInclude("/**") + .setIncludeList(Arrays.asList("/**")) + .addExclude("/favicon.ico") + .setExcludeList(Arrays.asList("/favicon.ico")) + .setAuth(obj -> {}) + .setBeforeAuth(obj -> {}) + ; + Assertions.assertEquals(filter.includeList.get(0), "/**"); + Assertions.assertEquals(filter.excludeList.get(0), "/favicon.ico"); + // 以下功能无法测试 + filter.init(null); + filter.doFilter(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse(), new MockFilterChain()); + filter.destroy(); + Assertions.assertThrows(SaTokenException.class, () -> filter.error.run(new SaTokenException("xxx"))); + + filter.setError(e -> e.getMessage()); + Assertions.assertEquals(filter.error.run(new SaTokenException("msg")), "msg"); + } + + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/ManyLoginTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/ManyLoginTest.java new file mode 100644 index 000000000..13703ae3e --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/ManyLoginTest.java @@ -0,0 +1,199 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.springboot; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.config.SaTokenConfig; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; +import cn.dev33.satoken.session.SaTerminalInfo; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +/** + * Sa-Token 多端登录测试 + * + * @author click33 + * + */ +@SpringBootTest(classes = StartUpApplication.class) +public class ManyLoginTest { + + // 持久化Bean + @Autowired(required = false) + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n------------ 多端登录测试 star ..."); + } + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n---------- 多端登录测试 end ... \n"); + } + + @BeforeEach + public void beforeEach() { + SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); + } + + @AfterEach + public void afterEach() { + SaTokenContextServletUtil.clearContext(); + } + + // 测试:并发登录、共享token、同端 + @Test + public void login() { + SaManager.setConfig(new SaTokenConfig().setIsShare(true)); + + StpUtil.login(10001); + String token1 = StpUtil.getTokenValue(); + + StpUtil.login(10001); + String token2 = StpUtil.getTokenValue(); + + Assertions.assertEquals(token1, token2); + } + + // 测试:并发登录、共享token、不同端 + @Test + public void login2() { + SaManager.setConfig(new SaTokenConfig()); + + StpUtil.login(10001, "APP"); + String token1 = StpUtil.getTokenValue(); + + StpUtil.login(10001, "PC"); + String token2 = StpUtil.getTokenValue(); + + Assertions.assertNotEquals(token1, token2); + } + + // 测试:并发登录、不共享token + @Test + public void login3() { + SaManager.setConfig(new SaTokenConfig().setIsShare(false)); + + StpUtil.login(10001); + String token1 = StpUtil.getTokenValue(); + + StpUtil.login(10001); + String token2 = StpUtil.getTokenValue(); + + Assertions.assertNotEquals(token1, token2); + } + + // 测试:禁并发登录,后者顶出前者 + @Test + public void login4() { + SaManager.setConfig(new SaTokenConfig().setIsConcurrent(false)); + + StpUtil.login(10001); + String token1 = StpUtil.getTokenValue(); + + StpUtil.login(10001); + String token2 = StpUtil.getTokenValue(); + + // token不同 + Assertions.assertNotEquals(token1, token2); + + // token1会被标记为:已被顶下线 + Assertions.assertEquals(dao.get("satoken:login:token:" + token1), "-4"); + + // Account-Session里的 token1 签名会被移除 + List terminalList = StpUtil.getSessionByLoginId(10001).getTerminalList(); + for (SaTerminalInfo terminal : terminalList) { + Assertions.assertNotEquals(terminal.getTokenValue(), token1); + } + } + + // 测试:多端登录,一起强制注销 + @Test + public void login5() { + SaManager.setConfig(new SaTokenConfig()); + + StpUtil.login(10001, "APP"); + String token1 = StpUtil.getTokenValue(); + + StpUtil.login(10001, "PC"); + String token2 = StpUtil.getTokenValue(); + + StpUtil.login(10001, "h5"); + String token3 = StpUtil.getTokenValue(); + + // 注销 + StpUtil.logout(10001); + + // 三个Token应该全部无效 + Assertions.assertNull(dao.get("satoken:login:token:" + token1)); + Assertions.assertNull(dao.get("satoken:login:token:" + token2)); + Assertions.assertNull(dao.get("satoken:login:token:" + token3)); + + // Account-Session也应该被清除掉 + Assertions.assertNull(StpUtil.getSessionByLoginId(10001, false)); + Assertions.assertNull(dao.getSession("satoken:login:session:" + 10001)); + } + + // 测试:多端登录,一起强制踢下线 + @Test + public void login6() { + SaManager.setConfig(new SaTokenConfig()); + + StpUtil.login(10001, "APP"); + String token1 = StpUtil.getTokenValue(); + + StpUtil.login(10001, "PC"); + String token2 = StpUtil.getTokenValue(); + + StpUtil.login(10001, "h5"); + String token3 = StpUtil.getTokenValue(); + + // 注销 + StpUtil.kickout(10001); + + // 三个Token应该全部无效 + Assertions.assertEquals(dao.get("satoken:login:token:" + token1), "-5"); + Assertions.assertEquals(dao.get("satoken:login:token:" + token2), "-5"); + Assertions.assertEquals(dao.get("satoken:login:token:" + token3), "-5"); + + // Account-Session也应该被清除掉 + Assertions.assertNull(StpUtil.getSessionByLoginId(10001, false)); + Assertions.assertNull(dao.getSession("satoken:login:session:" + 10001)); + } + + // 测试:多账号模式,在一个账号体系里登录成功,在另一个账号体系不会校验通过 + @Test + public void login7() { + SaManager.setConfig(new SaTokenConfig()); + + StpUtil.login(10001); + String token1 = StpUtil.getTokenValue(); + + StpLogic stp = new StpLogic("user"); + + Assertions.assertNotNull(StpUtil.getLoginIdByToken(token1)); + Assertions.assertNull(stp.getLoginIdByToken(token1)); + } +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SaPathMatcherTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SaPathMatcherTest.java new file mode 100644 index 000000000..bdef5479d --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SaPathMatcherTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.springboot; + +import cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder; +import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil; +import cn.dev33.satoken.spring.pathmatch.SaPatternsRequestConditionHolder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringBootVersion; + +/** + * SaPathMatcher 路由匹配测试 + * + * @author click33 + * + */ +public class SaPathMatcherTest { + + // 开始 + @BeforeAll + public static void beforeClass() { + + } + + // 结束 + @AfterAll + public static void afterClass() { + + } + + // 测试,SaPathMatcherHolder + @Test + public void testSaPathMatcherHolder() { + Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/get", "/user/get")); + Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/*", "/user/get")); + Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/**", "/user/get/list")); + Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/**/page", "/user/get/list/page")); + Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/get/{id}", "/user/get/123")); + Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/get/{id}/page", "/user/get/123/page")); + Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/*.js", "/sa.js")); + Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/**/*.js", "/user/sa.js")); + + // SaPathMatcherHolder 无法匹配斜杠后缀 + Assertions.assertFalse(SaPathMatcherHolder.getPathMatcher().match("/user/get", "/user/get/")); + } + + // 测试,SaPatternsRequestConditionHolder + @Test + public void testSaPatternsRequestConditionHolder() { + + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/get", "/user/get")); + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/*", "/user/get")); + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/**", "/user/get/list")); + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/**/page", "/user/get/list/page")); + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/get/{id}", "/user/get/123")); + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/get/{id}/page", "/user/get/123/page")); + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/*.js", "/sa.js")); + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/**/*.js", "/user/sa.js")); + + // SaPatternsRequestConditionHolder 可匹配斜杠后缀 + Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/get", "/user/get/")); + } + + // 测试,testSaPathPatternParserUtil + @Test + public void testSaPathPatternParserUtil() { + + Assertions.assertTrue(SaPathPatternParserUtil.match("/user/get", "/user/get")); + Assertions.assertTrue(SaPathPatternParserUtil.match("/user/*", "/user/get")); + Assertions.assertTrue(SaPathPatternParserUtil.match("/user/**", "/user/get/list")); + // PathPatternParser 不允许 ** 后面还有内容 + // Assertions.assertTrue(SaPathPatternParserUtil.match("/user/**/page", "/user/get/list/page")); + Assertions.assertTrue(SaPathPatternParserUtil.match("/user/get/{id}", "/user/get/123")); + Assertions.assertTrue(SaPathPatternParserUtil.match("/user/get/{id}/page", "/user/get/123/page")); + Assertions.assertTrue(SaPathPatternParserUtil.match("/*.js", "/sa.js")); + // Assertions.assertTrue(SaPathPatternParserUtil.match("/user/**/*.js", "/user/sa.js")); + + // SaPathPatternParserUtil + // 在 springboot2.x 版本下 可匹配斜杠后缀 + // 在 springboot3.x 版本下 不可匹配斜杠后缀 + if(SpringBootVersion.getVersion().startsWith("2.")) { + Assertions.assertTrue(SaPathPatternParserUtil.match("/user/get", "/user/get/")); + } + if(SpringBootVersion.getVersion().startsWith("3.")) { + Assertions.assertFalse(SaPathPatternParserUtil.match("/user/get", "/user/get/")); + } + } + + + + + + + + + + + + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SpringMVCUtilTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SpringMVCUtilTest.java new file mode 100644 index 000000000..1037797c9 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SpringMVCUtilTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.springboot; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.spring.SpringMVCUtil; + +/** + * SpringMVCUtil 测试 + * + * @author click33 + * + */ +public class SpringMVCUtilTest { + + // 开始 + @BeforeAll + public static void beforeClass() { + + } + + // 结束 + @AfterAll + public static void afterClass() { + + } + + // 测试,上下文 API + @Test + public void testSaTokenContext() { + Assertions.assertThrows(SaTokenException.class, () -> SpringMVCUtil.getRequest()); + Assertions.assertThrows(SaTokenException.class, () -> SpringMVCUtil.getResponse()); + Assertions.assertFalse(SpringMVCUtil.isWeb()); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/StartUpApplication.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/StartUpApplication.java new file mode 100644 index 000000000..2c53b5687 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/StartUpApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 启动类 + * @author Auster + * + */ +@SpringBootApplication +public class StartUpApplication { + public static void main(String[] args) { + SpringApplication.run(StartUpApplication.class, args); + } +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/satoken/StpInterfaceImpl.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/satoken/StpInterfaceImpl.java new file mode 100644 index 000000000..2033d7500 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/satoken/StpInterfaceImpl.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.springboot.satoken; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.stp.StpInterface; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 自定义权限验证接口扩展 + * + * @author Auster + * + */ +@Component +public class StpInterfaceImpl implements StpInterface { + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + int id = SaFoxUtil.getValueByType(loginId, int.class); + if(id == 10001) { + return Arrays.asList("user*", "art-add", "art-delete", "art-update", "art-get"); + } else { + return null; + } + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + int id = SaFoxUtil.getValueByType(loginId, int.class); + if(id == 10001) { + return Arrays.asList("admin", "super-admin"); + } else { + return null; + } + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/util/SoMap.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/util/SoMap.java new file mode 100644 index 000000000..f1a04bdc6 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/util/SoMap.java @@ -0,0 +1,762 @@ +/* + * Copyright 2020-2099 sa-token.com + * + * 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.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦 + *

    所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用 + *

    最新:2020-12-10 新增部分构造方法 + * @author click33 + */ +public class SoMap extends LinkedHashMap { + + private static final long serialVersionUID = 1L; + + public SoMap() { + } + + + /** 以下元素会在isNull函数中被判定为Null, */ + public static final Object[] NULL_ELEMENT_ARRAY = {null, ""}; + public static final List NULL_ELEMENT_LIST; + + + static { + NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY); + } + + // ============================= 读值 ============================= + + /** 获取一个值 */ + @Override + public Object get(Object key) { + if("this".equals(key)) { + return this; + } + return super.get(key); + } + + /** 如果为空,则返回默认值 */ + public Object get(Object key, Object defaultValue) { + Object value = get(key); + if(valueIsNull(value)) { + return defaultValue; + } + return value; + } + + /** 转为String并返回 */ + public String getString(String key) { + Object value = get(key); + if(value == null) { + return null; + } + return String.valueOf(value); + } + + /** 如果为空,则返回默认值 */ + public String getString(String key, String defaultValue) { + Object value = get(key); + if(valueIsNull(value)) { + return defaultValue; + } + return String.valueOf(value); + } + + /** 转为int并返回 */ + public int getInt(String key) { + Object value = get(key); + if(valueIsNull(value)) { + return 0; + } + return Integer.valueOf(String.valueOf(value)); + } + /** 转为int并返回,同时指定默认值 */ + public int getInt(String key, int defaultValue) { + Object value = get(key); + if(valueIsNull(value)) { + return defaultValue; + } + return Integer.valueOf(String.valueOf(value)); + } + + /** 转为long并返回 */ + public long getLong(String key) { + Object value = get(key); + if(valueIsNull(value)) { + return 0; + } + return Long.valueOf(String.valueOf(value)); + } + + /** 转为double并返回 */ + public double getDouble(String key) { + Object value = get(key); + if(valueIsNull(value)) { + return 0.0; + } + return Double.valueOf(String.valueOf(value)); + } + + /** 转为boolean并返回 */ + public boolean getBoolean(String key) { + Object value = get(key); + if(valueIsNull(value)) { + return false; + } + return Boolean.valueOf(String.valueOf(value)); + } + + /** 转为Date并返回,根据自定义格式 */ + public Date getDateByFormat(String key, String format) { + try { + return new SimpleDateFormat(format).parse(getString(key)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** 转为Date并返回,根据格式: yyyy-MM-dd */ + public Date getDate(String key) { + return getDateByFormat(key, "yyyy-MM-dd"); + } + + /** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */ + public Date getDateTime(String key) { + return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss"); + } + + /** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */ + @SuppressWarnings("unchecked") + public List getList(String key) { + Object value = get(key); + List list = null; + if(value == null || value.equals("")) { + list = new ArrayList(); + } + else if(value instanceof List) { + list = (List)value; + } else { + list = new ArrayList(); + list.add(value); + } + return list; + } + + /** 获取集合 (指定泛型类型) */ + public List getList(String key, Class cs) { + List list = getList(key); + List list2 = new ArrayList(); + for (Object obj : list) { + T objC = getValueByClass(obj, cs); + list2.add(objC); + } + return list2; + } + + /** 获取集合(逗号分隔式),(指定类型) */ + public List getListByComma(String key, Class cs) { + String listStr = getString(key); + if(listStr == null || listStr.equals("")) { + return new ArrayList<>(); + } + // 开始转化 + String [] arr = listStr.split(","); + List list = new ArrayList(); + for (String str : arr) { + if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) { + str = str.trim(); + } + T objC = getValueByClass(str, cs); + list.add(objC); + } + return list; + } + + + /** 根据指定类型从map中取值,返回实体对象 */ + public T getModel(Class cs) { + try { + return getModelByObject(cs.newInstance()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** 从map中取值,塞到一个对象中 */ + public T getModelByObject(T obj) { + // 获取类型 + Class cs = obj.getClass(); + // 循环复制 + for (Field field : cs.getDeclaredFields()) { + try { + // 获取对象 + Object value = this.get(field.getName()); + if(value == null) { + continue; + } + field.setAccessible(true); + Object valueConvert = getValueByClass(value, field.getType()); + field.set(obj, valueConvert); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException("属性取值出错:" + field.getName(), e); + } + } + return obj; + } + + + + /** + * 将指定值转化为指定类型并返回 + * @param obj + * @param cs + * @param + * @return + */ + @SuppressWarnings("unchecked") + public static T getValueByClass(Object obj, Class cs) { + String obj2 = String.valueOf(obj); + Object obj3 = null; + if (cs.equals(String.class)) { + obj3 = obj2; + } else if (cs.equals(int.class) || cs.equals(Integer.class)) { + obj3 = Integer.valueOf(obj2); + } else if (cs.equals(long.class) || cs.equals(Long.class)) { + obj3 = Long.valueOf(obj2); + } else if (cs.equals(short.class) || cs.equals(Short.class)) { + obj3 = Short.valueOf(obj2); + } else if (cs.equals(byte.class) || cs.equals(Byte.class)) { + obj3 = Byte.valueOf(obj2); + } else if (cs.equals(float.class) || cs.equals(Float.class)) { + obj3 = Float.valueOf(obj2); + } else if (cs.equals(double.class) || cs.equals(Double.class)) { + obj3 = Double.valueOf(obj2); + } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) { + obj3 = Boolean.valueOf(obj2); + } else { + obj3 = (T)obj; + } + return (T)obj3; + } + + + // ============================= 写值 ============================= + + /** + * 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去) + */ + public void setDefaultValue(String key, Object defaultValue) { + if(isNull(key)) { + set(key, defaultValue); + } + } + + /** set一个值,连缀风格 */ + public SoMap set(String key, Object value) { + // 防止敏感key + if(key.toLowerCase().equals("this")) { + return this; + } + put(key, value); + return this; + } + + /** 将一个Map塞进SoMap */ + public SoMap setMap(Map map) { + if(map != null) { + for (String key : map.keySet()) { + this.set(key, map.get(key)); + } + } + return this; + } + + /** 将一个对象解析塞进SoMap */ + public SoMap setModel(Object model) { + if(model == null) { + return this; + } + Field[] fields = model.getClass().getDeclaredFields(); + for (Field field : fields) { + try{ + field.setAccessible(true); + boolean isStatic = Modifier.isStatic(field.getModifiers()); + if(!isStatic) { + this.set(field.getName(), field.get(model)); + } + }catch (Exception e){ + throw new RuntimeException(e); + } + } + return this; + } + + /** 将json字符串解析后塞进SoMap */ + public SoMap setJsonString(String jsonString) { + try { + @SuppressWarnings("unchecked") + Map map = new ObjectMapper().readValue(jsonString, Map.class); + return this.setMap(map); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + // ============================= 删值 ============================= + + /** delete一个值,连缀风格 */ + public SoMap delete(String key) { + remove(key); + return this; + } + + /** 清理所有value为null的字段 */ + public SoMap clearNull() { + Iterator iterator = this.keySet().iterator(); + while(iterator.hasNext()) { + String key = iterator.next(); + if(this.isNull(key)) { + iterator.remove(); + this.remove(key); + } + + } + return this; + } + /** 清理指定key */ + public SoMap clearIn(String ...keys) { + List keys2 = Arrays.asList(keys); + Iterator iterator = this.keySet().iterator(); + while(iterator.hasNext()) { + String key = iterator.next(); + if(keys2.contains(key) == true) { + iterator.remove(); + this.remove(key); + } + } + return this; + } + /** 清理掉不在列表中的key */ + public SoMap clearNotIn(String ...keys) { + List keys2 = Arrays.asList(keys); + Iterator iterator = this.keySet().iterator(); + while(iterator.hasNext()) { + String key = iterator.next(); + if(keys2.contains(key) == false) { + iterator.remove(); + this.remove(key); + } + + } + return this; + } + /** 清理掉所有key */ + public SoMap clearAll() { + clear(); + return this; + } + + + // ============================= 快速构建 ============================= + + /** 构建一个SoMap并返回 */ + public static SoMap getSoMap() { + return new SoMap(); + } + /** 构建一个SoMap并返回 */ + public static SoMap getSoMap(String key, Object value) { + return new SoMap().set(key, value); + } + /** 构建一个SoMap并返回 */ + public static SoMap getSoMap(Map map) { + return new SoMap().setMap(map); + } + + /** 将一个对象集合解析成为SoMap */ + public static SoMap getSoMapByModel(Object model) { + return SoMap.getSoMap().setModel(model); + } + + /** 将一个对象集合解析成为SoMap集合 */ + public static List getSoMapByList(List list) { + List listMap = new ArrayList(); + for (Object model : list) { + listMap.add(getSoMapByModel(model)); + } + return listMap; + } + + /** 克隆指定key,返回一个新的SoMap */ + public SoMap cloneKeys(String... keys) { + SoMap so = new SoMap(); + for (String key : keys) { + so.set(key, this.get(key)); + } + return so; + } + /** 克隆所有key,返回一个新的SoMap */ + public SoMap cloneSoMap() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(key, this.get(key)); + } + return so; + } + + /** 将所有key转为大写 */ + public SoMap toUpperCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(key.toUpperCase(), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + /** 将所有key转为小写 */ + public SoMap toLowerCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(key.toLowerCase(), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + /** 将所有key中下划线转为中划线模式 (kebab-case风格) */ + public SoMap toKebabCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(wordEachKebabCase(key), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + /** 将所有key中下划线转为小驼峰模式 */ + public SoMap toHumpCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(wordEachBigFs(key), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + /** 将所有key中小驼峰转为下划线模式 */ + public SoMap humpToLineCase() { + SoMap so = new SoMap(); + for (String key : this.keySet()) { + so.set(wordHumpToLine(key), this.get(key)); + } + this.clearAll().setMap(so); + return this; + } + + + + + // ============================= 辅助方法 ============================= + + + /** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */ + public boolean isNull(String key) { + return valueIsNull(get(key)); + } + + /** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */ + public boolean isContainNull(String ...keys) { + for (String key : keys) { + if(this.isNull(key)) { + return true; + } + } + return false; + } + + /** 与isNull()相反 */ + public boolean isNotNull(String key) { + return !isNull(key); + } + /** 指定key的value是否为null,作用同isNotNull() */ + public boolean has(String key) { + return !isNull(key); + } + + /** 指定value在此SoMap的判断标准中是否为null */ + public boolean valueIsNull(Object value) { + return NULL_ELEMENT_LIST.contains(value); + } + + /** 验证指定key不为空,为空则抛出异常 */ + public SoMap checkNull(String ...keys) { + for (String key : keys) { + if(this.isNull(key)) { + throw new RuntimeException("参数" + key + "不能为空"); + } + } + return this; + } + + static Pattern patternNumber = Pattern.compile("[0-9]*"); + /** 指定key是否为数字 */ + public boolean isNumber(String key) { + String value = getString(key); + if(value == null) { + return false; + } + return patternNumber.matcher(value).matches(); + } + + + + + /** + * 转为JSON字符串 + */ + public String toJsonString() { + try { +// SoMap so = SoMap.getSoMap(this); + return new ObjectMapper().writeValueAsString(this); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +// +// /** +// * 转为JSON字符串, 带格式的 +// */ +// public String toJsonFormatString() { +// try { +// return JSON.toJSONString(this, true); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// } + + // ============================= web辅助 ============================= + + + /** + * 返回当前request请求的的所有参数 + * @return + */ + public static SoMap getRequestSoMap() { + // 大善人SpringMVC提供的封装 + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if(servletRequestAttributes == null) { + throw new RuntimeException("当前线程非JavaWeb环境"); + } + // 当前request + HttpServletRequest request = servletRequestAttributes.getRequest(); + if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) { + initRequestSoMap(request); + } + return (SoMap)request.getAttribute("currentSoMap"); + } + + /** 初始化当前request的 SoMap */ + private static void initRequestSoMap(HttpServletRequest request) { + SoMap soMap = new SoMap(); + Map parameterMap = request.getParameterMap(); // 获取所有参数 + for (String key : parameterMap.keySet()) { + try { + String[] values = parameterMap.get(key); // 获得values + if(values.length == 1) { + soMap.set(key, values[0]); + } else { + List list = new ArrayList(); + for (String v : values) { + list.add(v); + } + soMap.set(key, list); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + request.setAttribute("currentSoMap", soMap); + } + + /** + * 验证返回当前线程是否为JavaWeb环境 + * @return + */ + public static boolean isJavaWeb() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装 + if(servletRequestAttributes == null) { + return false; + } + return true; + } + + + + // ============================= 常见key (以下key经常用,所以封装以下,方便写代码) ============================= + + /** get 当前页 */ + public int getKeyPageNo() { + int pageNo = getInt("pageNo", 1); + if(pageNo <= 0) { + pageNo = 1; + } + return pageNo; + } + /** get 页大小 */ + public int getKeyPageSize() { + int pageSize = getInt("pageSize", 10); + if(pageSize <= 0 || pageSize > 1000) { + pageSize = 10; + } + return pageSize; + } + + /** get 排序方式 */ + public int getKeySortType() { + return getInt("sortType"); + } + + + + + + // ============================= 分页相关(封装mybatis的page-help插件 ) ============================= + +// /** 分页插件 */ +// private com.github.pagehelper.Page pagePlug; +// /** 分页插件 - 开始分页 */ +// public SoMap startPage() { +// this.pagePlug= com.github.pagehelper.PageHelper.startPage(getKeyPageNo(), getKeyPageSize()); +// return this; +// } +// /** 获取上次分页的记录总数 */ +// public long getDataCount() { +// if(pagePlug == null) { +// return -1; +// } +// return pagePlug.getTotal(); +// } +// /** 分页插件 - 结束分页, 返回总条数 (该方法已过时,请调用更加符合语义化的getDataCount() ) */ +// @Deprecated +// public long endPage() { +// return getDataCount(); +// } + + + + + + // ============================= 工具方法 ============================= + + + /** + * 将一个一维集合转换为树形集合 + * @param list 集合 + * @param idKey id标识key + * @param parentIdKey 父id标识key + * @param childListKey 子节点标识key + * @return 转换后的tree集合 + */ + public static List listToTree(List list, String idKey, String parentIdKey, String childListKey) { + // 声明新的集合,存储tree形数据 + List newTreeList = new ArrayList(); + // 声明hash-Map,方便查找数据 + SoMap hash = new SoMap(); + // 将数组转为Object的形式,key为数组中的id + for (int i = 0; i < list.size(); i++) { + SoMap json = (SoMap) list.get(i); + hash.put(json.getString(idKey), json); + } + // 遍历结果集 + for (int j = 0; j < list.size(); j++) { + // 单条记录 + SoMap aVal = (SoMap) list.get(j); + // 在hash中取出key为单条记录中pid的值 + SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString()); + // 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中 + if (hashVp != null) { + // 检查是否有child属性,有则添加,没有则新建 + if (hashVp.get(childListKey) != null) { + @SuppressWarnings("unchecked") + List ch = (List) hashVp.get(childListKey); + ch.add(aVal); + hashVp.put(childListKey, ch); + } else { + List ch = new ArrayList(); + ch.add(aVal); + hashVp.put(childListKey, ch); + } + } else { + newTreeList.add(aVal); + } + } + return newTreeList; + } + + + + /** 指定字符串的字符串下划线转大写模式 */ + private static String wordEachBig(String str){ + String newStr = ""; + for (String s : str.split("_")) { + newStr += wordFirstBig(s); + } + return newStr; + } + /** 返回下划线转小驼峰形式 */ + private static String wordEachBigFs(String str){ + return wordFirstSmall(wordEachBig(str)); + } + + /** 将指定单词首字母大写 */ + private static String wordFirstBig(String str) { + return str.substring(0, 1).toUpperCase() + str.substring(1, str.length()); + } + + /** 将指定单词首字母小写 */ + private static String wordFirstSmall(String str) { + return str.substring(0, 1).toLowerCase() + str.substring(1, str.length()); + } + + /** 下划线转中划线 */ + private static String wordEachKebabCase(String str) { + return str.replaceAll("_", "-"); + } + + /** 驼峰转下划线 */ + private static String wordHumpToLine(String str) { + return str.replaceAll("[A-Z]", "_$0").toLowerCase(); + } + + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/resources/application.yml b/sa-token-test/sa-token-springboot-test/src/test/resources/application.yml new file mode 100644 index 000000000..3b419c10a --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/resources/application.yml @@ -0,0 +1,10 @@ +# 端口 +server: + port: 8081 + +# Sa-Token配置 +sa-token: + # Token名称 (同时也是cookie名称) + token-name: satoken + + \ No newline at end of file diff --git a/sa-token-test/sa-token-springboot-test/src/test/resources/sa-token2.properties b/sa-token-test/sa-token-springboot-test/src/test/resources/sa-token2.properties new file mode 100644 index 000000000..e65a57f62 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/resources/sa-token2.properties @@ -0,0 +1,12 @@ +# token 名称 (同时也是 cookie 名称) +tokenName=use-token +# token 有效期(单位:秒) 默认30天,-1 代表永久有效 +timeout=9000 +# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 +activeTimeout=240 +# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) +isConcurrent=false +# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) +isShare=false +# 是否输出操作日志 +isLog=true \ No newline at end of file diff --git a/sa-token-test/sa-token-temp-jwt-test/pom.xml b/sa-token-test/sa-token-temp-jwt-test/pom.xml new file mode 100644 index 000000000..eaf0eff15 --- /dev/null +++ b/sa-token-test/sa-token-temp-jwt-test/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-test + ${revision} + ../pom.xml + + jar + + sa-token-temp-jwt-test + sa-token-temp-jwt-test + sa-token-temp-jwt-test + + + + cn.dev33 + sa-token-spring-boot-starter + + + cn.dev33 + sa-token-temp-jwt + + + + diff --git a/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/SaTempTemplateForJwtTest.java b/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/SaTempTemplateForJwtTest.java new file mode 100644 index 000000000..daf1fe327 --- /dev/null +++ b/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/SaTempTemplateForJwtTest.java @@ -0,0 +1,105 @@ +package com.pj.test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.exception.ApiDisabledException; +import cn.dev33.satoken.temp.SaTempUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Sa-Token 整合 temp jwt + * + * @author click33 + * @since 1.42.0 + */ +@SpringBootTest(classes = StartUpApplication.class) +public class SaTempTemplateForJwtTest { + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ SaTempTemplateForJwtTest star ..."); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ SaTempTemplateForJwtTest end ... \n"); + } + + // 测试:临时Token认证模块 + @Test + public void testSaTemp() { + + // 生成token + String token = SaTempUtil.createToken("group-1014", 200); + // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).timedCache.dataMap.keySet()); + // System.out.println("satoken:temp-token:" + ":" + token); + Assertions.assertNotNull(token); + + // 解析token + String value = SaTempUtil.parseToken(token, String.class); + Assertions.assertEquals(value, "group-1014"); + + // 解析 token 并裁剪前缀 + long value2 = SaTempUtil.parseToken(token, "group-", Long.class); + Assertions.assertEquals(value2, 1014); + + // 默认类型 + Object value3 = SaTempUtil.parseToken(token); + Assertions.assertEquals(value3, "group-1014"); + + // 转换类型 + String value4 = SaTempUtil.parseToken(token, String.class); + Assertions.assertEquals(value4, "group-1014"); + + // 过期时间 + long timeout = SaTempUtil.getTimeout(token); + Assertions.assertTrue(timeout > 195); + Assertions.assertTrue(timeout < 201); + + // 回收token + Assertions.assertThrows(ApiDisabledException.class, () -> SaTempUtil.deleteToken(token) ); + } + + // 测试:临时Token认证模块索引 + @Test + public void testSaTempIndex() { + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 生成token + String token1 = SaTempUtil.createToken("1001", 200, true); + String token2 = SaTempUtil.createToken("1001", 300, true); + String token3 = SaTempUtil.createToken("1001", 400, true); + + Assertions.assertNotNull(token1); + Assertions.assertNotNull(token2); + Assertions.assertNotNull(token3); + // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap); + + // 解析token + Assertions.assertEquals(SaTempUtil.parseToken(token1, String.class), "1001"); + Assertions.assertEquals(SaTempUtil.parseToken(token2, String.class), "1001"); + Assertions.assertEquals(SaTempUtil.parseToken(token3, String.class), "1001"); + + // 缓存数据比对 + Assertions.assertNull(dao.getObject("satoken:temp-token:" + token1)); + Assertions.assertNull(dao.getObject("satoken:temp-token:" + token2)); + Assertions.assertNull(dao.getObject("satoken:temp-token:" + token3)); + + // 索引 + Assertions.assertThrows(ApiDisabledException.class, () -> SaTempUtil.getTempTokenList("1001") ); + } + + @Test + public void testGetJwtSecretKey() { + // 秘钥默认为null + String jwtSecretKey = SaManager.getSaTempTemplate().getJwtSecretKey(); + Assertions.assertNotNull(jwtSecretKey); + } + +} diff --git a/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/StartUpApplication.java b/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/StartUpApplication.java new file mode 100644 index 000000000..2a174efdb --- /dev/null +++ b/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/StartUpApplication.java @@ -0,0 +1,16 @@ +package com.pj.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 启动类 + * @author Auster + * + */ +@SpringBootApplication +public class StartUpApplication { + public static void main(String[] args) { + SpringApplication.run(StartUpApplication.class, args); + } +} diff --git a/sa-token-test/sa-token-temp-jwt-test/src/test/resources/application.yml b/sa-token-test/sa-token-temp-jwt-test/src/test/resources/application.yml new file mode 100644 index 000000000..1b25a92c2 --- /dev/null +++ b/sa-token-test/sa-token-temp-jwt-test/src/test/resources/application.yml @@ -0,0 +1,46 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # jwt秘钥 + jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10000ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + \ No newline at end of file